merge: integrate Refactor sources and MSVC build with Dev documentation

This commit is contained in:
KOKO\Mimi
2026-10-18 13:15:31 +09:00
238 changed files with 53568 additions and 7659 deletions
+4
View File
@@ -27,3 +27,7 @@ phases/**/step*-output.json
# local document backups and initialization state
.dryforge/
# direct MSVC and CMake local build state
/.build/
/msvc/Fesa.Local.props
+15 -3
View File
@@ -1,7 +1,7 @@
# FESA Structural Solver
FESA는 개인 연구를 위해 개발하는 유한요소 구조해석 솔버이며 외부 사용자 배포를 장기
목표로 한다. C++17 이상·Windows MSVC·CMake/CTest를 사용하고 승인된 Abaqus 입력
목표로 한다. C++17 이상·Windows MSVC·직접 MSBuild와 CMake/CTest를 사용하고 승인된 Abaqus 입력
subset에서 재현 가능한 해석과 HDF5 결과를 제공한다. 코드 존재와 기능 검증 완료는 다르다.
## 프로젝트 문서와 영역
@@ -39,8 +39,12 @@ subset에서 재현 가능한 해석과 HDF5 결과를 제공한다. 코드 존
│ └── hdf5/AGENTS.md → schema·readback·finalize
├── tests/
│ └── AGENTS.md → 계층별 검사·실패·비교 evidence
└── cmake/
└── AGENTS.md → dependency·runtime 연결
├── cmake/
│ └── AGENTS.md → dependency·runtime 연결
├── msvc/
│ └── AGENTS.md → 직접 MSBuild·source 등록·runtime 연결
└── scripts/
└── AGENTS.md → 읽기 전용 결과 비교 도구
```
## 핵심 규칙
@@ -87,3 +91,11 @@ Public header 변경은 대응하는 src 영역의 책임과 consumer까지 확
문서만 변경하면 정적 검토를 하고 build/test를 실행하지 않는다. 동작 변경은 관련 검사부터,
솔버 기능 완료 시에는 구성된 전체 MSVC x64 Debug build/CTest와 필수 reference 비교를 수행한다.
직접 MSBuild가 주 개발 빌드이며 CMake/Ninja는 같은 제품·검사 source를 사용하는 보조 경로다.
실제 명령·환경·`.build/native/Debug` 및 `.build/cmake-debug` 출력은 solver-core를 따른다.
두 경로의 전체 검사 등록과 실제 실행 범위를 구분하고 별도 Python·reference 결과를 기록한다.
이번 병합은 2026-10-18 사용자 변경에 따라 정적해석·공통 계약 검사와 정식 정적5모델만
실행해 다음 단계로 진행한다. HHT·동해석·고유치·과도 열해석 실행은 미검증으로 남기고
해당 테스트는 보존한다. 일반 솔버 기능의 전체 검증 의무를 이 한정 병합으로 대체하지 않는다.
`reference/temp`는 검증에 사용하지 않는다. 정식 자료 누락은 미완료로 기록하고 통과로 바꾸지 않는다.
+15 -3
View File
@@ -1,7 +1,7 @@
# FESA Structural Solver
FESA는 개인 연구를 위해 개발하는 유한요소 구조해석 솔버이며 외부 사용자 배포를 장기
목표로 한다. C++17 이상·Windows MSVC·CMake/CTest를 사용하고 승인된 Abaqus 입력
목표로 한다. C++17 이상·Windows MSVC·직접 MSBuild와 CMake/CTest를 사용하고 승인된 Abaqus 입력
subset에서 재현 가능한 해석과 HDF5 결과를 제공한다. 코드 존재와 기능 검증 완료는 다르다.
## 프로젝트 문서와 영역
@@ -39,8 +39,12 @@ subset에서 재현 가능한 해석과 HDF5 결과를 제공한다. 코드 존
│ └── hdf5/AGENTS.md → schema·readback·finalize
├── tests/
│ └── AGENTS.md → 계층별 검사·실패·비교 evidence
└── cmake/
└── AGENTS.md → dependency·runtime 연결
├── cmake/
│ └── AGENTS.md → dependency·runtime 연결
├── msvc/
│ └── AGENTS.md → 직접 MSBuild·source 등록·runtime 연결
└── scripts/
└── AGENTS.md → 읽기 전용 결과 비교 도구
```
## 핵심 규칙
@@ -87,3 +91,11 @@ Public header 변경은 대응하는 src 영역의 책임과 consumer까지 확
문서만 변경하면 정적 검토를 하고 build/test를 실행하지 않는다. 동작 변경은 관련 검사부터,
솔버 기능 완료 시에는 구성된 전체 MSVC x64 Debug build/CTest와 필수 reference 비교를 수행한다.
직접 MSBuild가 주 개발 빌드이며 CMake/Ninja는 같은 제품·검사 source를 사용하는 보조 경로다.
실제 명령·환경·`.build/native/Debug` 및 `.build/cmake-debug` 출력은 solver-core를 따른다.
두 경로의 전체 검사 등록과 실제 실행 범위를 구분하고 별도 Python·reference 결과를 기록한다.
이번 병합은 2026-10-18 사용자 변경에 따라 정적해석·공통 계약 검사와 정식 정적5모델만
실행해 다음 단계로 진행한다. HHT·동해석·고유치·과도 열해석 실행은 미검증으로 남기고
해당 테스트는 보존한다. 일반 솔버 기능의 전체 검증 의무를 이 한정 병합으로 대체하지 않는다.
`reference/temp`는 검증에 사용하지 않는다. 정식 자료 누락은 미완료로 기록하고 통과로 바꾸지 않는다.
+5 -5
View File
@@ -1,13 +1,14 @@
cmake_minimum_required(VERSION 3.25)
project(FESA VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
# Pin the original backend ABI/link/thread choices rather than package defaults.
set(MKL_INTERFACE ilp64)
set(MKL_LINK dynamic)
set(MKL_THREADING intel_thread)
include(cmake/FesaDependencies.cmake)
find_package(Doxygen QUIET)
if(Doxygen_FOUND)
add_custom_target(fesa_docs
@@ -19,6 +20,5 @@ if(Doxygen_FOUND)
endif()
enable_testing()
add_subdirectory(src/fesa)
add_subdirectory(tests)
+21 -7
View File
@@ -3,11 +3,11 @@
"configurePresets": [
{
"name": "windows-msvc-ninja-debug",
"displayName": "Windows MSVC x64 Debug (Ninja)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/windows-msvc-ninja-debug",
"binaryDir": "${sourceDir}/.build/cmake-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"FESA_GTEST_SOURCE_DIR": "$env{FESA_GTEST_SOURCE_DIR}",
"MKL_DIR": "$env{MKL_DIR}",
"TBB_DIR": "$env{TBB_DIR}",
@@ -16,11 +16,15 @@
},
{
"name": "windows-msvc-ninja-release",
"displayName": "Windows MSVC x64 Release with symbols (Ninja)",
"inherits": "windows-msvc-ninja-debug",
"binaryDir": "${sourceDir}/build/windows-msvc-ninja-release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/.build/cmake-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"FESA_GTEST_SOURCE_DIR": "$env{FESA_GTEST_SOURCE_DIR}",
"MKL_DIR": "$env{MKL_DIR}",
"TBB_DIR": "$env{TBB_DIR}",
"HDF5_DIR": "$env{HDF5_DIR}"
}
}
],
@@ -37,7 +41,17 @@
"testPresets": [
{
"name": "windows-msvc-ninja-debug",
"configurePreset": "windows-msvc-ninja-debug"
"configurePreset": "windows-msvc-ninja-debug",
"output": {
"outputOnFailure": true
}
},
{
"name": "windows-msvc-ninja-release",
"configurePreset": "windows-msvc-ninja-release",
"output": {
"outputOnFailure": true
}
}
]
}
+48
View File
@@ -0,0 +1,48 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.0.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_solver", "msvc\fesa_solver.vcxproj", "{3AEC39D9-4641-5161-9107-265EB50CA882}"
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_cli", "msvc\fesa_cli.vcxproj", "{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}"
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_unit_tests", "msvc\fesa_unit_tests.vcxproj", "{42558F77-73F2-5CA7-8E3C-BB0E1646CD45}"
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_integration_tests", "msvc\fesa_integration_tests.vcxproj", "{7770C26B-DBFE-505D-8788-8B7EFD9A5A70}"
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_reference_tests", "msvc\fesa_reference_tests.vcxproj", "{330D2DFB-E57D-517D-A795-F1EA620B69F0}"
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "gtest", "msvc\gtest.vcxproj", "{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3AEC39D9-4641-5161-9107-265EB50CA882}.Debug|x64.ActiveCfg = Debug|x64
{3AEC39D9-4641-5161-9107-265EB50CA882}.Debug|x64.Build.0 = Debug|x64
{3AEC39D9-4641-5161-9107-265EB50CA882}.Release|x64.ActiveCfg = Release|x64
{3AEC39D9-4641-5161-9107-265EB50CA882}.Release|x64.Build.0 = Release|x64
{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Debug|x64.ActiveCfg = Debug|x64
{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Debug|x64.Build.0 = Debug|x64
{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Release|x64.ActiveCfg = Release|x64
{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Release|x64.Build.0 = Release|x64
{42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Debug|x64.ActiveCfg = Debug|x64
{42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Debug|x64.Build.0 = Debug|x64
{42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Release|x64.ActiveCfg = Release|x64
{42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Release|x64.Build.0 = Release|x64
{7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Debug|x64.ActiveCfg = Debug|x64
{7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Debug|x64.Build.0 = Debug|x64
{7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Release|x64.ActiveCfg = Release|x64
{7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Release|x64.Build.0 = Release|x64
{330D2DFB-E57D-517D-A795-F1EA620B69F0}.Debug|x64.ActiveCfg = Debug|x64
{330D2DFB-E57D-517D-A795-F1EA620B69F0}.Debug|x64.Build.0 = Debug|x64
{330D2DFB-E57D-517D-A795-F1EA620B69F0}.Release|x64.ActiveCfg = Release|x64
{330D2DFB-E57D-517D-A795-F1EA620B69F0}.Release|x64.Build.0 = Release|x64
{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Debug|x64.ActiveCfg = Debug|x64
{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
EndGlobalSection
EndGlobal
+4
View File
@@ -11,3 +11,7 @@
우연한 dependency로 성공한 것을 독립 배포 검증으로 간주하지 않는다.
- Target graph·preset을 바꾸면 configure·MSVC compiler·실제 CTest discovery와 runtime
loading을 검증한다. Solution-only 성공으로 CMake 제품 검증을 대체하지 않는다.
- 직접 MSVC 프로젝트와 source·검사 identity·CRT·ISA·link·DLL을 대조한다.
Preset tree는 `.build/cmake-debug`·`.build/cmake-release`이며 Native 출력과 섞지 않는다.
선택적 fesa_docs target은 직접 MSBuild의 선행 단계가 아니다.
+1 -1
View File
@@ -12,4 +12,4 @@
- [Hex8 구조해석](hex8-structural-analysis/README.md)
- [Shell 열전달](shell-transient-heat-transfer/README.md)
- [계산 성능](calculation-performance/README.md)
- [C++ 모듈화](cpp-object-oriented-modular-refactoring/README.md)
- [C++ 모듈화·Refactor 병합 현황과 이관 근거](cpp-object-oriented-modular-refactoring/README.md)
+10
View File
@@ -35,3 +35,13 @@ Hex8 기하 packet 수정본의 관련 Debug/Release82개씩, 독립 snapshot9,8
그 수정본의 단일 동적 개발 실행은 native0·117.7986225초로 완료됐으나100초 목표 초과다.101 U frame 비교는 blocking PASS, 최대 relative RMS 약2.155586e-6이었다. Outer wrapper는 종료 자식의 null StartTime 처리에서1로 끝났고 실제 FESA native0·후속 comparator0과 구분한다. 이전135.9302297초와의 차이는 서로 다른 source의 단일 개발 표본이며 반복 통계나 특정 함수 비용이 아니다.
Shell 명시 영 seed 이후 SW60 수집은 partial이며 collector native4·target 종료 미관측, increment1까지 진행했다. 함수 sample의 Jet 산술·할당·작은 MKL 호출·OpenMP spin은 비용 후보지만 sample 부재를 cache hit 보장으로 바꾸거나 nested inclusive CPU를 합산해 wall time을 만들지 않는다. 전체 shell100초·최종 전체 Debug·원본5개 재실행·최종 물리 평가는 미완료다. 보류 후보의 PASS를 복원 source나 최신 통합본에 승계하지 않는다.
## Refactor 채택 시 보존할 성능 한계
Refactor의 2026-10-16 `0b8de3a` 당시 기록은 ShellNL whole elapsed가 원본 대비
Native+86.52%·CMake+120.91% 회귀했다고 보고한다. 이는 해당 source·환경의 과거 단일 기록이며
변산·메모리·배경 부하와 원인은 미확정이다. 이후 객체 복사 제거 revision의 정적 검증을
새 Release 성능 향상으로 승계하지 않는다. 원본 `.build` archive는 통합 작업 공간으로
자동 전달되지 않았으며 이번 병합에서 해당 시간의 원시 근거를 새로 재현한 것은 아니다.
이번 작업은 제품 bytes를 채택하고 검증하는 범위다. 지속 SpMV cache나 새 산술 최적화를
도입하지 않으며 성능 회귀 원인 해결·새 benchmark는 별도 작업으로 남긴다.
@@ -24,3 +24,201 @@ CLI는 입력 `.inp` 하나, lexical sibling HDF5·log, warning/오류 진단과
- 과거 실행 절차를 현재 실행 계획으로 복원하지 않는다. CTest와 별도 Python suite의 성공을 합치지 않는다.
이 문서 작업에서는 build/test를 실행하지 않았다. 당시 모듈화와 reference의 bounded 검증은 남기되 최신 전체 회귀·외부 배포 준비 완료를 선언하지 않는다. 현재 물리적 미해결 사항은 각 기능 README가 소유한다.
## Refactor 코드 채택 후 구조
2026-10-18 통합은 FESADev `7438c827` 이력에 Refactor `11f09eeb`의 제품 `.h/.cpp`
274개와 직접 MSVC 빌드를 선별 도입한다. 옛 구현을 섞는 호환 계층이나 새 정식화는 추가하지 않는다.
[파일 manifest](merge-manifest.json)는 원본 Git blob과 working bytes, 보존·도입 방침을 구분하고,
[테스트 이관표](test-migration.tsv)는 양쪽 기존 identity·assertion·oracle와 최종 위치를 연결한다.
- [AnalysisProcedure](../../include/fesa/analysis/analysis_procedure.h)와
[factory](../../src/fesa/analysis/prepared_analysis.cpp)는 shell linear/nonlinear static,
eigen, HHT, thermal과 solid static/frequency/HHT를 선택한다. 실행 수명은 호출자 adapter보다 짧다.
[PreparedAnalysisOutcome](../../include/fesa/analysis/prepared_analysis_outcome.h)은 family별
typed completed payload를 유지하고, [결과 dispatch](../../src/fesa/results/prepared_results_writer.cpp)는
검증된 payload를 해당 writer로 보낸다. 기존 schema·atomic finalize 의미는 유지한다.
- [Element capabilities](../../include/fesa/elements/element_capabilities.h)는 ShellTrialElement,
SolidTrialElement, ShellInertiaElement, ThermalElement, ShellReferenceElement를 분리한다.
Adapter는 immutable kernel을 빌리므로 kernel의 수명과 주소가 안정적이어야 한다.
Equation·accepted history는 요소에 두지 않는다. Local layout과 full response를 typed로 보존한다.
- [EquilibriumAlgorithm/EquilibriumStep](../../src/fesa/analysis/equilibrium_algorithm.h)는
iteration 순서와 procedure-local typed pending candidate를 분리한다. Algorithm은 candidate를
소유하거나 type-erase하지 않는다. Static은1, mechanical HHT는0에서 시작하는 inclusive 순서를 유지하고,
procedure의 검증·commit 경계가 accepted state를 바꾼다.
- [ShellHhtIntegrator](../../src/fesa/analysis/hht_reduced_integrator.h)는 predictor·rate 계산·
physical-rate 복원을 분리한다. Chart/root/solver와 callback은 동기 borrowed view이며
rate 출력 buffer는 호출자 소유로 입력·서로와 alias하지 않는다. `RunReducedHhtStep`은
predictor→pending endpoint→commit 순서만 조정하고 accepted history를 소유하지 않는다.
- [PARDISO Session](../../src/fesa/solvers/linear/pardiso_session.h)은 private factor/solve/release
수명을 공통화한다. 기존1×1 TLS 회피의 범위를 vendor 전체 해결로 확대하지 않는다.
[SparsePattern](../../include/fesa/math/sparse_pattern.h)은 CSR 값 구조이며 지속 backend cache가 아니다.
[SpMV](../../src/fesa/math/sparse_matrix.cpp)는 여전히 호출마다 변환 배열·값 복사·handle을 소유한다.
수식·적분·reduction 순서·cache 수명에 영향이 있는 도입 차이는 manifest에 따로 분류했다.
출처 일치·compile·테스트 통과만으로 수치 동등성을 선언하지 않으며 독립 수치 검토와
영향받은 analytical/invariant·물리·reference 검증을 별도로 수행한다.
## 이번 병합의 현재 검증 상태
T2/T3 단계에서 직접 MSVC와 CMake/Ninja의 Debug solver/CLI 및 세 테스트 target build가 완료됐다.
제품274개는 Refactor working bytes와 일치한다. 양쪽 실제 discovery는 unit849,
integration72, reference43으로 총964개이며 canonical identity가 일치한다.
당시 선별 실행은 CMake176개, Native15개, 합성 reference comparator28개,
Python145개가 failure/error/skip 없이 통과했다. 이는 전체964개 실행·실제 모델 비교 완료가 아니다.
근거는 작업 공간의 `.build/merge-verification/t2-20261018-01/`와
`t3-20261018-01/`에 있다. 원래 dev와 Refactor 저장소 및 기준자료는 보존했다.
2026-10-18 사용자 지시로 `reference/temp`는 검증에 사용하지 않는다. 정식 경로에 없는
`shellNL-meshfine`, `shellNL-meshfine_02`, `shellNL-meshfine-pressure` 자료는 복사·복원하지 않는다.
해당 실제 비교는 자료 미확보로 미완료이며 테스트를 skip 처리하거나 통과로 바꾸지 않는다.
전체 Debug 검증·독립 수치 검토·정식 자료 비교의 현재 진행은 아래 T5 중간 기록과 구분한다.
정리 전 필수 검증을 충족하지 못하면 테스트 삭제와 최종 병합 완료 판정을 진행하지 않는다.
T4 문서 갱신 자체로 build/test를 추가 실행하지 않았다.
### T5 최초 중간 snapshot — 2026-10-18 03:00 UTC, 이후 범위 변경 전 이력
2026-10-18에 전체 Native Debug 솔루션, CMake Debug configure/build와 Ninja dependency
검사를 새로 실행해 모두 exit0을 확인했다. 제품102 compile unit의 dependency가 유효하고,
양쪽 세 테스트 executable의 실제 identity는 unit849·integration72·reference43,
CTest964개로 일치한다. Public header111개는 각각 독립 TU로 private/vendor include 경로 없이
MSVC C++17 `/EHsc /W4 /WX /MDd /Zs` 검사를 통과했다. CLI·test의 네 runtime 디렉터리에서
DLL7개의 SHA256이 같다. 제품274개와 정식 reference32개 bytes도 중간 기록 전에 재확인했다.
`reference/temp`의12개 경로는 byte 검증 대상에서 제외했고 입력·CSV·결과를 사용하지 않았다.
Native unit 전체는 필터 없이 계속 실행 중이며 **전체 종료 코드와 XML은 아직 없다**.
중간 stdout에서 완료57개 identity를 관측했으며 다음 개별 검사는 새로 PASS 종료했다.
- `ReducedManufacturedControllerKeepsExactInitialOrbit`: 396862ms.
- `C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence`: 7-grid, 7153515ms.
- `C08bInitialPhysicalSpectrumProbe`: 716ms.
- `PredictorStartCompletesPreviouslyFailingLumpedFreePlateGrid`: 539678ms.
현재 `C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants`의8-grid를 수행한다.
h=1/64·1/128·1/256·1/512·1/1024의 계산 요약이 생성됐으나 남은8192·16384·32768증분
fine-grid와 최종 시간수렴·물리 gate는 미완료다. 모든 grid의 총 증분수는57840이다.
CPU/RSS 상승이나 부분 로그를 전체 통과 근거로 삼지 않는다. Native 나머지 executable,
전체 CTest와 Python suite는 같은 직렬 큐에서 대기하며 임의 timeout·skip·필터를 추가하지 않았다.
Refactor 원본 밖 별도 source snapshot304개에서 Native Debug 기준 solver/CLI를 새로
빌드해 exit0과 DLL7개 일치를 확인했다. 정적5모델을 포함한 정식 입력11개에 대해 기준·Native·
CMake 실행물을 같은 scratch 입력 경로와 bytes에서 직렬 실행하고 schema·identity·각 field를
대조하는 후속 큐를 준비했다. **새 실제 모델 실행·비교는 아직 시작하지 않았다.** 보고서 JSON의
case·policy·quantity와 실제 exit를 확인하며, parity의 빈 결과·자료 부재·dtype 차이·불일치는
nonzero로 남긴다. Exact field assertion의 불일치를 새 물리 허용치 위반으로 승격하지 않고
출처·dtype·identity·rounding 차이를 분류한 뒤 기존 승인 기준과 대조한다. 임시 도구의 합성
증거 하한 검사9건은 솔버나 물리 검증 통과가 아니다. 빈 결과·자료 누락의 옛 false-green을
각각 exit0으로 재현하고 수정 후 exit1·exit2가 되는 것을 확인했다.
독립 수치 검토는 명백한 식 오류나 승인 기준 변경을 발견하지 못했으나 `CONCERNS`다.
inverse→factor solve·Jet·eigen whitening·paired 산술과 spatial RM 변환은 rounding graph에
영향이 있어 비트 보존을 가정하지 않는다. 특히 `hdf5_primitives.cpp`, dynamic/nonlinear HDF5
writer의 `J^T m=g` LU action은 T2 영향 routing 누락을 T5 manifest에 보충하고 finite-rotation
virtual-work·완전 inverse action·HDF5 reaction readback 검사 identity에 연결했다.
이 정적 검토를 실제 전체 실행·reference closure의 승인으로 해석하지 않는다.
정식 `shellNL-meshfine`·`shellNL-meshfine-pressure`는 자료 미확보로 미실행이고,
`shellNL-meshfine_02`는 자료 부재와 현재 comparator 연결 부재를 함께 남긴다.
Thermal 새 필수 reference 부재와 기존 NT11 diagnostic/nonblocking 불일치도 유지한다.
필수 gate를 충족하지 않았으므로 테스트 삭제·T6 진입·최종 병합 완료 판정을 진행하지 않는다.
중간 근거는 `.build/merge-verification/t5-20261018-01/before-prune/`에 있으며
`running-checkpoint.json`과 `progress.jsonl`이 실행 큐·PID·현재 관측을 보존한다.
파일 manifest의 `t5_before_prune_verification`은 이 중간 snapshot을 기록하며 T1/T2/T3 원필드는 보존했다.
### T5 사용자 승인 정적 범위 — 검증 완료, 제한 유지
사용자는 장시간 HHT 동해석 검증을 중단하고 정적 해석 검증 후 다음 단계로 진행하도록
범위를 변경했다. 위 최초 snapshot의 전체 큐·미완료 gate 설명은 당시 이력이다. 현재는
HHT·solid dynamic·eigen/frequency·transient thermal 실제 수치 실행을 유예하고 해당 테스트를
보존한다. 기존 전체 큐와 후속 watcher는 `user_cancelled_static_only`로 종료했으며,
부분 stdout·미생성 XML·취소 근거를 보존했다. 이를 수치 실패나 통과로 계산하지 않는다.
현재964개 identity를 source body·경로·행·SHA256에 대응시켜 정적·공통631개(unit555,
integration35, reference41)를 선택하고333개를 유예했다. 과거 정적553개는 분류 근거로만
읽었으며 그중11개를 유예하고 현재 추가 공통89개를 포함했다. 순수 SO3 미분·좌표변환,
mock 수명·dispatch, parser/mapping 및 합성 comparator 정책 검사는 포함하고 실제 비정적
procedure를 호출하는 검사는 제외했다. 선택표는 `before-prune/static-only/selection.json`에 있다.
새 직렬 실행은 Native·CMake의 선택631개, 명시적 목록 CTest631개 및 Python 합성·정책
검사다. 고유 C++ 검사 수는631개이며 실행 경로별 수를 합쳐 고유 검사 수로 보고하지 않는다.
Native·CMake 각631개 및 CTest631개가 실제 exit0·skip0으로 확인됐다. Native/CMake의
unit555·reference41은 처음 실행에서 통과했고 integration35는 아래 환경 대조 후 전부 통과했다.
CTest JUnit의 실제 이름을 canonical identity로 역매핑해 선택631개와 누락·중복0을 확인했다.
Python 합성·정책145개도 exit0·failure/error/skip0이며 실제 비정적 물리 검증으로 세지 않는다.
정식 정적5모델도 fresh Refactor 기준·통합 Native·통합 CMake에서 같은 scratch 입력 bytes/경로로
총15번 실행했고 등록 비교15개·보고서 검증15개·schema/identity/field parity5개가 모두 exit0이다.
추가 정식 shellNL 자료의 부재와 `_02` comparator 연결 부재는 미완료로 유지하며, 확보된
정적 범위의 gate가 충족되면 다음 단계로 진행할 수 있다는 사용자 지시를 따른다.
임시 parity 도구의0길이 dataset만 있는 파일 false-green도 실제 exit0으로 재현했다.
필수 inventory와 파일 전체의 비어있지 않은 data 검사를 추가한 도구는 같은 경우 exit1,
의미있는 합성 동일값 exit0·불일치 exit1을 보였다. 개별0길이 dataset은 허용한다.
이는 임시 도구 검증이며 제품 물리·reference 통과를 대신하지 않는다. 현재 근거 경로는
`.build/merge-verification/t5-20261018-01/before-prune/static-only/run-20261018-01/`이다.
정적 모델의 beam은 U/UR·RF/RM·section actions6가족212행, shell은 U blocking·UR warning-only
2가족294행을 기존 기준으로 비교했다. ShellNL은 translation blocking·rotation warning-only와
내부 force/moment balance·accepted free residual을 통과했다. RF/RM3가족의 수치 비교는
**false/review-only**로 보존하며 stress mapping은 여전히 미판정이다. Hex8 선형·비선형 정적은
각 Step-1/increment10의 U(length)·RF(force)·S(force/length²)를 원본 CSV·source identity와
기존5%/1% 기준으로 비교했다. 개별 quantity/frame/policy·원본/실행물/결과 hash 및 literal
argv/cwd/env는 manifest와 raw `final-static-summary.json`·각 closed execution record에 있다.
초기 Native/CMake integration에서는 같은 application 검사1개가 각각 output-busy로 실패했다.
Native 실패 lock 경로261자, CMake260자였으며 동일 Native exe·동일 identity에서 TMP/TEMP만
바꾼 대조가 긴 경로 exit1→짧은 task-owned 경로 exit0을 보였다. 짧은 고유 TMP에서 양쪽
integration35개 전체를 다시 실행해 identity35·exit0·failure/error/skip0을 확인했다. 원래
실패 XML/log와 최초 큐 aggregate exit1은 환경/경로 한계로 보존했다. 제품·테스트·허용치는
바꾸지 않았다. 모델 input/result/lock 길이도 사전 확인했으며 최대193자다.
독립 정적 수치 보충 검토는 `CONCERNS / required_static_missing=false`다. 선택631개의
정적 수치 필수 coverage 누락은 발견하지 못했으며, 이를 RF/RM·stress mapping 또는 유예
기능의 물리 승인으로 확대하지 않는다. 사용자 수정 범위의 정리 전 정적 gate는 충족했으므로
다음 단계로 진행할 수 있다. 비정적333개·누락 정식 fine/_02/pressure 비교·열해석 새 reference
부재와 기존 NT11 diagnostic 제한은 미완료로 유지한다. 테스트 정리·최종 병합은 아직 수행하지 않았다.
### T6 정적 범위 검사 정리 — 삭제 0개
[최종 정리 검토](test-cleanup-review.md)에 따라 검사·helper를 모두 보존했다.
실제 본문에서 public/private owner, 실패 뒤 출력·factor 보존, TLS·FP flags·비트 및
독립 frozen oracle의 경계가 달라 동등 대체를 입증하지 못했다. 폐기 구조만 검사한다고
확정할 근거도 없었다. 비정적333개와 자료 없는 fine/_02/pressure 관련 검사는 보존한다.
이관표2,876행의 원래12열은 그대로 두고 최종 판정·근거 연결3열만 추가했다.
검사 source·helper·양쪽 등록 변경이0이므로 문서 전용 정적 검토만 수행했다.
T6 raw `.build/merge-verification/t6-20261018/`는 원셀 보존, 현재964개 body와 선택631개,
Native/CMake 등록·제품274개·T5 실행물/hash 보존을 확인한다. T5 당시 실행 결과를
새 실행으로 보고하지 않으며 전체964개 PASS도 주장하지 않는다. 최종 정적 검증과
병합 완료 판정은 T7에 남긴다.
### T7 최종 정적 검증과 근거 보존 — 통합 승인 전
이 절이 위의 최초 전체 실행 snapshot·T5/T6 진행 상태보다 최신이다. T6/T7에서 코드·검사·
helper·비교 도구·빌드 등록을 바꾸지 않았으므로 문서 전용 변경에 대한 지침에 따라 build/test와
모델을 반복 실행하지 않았다. 대신 T5 검증 commit `4841f122`와 최종 코드 상태 `4846206e`의
해당 입력 집합이 같음을 확인하고 실제 실행물·DLL·입력·결과의 hash를 대조했다. 이 확인을
통과한 **T5 실제 실행 근거를 재사용**하며 T7의 새 실행으로 세지 않는다.
새 정적 검토는 제품274개 Refactor bytes,964개 검사 body·631개 선택/333개 보존, 이관표
원셀·정리 판정, 양쪽 등록과 실행물, 성공 XML8개와 정적5모델×3실행물의 closed record15개·
전체50단계 exit0을 확인했다. Formulation23개와 상대 링크397개, 루트 AGENTS/CLAUDE 동일성,
원래 두 저장소 pin과 정식 reference32개를 확인했다. 최초 보호 목록55개 중 temp12개를
읽기·hash 대상에서 제외하고 나머지43개 원본·로그·설정·과거 계획을 대조했다.
삭제한 검사·helper는0개이며 전체964개 또는 유예333개를 통과로 보고하지 않는다.
원시 근거는 작업 worktree 정리 뒤에도 유지하도록 원래 FESADev root의
`.build/merge-verification/refactor-integration-final-20261018-01/`에 복사했다.
`archive-inventory.json`은 원래 절대 경로→보존 위치·SHA256·크기를 연결한다. Worker raw,
base의 문서/독립 수치 검토 raw, 실제 실행물9개·runtime DLL35개(5디렉터리×7개), 새로 빌드한
Refactor 기준 source304개 등1,379개 파일의 복사 hash를 확인했다. 기존 실패·중단·옛 임시 도구
기록도 포함하며 원본을 이동·삭제하거나 과거 argv/cwd를 다시 쓰지 않았다. 현재 근거는
`worker-raw/t7-20261018/final-static-state.json`과 `archive-integrity.json`에서 찾을 수 있다.
HHT 등 비정적 검증과 미확보 fine/_02/pressure 비교, ShellNL RF/RM false/review-only·응력
mapping 미판정, 열해석 reference/NT11 제한은 그대로 남는다. 승인된 정적 범위의 최종 상태를
확인했으며 독립 최종 검토와 사용자 통합 승인 후 원래 dev에 반영한다. 아직 원래 dev 병합·
원격 push를 수행하지 않았다.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
# T6 검사 정리 판정 — 승인된 정적 범위
삭제는 **0개**다. 검사 본문·helper·CMake/MSVC 등록을 변경하지 않았다.
같은 행동·실패 경계·수치 조건의 동등 대체 또는 폐기된 내부 구조만 검사한다는 근거가
충분한 후보를 찾지 못했다. 모든 검사가 서로 독립적이라고 증명했다는 뜻은 아니다.
판정이 불충분한 검사를 보존한다는 R6 계약을 적용했다.
[이관표](test-migration.tsv)의 기존 2,876행·12열·identity·assertion 셀은 보존하고
`cleanup_decision`, `cleanup_reason_id`, `cleanup_evidence` 3열만 덧붙였다.
Dev/Refactor의 같은 최종 검사로 연결되는 두 행은 출처 이력이며 실행 중복이 아니다.
삭제가 없으므로 대체 identity도 없다. 아래 identity는 **대체 지정이 아닌 비교 검토 대상**이다.
`disposition`, `final_sha256` 등 옛 셀과 T3 manifest는 당시 이력으로 그대로 둔다.
## R-DEFER
HHT·dynamic·eigen/frequency·transient thermal 실제 수치 검증 333개를 유예 상태로
보존한다. 선택 밖이라는 이유로 삭제하지 않는다. 같은 파일의 공통/helper 구간도 보존한다.
전체 body hash의 유일한 반복 묶음은
`FixedStations/ThermalSlab.ActualMeshesMatchCertifiedP2AndSeparateFourierError/0`부터
`/11`까지다. 같은 `TEST_P` 본문을 서로 다른 parameter로 실행하는 12개 identity이고
비정적 범위이므로 중복 삭제 후보로 채택하지 않았다.
## R-POLICY
`tests/test_cpp_policy_contract.py`의 두 검사를 유지한다.
`test_p_style_001_cpp_style_and_doxygen_tooling_contract`는 clang 형식·명명 규약,
Doxyfile의 입력/제외/경고와 optional `fesa_docs` 계약을 검사한다.
`test_p_style_002_msvc_targets_enable_standard_exception_unwinding`는 CMake의
solver/CLI `/EHsc`, 직접 MSVC의 shared props `Sync`, 다섯 프로젝트의 import와
override를 검사한다. Refactor에 파일이 없다는 사실은 폐기 근거가 아니다.
## R-ORACLE
`tests/unit/math/high_low_arithmetic_original.h`를 유지한다. include consumer는
`high_low_arithmetic_test.cpp`와 `hex8_high_low_test.cpp` 두 개다.
`HighLowArithmetic.OptimizedArithmeticMatchesFrozenOriginalBits`는 원본 연산으로 raw
pair를 정규화한 뒤 high/low 비트를 비교한다. 제품 predicate를 oracle로 쓰지 않는다.
`BitExponentMatchesFrozenOracleAcrossEveryExponent`는 normal exponent·trailing bit와
subnormal 경계를 검사한다. Hex8의 `FrozenMaterialPacket`·`FrozenGeometryPacket`도
이 helper의 원본 연산 순서로 독립 expected를 만든다. 제품 helper로 치환하면 독립성이 사라진다.
## R-NUMERIC
다음은 이름/행동이 비슷해도 전체 계약의 동등 대체가 입증되지 않은 실제 본문 비교다.
| 비교 대상 | 유지 근거 |
| --- | --- |
| `DenseMath.RowMajorMatrixMatchesKnownGemvGemm`, `MatrixProductKeepsShapeAndFiniteValidationOrder`와 private `DenseBlasInternal.RawProductsPreserveDestinationOnNonfiniteInput` | Public Matrix는 storage copy/move·차원 예외와 shape-before-finite 진단 순서를 소유한다. Private raw adapter는 caller 출력 buffer 보존을 검사한다. 공개 wrapper 성공만으로 private 실패 경계가 검증되지 않는다. |
| `DenseFactor.CholeskySolvesAndWhiteningRetainsScaleAndOrientation`와 `DenseBlasInternal.CholeskyAndInverseMatchKnownFactorsAndKeepFailedOutput` | 전자는 좌우·전치·직사각 RHS와 scaled whitening Gram을 검사한다. 후자는 identity RHS로 lower inverse의 명시적 `.5,-.25,.5` 값과 실패 뒤 기존 lower/inverse·factor 재사용을 검사한다. 일부 값의 중첩만으로 양쪽 전체 조건을 대체할 수 없다. |
| `DenseFactor.OwnsSnapshotAndFailedCandidatesPreserveReusableFactor`, `EmptyAndInfinityConditionBoundaryAreExplicit` | 입력 변경 후 snapshot, shape/NaN/pivot 실패·overflow solve 후 재사용, 빈 좌우 RHS 및 `2^-32` 양측/포함 경계가 있다. 낡은 lower/inverse 명칭이 남아 있어도 본문은 현재 factor 계약을 검사한다. |
| `MklPardisoSolver.SingleEquationScopeTransitionsBetweenRetainedSizes`와 `TangentSolver.SingleEquationScopeTransitionsBetweenRetainedSizes` | SPD `Factorize/Solve`와 비대칭 `FactorizeFresh/SolveCorrection` owner가 다르며 후자는 `{2,1,0,3}` 행렬을 쓴다. `ReusesRhsButFreshValuesAndFailureCannotReuseStaleState`는 fresh 실패 후 `solver-not-factorized`와 correction 보존도 검사한다. TLS 공통화만으로 각 owner 호출 경계가 없어지지 않는다. |
| `HighLowArithmetic.PacketDispatchRetainsComponentsAndInstructionGates`와 `Hex8HighLow.PacketVectorGuardsPreserveScalarFlagsAndBits`, `PacketDoesNotClearPriorMaskedStickyFlags`, `ConcurrentFirstLinearTrialsAcrossCopiesAndMoveKeepBits` | dispatch ISA/OS·FTZ 복원과 scalar/packet 선택, SIMD lane의 RN64/status·FP sticky flag·비트, copy/move/concurrent cache 결정론은 다른 경계다. 단순 scalar 성공/값 비교가 flags와 결정론을 대체하지 않는다. |
이 범주의 나머지 행도 본래 assertion/oracle와 identity를 유지한다. 전체 쌍에 대한
의미적 동등성을 주장하지 않으며, 추가 증명 없이 시험을 합치거나 helper를 제거하지 않았다.
## R-CAPABILITY
`tests/unit/elements/element_capabilities_test.cpp`는 adapter 경계를 소유한다.
`ShellTrialPreservesCompleteDirectionalResponseAndFailure` 본문은 kernel과 capability의
force/tangent/recovery·drilling 고차 directional 응답을 비트로 비교하고 direction 입력을
보존한다. 잘못된 trial의 structured status와 이후 정상 재사용까지 비교한다.
`BoundViewsSurviveOwnerVectorMovesAndRejectOtherFamilies`는 borrowed view 수명·다른 family
거부를 검사한다. Kernel 수식 검사만으로 adapter 전달·소유 경계가 검증되지 않는다.
Inertia/thermal 전용 identity는 R-DEFER로 분류하며 삭제하지 않았다.
## R-COMPARISON
C++ reference policy와 Python comparator는 같은 5%/1% 기준을 구현해도 별도 consumer다.
`ReferenceTolerancePolicy.AppliesRelativeAndNearZeroThresholdsAtTheirInclusiveBoundaries`는
C++ family row branch·threshold·relative applicability를 검사하고,
`test_inclusive_row_and_rms_boundaries_are_independent`는 Python 결과의 독립 row/RMS
경계를 검사한다. Python `test_report_is_deterministic_and_keeps_family_boundaries`는
보고서 JSON과 U/RF/S family·reference hash를 검사한다. Header collision·identity-before-NaN·
dtype/shape·없는 자료의 not-run도 solver 실제 실행과 다른 합성 계약이다.
정식 fine/_02/pressure 자료 부재·`_02` comparator 연결 부재는 여전히 미완료이며
유일 검사를 지우는 근거로 삼지 않았다. 비정적 이름의 합성 정책 검사를 실제 동해석 통과로 세지 않는다.
## R-STATIC
현재 정적·공통 선택 identity는 유지한다. T5의 source body/행/hash와 현재 본문을
대조하고 이관표의 최종 macro identity에 연결했다. 완전한 대체의 행동·실패 경계·수치
조건을 입증하지 못했으므로 기존 assertion과 독립 oracle를 보존한다. 실행 성공·장시간·
과거 제외 여부를 삭제 근거로 사용하지 않았다.
## R-SUPPORT
파일 record·helper/oracle/fixture/include/static assertion·등록/정책 문서 행이다.
같은 최종 파일을 가리키는 역사 행을 제거하지 않았다. 제품/검사를 삭제하지 않아 이번
변경이 만든 orphan은 없다. 기존 helper의 모든 간접 참조를 의미적으로 입증했다고
주장하지 않으며, 참조 없는 폐기 구조라고 확정할 근거가 부족한 항목도 보존한다.
## 정적 검증과 인계
T6 raw는 `.build/merge-verification/t6-20261018/`다. `audit.py`는 솔버/테스트를 실행하지
않고 TSV 원셀·전체 최종 행 분류, T5 964개 body 및 631/333 선택, 양쪽 source 등록,
제품274개 Refactor bytes, T5 실행물·기준자료·원본 manifest 보존을 검사한다.
구체 명령·exit·hash·검사 수는 `audit.json`과 manifest의 `t6_test_cleanup`에 기록한다.
검사 source·등록 변화가 0이므로 AGENTS의 문서 전용 규칙에 따라 build/test를 재실행하지 않았다.
T5 Native/CMake/CTest 각631·Python145, 정식 정적5모델의 기존 실행 근거를 그대로
인계한다. T6 정적 검토를 새 solver 실행·전체964개 PASS로 보고하지 않는다.
비정적333·fine/_02/pressure·thermal 새 reference와 NT11 제한, ShellNL RF/RM
false/review-only와 stress mapping 미판정은 유지한다. T7 최종 정적 검증은 별도 작업이다.
File diff suppressed because one or more lines are too long
+8
View File
@@ -120,3 +120,11 @@ Reduced/hybrid/incompatible/20절점, plasticity/contact, mixed shell-solid, pre
warped stability·남은 bending shear locking, 임의 큰 변형/mesh/시간간격은 일반 보증 범위 밖이다.
원본 U 일치만으로 physical a·장시간 에너지·전체 시간수렴을 검증했다고 주장하지 않는다.
최적화 후 최종 source의 전체 MSVC Debug build/CTest·필수 원본·독립 물리 확인이 남는다.
## 통합된 local 경계
[SolidTrialElement/Hex8TrialAdapter](../../include/fesa/elements/element_capabilities.h)가 같은
stable-address immutable Hex8 kernel의 stiffness·reference mass·paired response를 빌린다.
Adapter에 equation·trial history를 옮기지 않으며 원래 paired 산술·실패 경계를 유지한다.
공통 procedure·typed pending과 결과 dispatch는 [모듈화 요약](../cpp-object-oriented-modular-refactoring/README.md)을 따른다.
Frequency 비교는 `reference/hexa/hexa_freq.txt`의 modal Value를 직접 사용한다.
+18 -1
View File
@@ -126,4 +126,21 @@ C08b finer-grid·최신 최적화 source의 원본 HDF5 비교·전체 회귀·
| --- | --- |
| [2026-10-04 본식](formulation-historical-docs-cleanup-20261004.md), [관성 원본](formulation-inertia-historical-docs-cleanup-20261004.md) | 당시 consistent 정책의 수식·검토 이력이다. 현재 lumped/reduced/fixed-null 정책이 우선한다. |
| [C08b Newton 연구](formulation-c08b-newton-research-20261012.md), [DAE 연구](formulation-dae-research-20261012.md) | 원인·후보 조사이며 모든 Newton/DAE 후보의 채택을 뜻하지 않는다. |
| [운동량 연구](formulation-momentum-research-20261012.md), [graph certificate 초안](formulation-graph-certificate-draft-20261012.md) | 불변량·controller/certificate 연구의 원본이다. 현재 acceptance나 엄밀한 전역 오차 보증을 자동 대체하지 않는다. |
| [운동량 연구](formulation-momentum-research-20261012.md), [graph certificate 초안](formulation-graph-certificate-draft-20261012.md) | 불변량·controller/certificate 연구의 원본이다. 현재 acceptance나 엄밀한 전역 오차 보증을 자동 대체하지 않는다. |
## 통합된 실행 경계
공통 [AnalysisProcedure](../../include/fesa/analysis/analysis_procedure.h) 선택 뒤 reduced 절차를 실행한다.
[ShellHhtIntegrator](../../src/fesa/analysis/hht_reduced_integrator.h)는 predictor/rate 복원을,
[EquilibriumStep](../../src/fesa/analysis/equilibrium_algorithm.h)은0부터 시작하는 correction과
typed pending을 분리한다. History는 procedure가 소유하고 동일 acceptance 뒤 commit한다.
빌드·선별 검사 결과는 [통합 현황](../cpp-object-oriented-modular-refactoring/README.md)을 따르며,
과거 interrupted run·finest binary64 noise의 미판정을 새 전체 통과로 바꾸지 않는다.
## 이번 병합 검증의 사용자 변경
2026-10-18 사용자가 장시간 HHT 검증을 중단하고 정적해석만 검증해 다음 단계로
진행하도록 변경했다. Native 전체 실행의 부분 stdout·기존 결과는 보존하되 현재 HHT
전체 PASS나 수치 FAIL로 계산하지 않는다. 비정적 테스트는 삭제하지 않는다. 완료된 개별
검사와 중단된 C08b의 범위·근거는 [병합 현황](../cpp-object-oriented-modular-refactoring/README.md)에
구분한다. 이 실행 범위 변경은 정식화·허용치·시간수렴 요구 자체의 변경이 아니다.
+14 -1
View File
@@ -48,7 +48,10 @@ Fine optional stress의 malformed node label `ss`는 비차단 inventory 문제
Family의 reference-only `S=max(abs(ref))`에 대해 near-zero `abs(ref)<=0.01*S`는
error `<=0.01*S`, 나머지는 relative error `<=5%`, family RMS/S `<=1%`를 함께 검사한다.
Zero-scale은 exact zero다. Blocking·warning·review의 수치와 판정 영향을 구분한다.
내부 accepted free residual·force/moment 평형의 normalized 기준은 `1e-10`이다.
Accepted free residual은 matching external scale이 nonzero이면 infinity-norm relative `<=1e-2`,
exact-zero이면 typed absolute `<=1e-3` force 또는 force-length다. Factor-one 전역 force/moment
balance는 각각 normalized `<=1e-8`이다. 이는 기존 comparator의 승인 계약이며 두 양을
하나의 `1e-10` 기준으로 요약하지 않는다. Linear 정적·HHT·eigen의 별도 gate는 바꾸지 않는다.
## 구현과 상태 수명
@@ -70,3 +73,13 @@ Reaction은 단일 global 합만 맞춰 local reaction 정확성을 대체할
큰 자세 변화와 원점 이동에서 spatial moment balance를 함께 확인해야 한다.
외부 U 통과는 warning UR 또는 review RF/RM의 수치 일치를 뜻하지 않는다.
현재 prescribed path와 단일재료 shell 사례가 arbitrary nonlinear 모델의 수렴을 보장하지 않는다.
## 통합 시 자료와 API 상태
Procedure는 공통 [AnalysisProcedure](../../include/fesa/analysis/analysis_procedure.h)로 선택되고
local trial은 [ShellTrialElement](../../include/fesa/elements/element_capabilities.h)를 통해 계산한다.
Kernel은 immutable·stable-address borrowed object이고 accepted candidate는 procedure 소유다.
Full Newton의 typed pending·iteration 순서는 [모듈화 요약](../cpp-object-oriented-modular-refactoring/README.md)을 따른다.
정식 `reference/shellNL-meshfine/`와 `reference/shellNL-meshfine_02/` 자료가 없어 해당 비교는 미완료다.
2026-10-18 사용자 지시에 따라 `reference/temp`로 대체하지 않는다. 정식 자료가 확보되어야
hash·source identity·comparator 지원을 확인하고 실제 비교를 재개할 수 있다.
+10 -1
View File
@@ -53,7 +53,9 @@ CSV의 frame·instance·source node와 component를 HDF5 nodal row에 직접 대
Shared row/schema/finite precheck는 tolerance 이전 조건이며 warning family에도 적용한다.
Reference-only `S=max(abs(ref))`에 대해 `abs(ref)<=0.01*S`는 error `<=0.01*S`,
나머지는 relative error `<=5%`, family RMS/S `<=1%`를 요구한다. `S=0`은 exact zero다.
내부 free residual·전역 force/moment balance는 normalized `1e-10`을 사용한다.
Nonlinear accepted free residual은 [비선형 정적 기준](../nonlinear-static-newton-raphson/README.md)을
따르고, factor-one 전역 force/moment balance는 각각 normalized `<=1e-8`이다.
이는 [기존 pressure 정식화](formulation.md)의 승인 범위이며 linear 정적 기준과 구분한다.
## 구현과 수명
@@ -75,3 +77,10 @@ DSLOAD와 DLOAD의 부호 규칙을 섞거나 reference area로 follower load를
같은 trial의 force/tangent를 함께 써야 Newton 일관성을 유지한다.
외부 displacement 일치만으로 압력 tangent나 local reaction 정확성을 추론하지 않는다.
한 개의 지정 shell pressure bundle은 arbitrary closed surface·offset face·contact pressure 검증이 아니다.
## 통합 시 기준자료 상태
정식 `reference/shellNL-meshfine-pressure/` 자료가 없어 실제 pressure bundle 비교는 미완료다.
2026-10-18 사용자 지시에 따라 `reference/temp`는 검증에 사용하지 않으며 임시 묶음을 승격하지 않는다.
Kernel·synthetic comparator 검사가 실제 pressure reference 통과를 대신하지 않는다.
정식 자료 확보 후 원본 hash·source/frame 대응을 확인하고 작업 복사본으로 재검증해야 한다.
@@ -131,3 +131,11 @@ hash-checked 입력의 유일 instance와 결합하며 node source identity로 b
접합부는 검증 한계다. 단일 NT11 시점으로 다른 station·flux·반력·전체 시간 정확도를
검증했다고 주장하지 않는다.
Dev 통합 승인은 새 필수 reference 통과 승인이 아니다.
## 통합된 kernel 경계
[ThermalElement/Ds4ThermalAdapter](../../include/fesa/elements/element_capabilities.h)는 immutable
DS4 kernel을 빌리고 DOF11–15 layout·K/C와 interval·복구를 typed로 제공한다.
Kernel의 주소·수명은 adapter보다 안정적이어야 한다. Thermal state·잔차 인증·accepted history와
writer 실패 경계는 그대로 procedure/state/writer에 남는다. 전체 검증 대상에 과도 열해석도 포함하며,
새 필수 reference 미확보와 기존 NT11 diagnostic 불일치를 통과로 바꾸지 않는다.
+56 -18
View File
@@ -1,6 +1,6 @@
# 솔버 공통 구조와 계약
FESA는 C++17 이상·Windows MSVC x64 Debug·CMake/CTest를 기준으로 하는 개인 연구용 솔버다. 승인된 Abaqus 입력 subset을 독립 모델로 해석하고 HDF5를 쓴다. Source spelling의 수용은 Abaqus 내부 요소·적분·알고리즘과의 동등성을 뜻하지 않는다. 외부 배포 형태·지원 환경·dependency 재배포 조건은 아직 정해지지 않았다.
FESA는 C++17 이상·Windows MSVC x64 Debug·직접 MSBuild와 CMake/CTest를 기준으로 하는 개인 연구용 솔버다. 승인된 Abaqus 입력 subset을 독립 모델로 해석하고 HDF5를 쓴다. Source spelling의 수용은 Abaqus 내부 요소·적분·알고리즘과의 동등성을 뜻하지 않는다. 외부 배포 형태·지원 환경·dependency 재배포 조건은 아직 정해지지 않았다.
## 책임과 수명
@@ -49,23 +49,61 @@ Configure/build/test의 compiler·구성·build tree를 함께 맞추고 discove
## 빌드와 직접 검사
MSVC x64 Developer PowerShell에서 저장소 root를 작업 디렉터리로 사용한다. CMake3.25 이상,
Ninja, 로컬 GoogleTest source checkout과 MKL·TBB·HDF5 개발 package가 필요하다.
`FESA_GTEST_SOURCE_DIR`은 GoogleTest의 CMakeLists.txt가 있는 source root이고,
`MKL_DIR`·`TBB_DIR`·`HDF5_DIR`은 각각 package config 파일이 있는 디렉터리다.
환경 변수에 로컬 경로를 지정한 뒤 [CMake presets](../../CMakePresets.json)의 직접 명령을 사용한다.
직접 관리하는 [FESA.sln](../../FESA.sln)과 [msvc 지침](../../msvc/AGENTS.md)이 주 개발 빌드다.
VS x64 개발 환경에서 저장소 root를 작업 디렉터리로 사용한다. v145 toolset과 로컬
GoogleTest source checkout, MKL ILP64/dynamic/Intel threading·TBB·HDF5·OpenMP가 필요하다.
`FESA_GTEST_SOURCE_DIR`, `FESA_MKL_ROOT`, `FESA_TBB_ROOT`, `FESA_HDF5_ROOT`,
`FESA_OMP_ROOT`에 설치 root를 지정하거나 ignored `msvc/Fesa.Local.props`로 주입한다.
개인 설치 경로를 추적 파일에 고정하거나 dependency를 자동 다운로드하지 않는다.
```powershell
cmake --preset windows-msvc-ninja-debug
cmake --build --preset windows-msvc-ninja-debug
ctest --preset windows-msvc-ninja-debug --show-only=json-v1
ctest --preset windows-msvc-ninja-debug --output-on-failure
```bat
MSBuild FESA.sln /m:2 /p:Configuration=Debug /p:Platform=x64
.build\native\Debug\tests\fesa_unit_tests.exe
.build\native\Debug\tests\fesa_integration_tests.exe
.build\native\Debug\tests\fesa_reference_tests.exe
```
Debug build tree는 `build/windows-msvc-ninja-debug`, 별도 Release는
`build/windows-msvc-ninja-release`이며 configure/build preset 이름도 각각 대응한다.
Debug와 Release의 binary·test inventory·evidence를 섞지 않는다. CTest discovery와 실제
종료 코드를 함께 확인한다. Python 검사가 필요하면 해당 suite를 별도로 실행하며 CTest에
포함됐다고 추론하지 않는다. [CMake](../../CMakeLists.txt)의 `fesa_docs` target은 Doxygen을
찾았을 때만 생성되므로 선택적 API 문서 산출물이다. CLI executable은 각 build tree의
`src/fesa/fesa.exe`이며 dependency runtime DLL을 함께 준비한다.
Native CLI는 `.build/native/Debug/src/fesa/fesa.exe`이며 테스트 executable은
`.build/native/Debug/tests/`에 있다. Release는 같은 구조의 `Release` 디렉터리다.
직접 MSBuild에 CMake·Ninja·Doxygen을 선행 조건으로 추가하지 않는다.
보조 경로는 CMake3.25 이상·Ninja·동일 MSVC 환경을 사용한다. `MKL_DIR`·`TBB_DIR`·
`HDF5_DIR`은 각각 config package 디렉터리다. 한국어 MSVC의 include 추적을 위해
configure/build 모두 code page 65001을 사용한다.
```bat
chcp 65001
cmake --preset windows-msvc-ninja-debug
cmake --build --preset windows-msvc-ninja-debug --parallel 2
ctest --preset windows-msvc-ninja-debug --show-only=json-v1
ctest --preset windows-msvc-ninja-debug --output-on-failure -j 1
python -m pytest tests -q
```
[Presets](../../CMakePresets.json)의 Debug tree는 `.build/cmake-debug`, Release는
`.build/cmake-release`다. CMake CLI도 각 tree의 `src/fesa/fesa.exe`에 있다.
Debug/Release의 binary·DLL·검사 목록·실행 근거를 섞지 않는다. AVX2는 전용 Hex8
source에만 적용하며 CRT·정밀도·source·link·runtime DLL과 검사 identity를 두 경로에서 대조한다.
`fesa_docs`는 Doxygen을 찾았을 때만 생기는 선택적 target이다.
이번 통합 검증은 2026-10-18 사용자 변경에 따라 정적해석·공통 계약 검사로 한정한다.
전체 등록 identity를 대조하되 실제 선택/제외 목록과 이유를 기록하고, 새로 이관한 정적 검사도
포함한다. 위 명령은 일반 전체 실행 예시이며 이번에는 명시적 정적 선택 목록을 사용한다.
정식 정적5모델(beam·linear/nonlinear shell·linear/nonlinear Hex8)을 새 기준 실행물과
Native/CMake에서 비교한다. Python 정책·합성 비교기는 실제 비정적 solver를 실행하는지
확인해 별도로 검사한다. HHT·solid dynamic·eigen/frequency·transient thermal 실행은 제외하고
미검증으로 남긴다. 과거 정적 PASS도 현재 실행으로 승계하지 않는다. Native/CTest는
서로 다른 실행 전용 TMP/TEMP와 raw 출력 경로를 사용해 직렬 실행한다. 상속된 필터·sharding을
제거하고 실제 identity·수·exit·XML/log를 discovery와 대조한다. Skip·0건·자료 누락은 PASS가 아니다.
모델은 hash가 확인된 새 작업 복사본에서만 실행한다. `reference/temp`는 검증에 사용하지 않는다.
현재 진행 결과와 미완료 비교는 [병합 현황](../cpp-object-oriented-modular-refactoring/README.md)에 기록한다.
## 공통 procedure와 결과 경계
[PrepareAnalysisInput/CreateAnalysisProcedure](../../include/fesa/analysis/prepared_analysis.h)는
한 번 mapping한 Domain에서 한 family를 선택한다. `AnalysisProcedure::Execute`는
[typed outcome](../../include/fesa/analysis/prepared_analysis_outcome.h)을 반환하며 파일을 열지 않는다.
Domain과 주입된 solver·parallel·log adapter는 실행 동안 살아 있고 이동하지 않아야 한다.
App이 로그 close 성공을 확인한 뒤 `FinalizePreparedAnalysisResult`가
[WritePreparedAnalysisResult](../../include/fesa/results/prepared_results_writer.h)로 연결한다.
필요한 writer 부재는 output failure이며 다른 결과 family로 위장하지 않는다.
+6
View File
@@ -13,3 +13,9 @@
optional state·virtual method를 공통 base에 추가하지 않는다.
- Interface 변경은 consumer compile, 소유권·수명, invalid combination과 현재 결과의
보존을 검증한다. 선언만 추가하고 parser나 CLI 지원까지 완료됐다고 보고하지 않는다.
- AnalysisProcedure는 파일을 열지 않고 typed completed outcome을 반환한다. Factory에 빌려준
Domain·solver·parallel·log adapter는 실행 동안 이동하지 않는다. Element capability adapter가
빌린 immutable kernel도 adapter보다 오래 살고 같은 주소를 유지해야 한다.
- Prepared 결과 dispatch의 writer 부재는 output failure다. 잘못된 family로 변환하거나
로그 close 이전에 최종 HDF5를 공개하지 않는다.
@@ -0,0 +1,23 @@
#ifndef FESA_ANALYSIS_ANALYSIS_PROCEDURE_H_
#define FESA_ANALYSIS_ANALYSIS_PROCEDURE_H_
#include "fesa/analysis/prepared_analysis_outcome.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
/// @brief Executes one prepared Domain without opening result files.
/// @note The Domain and injected adapters outlive Execute. Accepted histories
/// remain procedure-specific and are returned only after validation.
class AnalysisProcedure {
public:
virtual ~AnalysisProcedure() = default;
/// @brief Returns the original typed completed payload or first failure.
virtual Result<PreparedAnalysisOutcome> Execute(const Domain& domain) = 0;
};
} // namespace fesa
#endif // FESA_ANALYSIS_ANALYSIS_PROCEDURE_H_
+2 -1
View File
@@ -6,12 +6,13 @@
#include <vector>
#include "fesa/core/status.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/vector.h"
#include "fesa/results/result_records.h"
namespace fesa {
class DofManager;
/// @brief Owns mutable quantities required by the V0 linear-static frame.
class AnalysisState {
public:
@@ -0,0 +1,19 @@
#ifndef FESA_ANALYSIS_DYNAMIC_EXECUTION_RESULT_H_
#define FESA_ANALYSIS_DYNAMIC_EXECUTION_RESULT_H_
#include <vector>
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/core/diagnostic.h"
namespace fesa {
/// @brief Stores a complete HHT history before atomic result finalization.
struct DynamicExecutionResult {
DynamicAnalysisState state;
std::vector<Diagnostic> diagnostics;
};
} // namespace fesa
#endif // FESA_ANALYSIS_DYNAMIC_EXECUTION_RESULT_H_
+1 -1
View File
@@ -8,7 +8,7 @@
#include <vector>
#include "fesa/math/matrix.h"
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
#include "fesa/solvers/eigen/eigen_solver.h"
namespace fesa {
@@ -1,10 +1,14 @@
#ifndef FESA_ANALYSIS_HHT_DYNAMIC_PROCEDURE_H_
#define FESA_ANALYSIS_HHT_DYNAMIC_PROCEDURE_H_
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/analysis/dynamic_execution_result.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
class LinearSolver;
class NewtonIterationLogWriter;
class ParallelFor;
class TangentSolver;
@@ -6,13 +6,23 @@
namespace fesa {
class Domain;
class EigenSolver;
/// @brief Executes the supported linear MITC4 frequency procedure.
class LinearFrequencyProcedureExecutor final {
public:
/// @brief Preserves default FEAST construction for existing callers.
LinearFrequencyProcedureExecutor() = default;
/// @brief Borrows a solver that must outlive this executor and its calls.
explicit LinearFrequencyProcedureExecutor(const EigenSolver& solver) : solver_(&solver) {}
/// @brief Builds, solves, and validates one frequency modal candidate.
/// @return Requested physical modes plus all-c validation evidence.
Result<EigenAnalysisState> Execute(const Domain& domain) const;
private:
const EigenSolver* solver_{nullptr};
};
} // namespace fesa
@@ -0,0 +1,19 @@
#ifndef FESA_ANALYSIS_LINEAR_STATIC_EXECUTION_RESULT_H_
#define FESA_ANALYSIS_LINEAR_STATIC_EXECUTION_RESULT_H_
#include <vector>
#include "fesa/analysis/analysis_state.h"
#include "fesa/core/diagnostic.h"
namespace fesa {
/// @brief Stores a completed linear-static calculation before result writing.
struct LinearStaticExecutionResult {
AnalysisState state;
std::vector<Diagnostic> diagnostics;
};
} // namespace fesa
#endif // FESA_ANALYSIS_LINEAR_STATIC_EXECUTION_RESULT_H_
@@ -1,10 +1,13 @@
#ifndef FESA_ANALYSIS_LINEAR_STATIC_PROCEDURE_H_
#define FESA_ANALYSIS_LINEAR_STATIC_PROCEDURE_H_
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/analysis/linear_static_execution_result.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
class LinearSolver;
class ParallelFor;
@@ -0,0 +1,20 @@
#ifndef FESA_ANALYSIS_NONLINEAR_STATIC_EXECUTION_RESULT_H_
#define FESA_ANALYSIS_NONLINEAR_STATIC_EXECUTION_RESULT_H_
#include <vector>
#include "fesa/analysis/nonlinear_analysis_state.h"
#include "fesa/core/diagnostic.h"
namespace fesa {
/// @brief Stores a completed nonlinear-static calculation before result
/// writing.
struct NonlinearStaticExecutionResult {
NonlinearAnalysisState state;
std::vector<Diagnostic> diagnostics;
};
} // namespace fesa
#endif // FESA_ANALYSIS_NONLINEAR_STATIC_EXECUTION_RESULT_H_
@@ -1,10 +1,13 @@
#ifndef FESA_ANALYSIS_NONLINEAR_STATIC_PROCEDURE_H_
#define FESA_ANALYSIS_NONLINEAR_STATIC_PROCEDURE_H_
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/analysis/nonlinear_static_execution_result.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
class NewtonIterationLogWriter;
class ParallelFor;
class TangentSolver;
+19 -32
View File
@@ -2,23 +2,21 @@
#define FESA_ANALYSIS_PREPARED_ANALYSIS_H_
#include <filesystem>
#include <variant>
#include <memory>
#include <vector>
#include "fesa/analysis/analysis_state.h"
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/analysis/eigen_analysis_state.h"
#include "fesa/analysis/nonlinear_analysis_state.h"
#include "fesa/analysis/solid_frequency_state.h"
#include "fesa/analysis/solid_hht_dynamic_procedure.h"
#include "fesa/analysis/solid_static_procedure.h"
#include "fesa/analysis/transient_heat_procedure.h"
#include "fesa/analysis/analysis_procedure.h"
#include "fesa/analysis/prepared_analysis_outcome.h"
#include "fesa/core/diagnostic.h"
#include "fesa/core/status.h"
#include "fesa/model/domain.h"
namespace fesa {
class ParallelFor;
class LinearSolver;
class TangentSolver;
class EigenSolver;
class LinearStaticProcedureExecutor;
class LinearFrequencyProcedureExecutor;
class EigenResultsWriter;
@@ -31,6 +29,9 @@ class DynamicResultsWriter;
class ThermalResultsWriter;
class SolidResultsWriter;
class SolidFrequencyProcedureExecutor;
class SolidStaticProcedureExecutor;
class SolidHhtDynamicProcedureExecutor;
class TransientHeatProcedureExecutor;
struct DerivedOutputPaths;
/// @brief Owns one parsed and semantically mapped analysis input.
@@ -46,30 +47,16 @@ struct PreparedAnalysisInput {
Result<PreparedAnalysisInput> PrepareAnalysisInput(
const std::filesystem::path& input_path);
/// @brief Stores a completed linear-static calculation before result writing.
struct LinearStaticExecutionResult {
AnalysisState state;
std::vector<Diagnostic> diagnostics;
};
/// @brief Creates exactly one selected family with caller-owned adapters.
/// @note Domain and adapters remain alive and unmoved throughout execution.
Result<std::unique_ptr<AnalysisProcedure>> CreateAnalysisProcedure(
const Domain &domain, const ParallelFor &parallel_for,
LinearSolver &linear_solver, TangentSolver &tangent_solver,
const EigenSolver &eigen_solver, NewtonIterationLogWriter &log);
/// @brief Stores a completed nonlinear-static calculation before result
/// writing.
struct NonlinearStaticExecutionResult {
NonlinearAnalysisState state;
std::vector<Diagnostic> diagnostics;
};
/// @brief Stores a complete HHT history before atomic result finalization.
struct DynamicExecutionResult {
DynamicAnalysisState state;
std::vector<Diagnostic> diagnostics;
};
using PreparedAnalysisOutcome =
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
EigenAnalysisState, DynamicExecutionResult,
ThermalExecutionResult, SolidStaticExecutionResult,
SolidFrequencyState, SolidDynamicExecutionResult>;
/// @brief Executes a caller-selected procedure through the common interface.
Result<PreparedAnalysisOutcome>
ExecutePreparedAnalysis(const Domain &domain, AnalysisProcedure &procedure);
/// @brief Selects the approved procedure from a prepared Domain.
/// @pre The executors and log outlive this call.
@@ -0,0 +1,26 @@
#ifndef FESA_ANALYSIS_PREPARED_ANALYSIS_OUTCOME_H_
#define FESA_ANALYSIS_PREPARED_ANALYSIS_OUTCOME_H_
#include <variant>
#include "fesa/analysis/dynamic_execution_result.h"
#include "fesa/analysis/eigen_analysis_state.h"
#include "fesa/analysis/linear_static_execution_result.h"
#include "fesa/analysis/nonlinear_static_execution_result.h"
#include "fesa/analysis/solid_dynamic_execution_result.h"
#include "fesa/analysis/solid_frequency_state.h"
#include "fesa/analysis/solid_static_execution_result.h"
#include "fesa/analysis/thermal_execution_result.h"
namespace fesa {
/// @brief Owns a completed procedure result before result-file finalization.
using PreparedAnalysisOutcome =
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
EigenAnalysisState, DynamicExecutionResult,
ThermalExecutionResult, SolidStaticExecutionResult,
SolidFrequencyState, SolidDynamicExecutionResult>;
} // namespace fesa
#endif // FESA_ANALYSIS_PREPARED_ANALYSIS_OUTCOME_H_
@@ -0,0 +1,17 @@
#ifndef FESA_ANALYSIS_SOLID_DYNAMIC_EXECUTION_RESULT_H_
#define FESA_ANALYSIS_SOLID_DYNAMIC_EXECUTION_RESULT_H_
#include "fesa/analysis/solid_dynamic_analysis_state.h"
#include "fesa/fem/dof_manager.h"
namespace fesa {
/// @brief Owns the validated 3N dynamic result independently of serialization.
struct SolidDynamicExecutionResult {
DofManager dofs;
SolidDynamicAnalysisState state;
};
} // namespace fesa
#endif // FESA_ANALYSIS_SOLID_DYNAMIC_EXECUTION_RESULT_H_
@@ -5,14 +5,24 @@
namespace fesa {
class Domain;
class EigenSolver;
/// @brief Executes unloaded reference-state C3D8 generalized frequency
/// analysis.
class SolidFrequencyProcedureExecutor final {
public:
/// @brief Preserves default FEAST construction for existing callers.
SolidFrequencyProcedureExecutor() = default;
/// @brief Borrows a solver that must outlive this executor and its calls.
explicit SolidFrequencyProcedureExecutor(const EigenSolver& solver) : solver_(&solver) {}
/// @brief Returns lowest requested modes after validating every returned
/// pair.
/// @note Domain is immutable and must remain alive during execution.
Result<SolidFrequencyState> Execute(const Domain& domain) const;
private:
const EigenSolver* solver_{nullptr};
};
} // namespace fesa
#endif // FESA_ANALYSIS_SOLID_FREQUENCY_PROCEDURE_H_
@@ -8,7 +8,7 @@
#include <vector>
#include "fesa/math/matrix.h"
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
#include "fesa/solvers/eigen/eigen_solver.h"
namespace fesa {
/// @brief Identifies a source-ordered physical Solid eigenmode.
@@ -1,19 +1,13 @@
#ifndef FESA_ANALYSIS_SOLID_HHT_DYNAMIC_PROCEDURE_H_
#define FESA_ANALYSIS_SOLID_HHT_DYNAMIC_PROCEDURE_H_
#include "fesa/analysis/solid_dynamic_analysis_state.h"
#include "fesa/analysis/solid_dynamic_execution_result.h"
namespace fesa {
class LinearSolver;
class ParallelFor;
class TangentSolver;
/// @brief Owns the validated 3N dynamic result independently of serialization.
struct SolidDynamicExecutionResult {
DofManager dofs;
SolidDynamicAnalysisState state;
};
/// @brief Integrates linear or TL Solid mechanics with constant consistent
/// mass.
class SolidHhtDynamicProcedureExecutor {
@@ -0,0 +1,19 @@
#ifndef FESA_ANALYSIS_SOLID_STATIC_EXECUTION_RESULT_H_
#define FESA_ANALYSIS_SOLID_STATIC_EXECUTION_RESULT_H_
#include <vector>
#include "fesa/analysis/solid_static_state.h"
#include "fesa/core/diagnostic.h"
namespace fesa {
/// @brief Owns successful static frames and sorted model diagnostics.
struct SolidStaticExecutionResult {
SolidStaticState state;
std::vector<Diagnostic> diagnostics;
};
} // namespace fesa
#endif // FESA_ANALYSIS_SOLID_STATIC_EXECUTION_RESULT_H_
@@ -1,19 +1,13 @@
#ifndef FESA_ANALYSIS_SOLID_STATIC_PROCEDURE_H_
#define FESA_ANALYSIS_SOLID_STATIC_PROCEDURE_H_
#include "fesa/analysis/solid_static_state.h"
#include "fesa/analysis/solid_static_execution_result.h"
namespace fesa {
class LinearSolver;
class ParallelFor;
class TangentSolver;
/// @brief Owns successful static frames and sorted model diagnostics.
struct SolidStaticExecutionResult {
SolidStaticState state;
std::vector<Diagnostic> diagnostics;
};
/// @brief Executes linear K0 or fixed-factor TL StVK static analysis.
class SolidStaticProcedureExecutor {
public:
+1 -1
View File
@@ -99,7 +99,7 @@ class SolidStaticState {
const noexcept;
private:
friend class SolidStaticProcedureExecutor;
friend class SolidStaticCore;
bool initialized_{false};
bool nonlinear_{false};
std::vector<SolidIntegrationPointRow> reference_points_;
@@ -6,11 +6,15 @@
#include <vector>
#include "fesa/analysis/thermal_time_grid.h"
#include "fesa/assembly/thermal_assembler.h"
#include "fesa/core/status.h"
#include "fesa/results/thermal_result_records.h"
namespace fesa {
class AnalysisModel;
class DofManager;
class ThermalAssembly;
/// @brief Owns the immutable thermal result inventory and accepted history.
/// @note No Domain/model/assembly references survive Create; state may outlive
/// them. Numerical equilibrium and energy checks belong to the procedure before
@@ -0,0 +1,19 @@
#ifndef FESA_ANALYSIS_THERMAL_EXECUTION_RESULT_H_
#define FESA_ANALYSIS_THERMAL_EXECUTION_RESULT_H_
#include <vector>
#include "fesa/analysis/thermal_analysis_state.h"
#include "fesa/core/diagnostic.h"
namespace fesa {
/// @brief Owns complete thermal history before any result output is opened.
struct ThermalExecutionResult {
ThermalAnalysisState state;
std::vector<Diagnostic> diagnostics;
};
} // namespace fesa
#endif // FESA_ANALYSIS_THERMAL_EXECUTION_RESULT_H_
@@ -1,21 +1,14 @@
#ifndef FESA_ANALYSIS_TRANSIENT_HEAT_PROCEDURE_H_
#define FESA_ANALYSIS_TRANSIENT_HEAT_PROCEDURE_H_
#include <vector>
#include "fesa/analysis/thermal_analysis_state.h"
#include "fesa/analysis/thermal_execution_result.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
class LinearSolver;
class ParallelFor;
/// @brief Owns complete thermal history before any result output is opened.
struct ThermalExecutionResult {
ThermalAnalysisState state;
std::vector<Diagnostic> diagnostics;
};
/// @brief Executes fixed-increment backward Euler with checked thermal
/// recovery.
class TransientHeatProcedureExecutor final {
+2 -1
View File
@@ -30,7 +30,8 @@ class LoadAssembler {
double step_time = 0.0);
/// @brief Forms Ff-Kfc*dc in stable free/constrained order.
/// @note This operation neither factorizes nor invokes a solver.
/// @note Compatibility facade delegates to EssentialConstraintPolicy; this
/// operation neither factorizes nor invokes a solver.
static Result<Vector> EffectiveFreeRhs(const Vector& full_load,
const SparseMatrix& kfc,
const Vector& prescribed_values,
@@ -2,10 +2,12 @@
#define FESA_ASSEMBLY_THERMAL_ASSEMBLER_H_
#include <array>
#include <memory>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/element_capabilities.h"
#include "fesa/elements/thermal_shell.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/sparse_matrix.h"
@@ -50,6 +52,8 @@ class ThermalAssembly final {
const std::vector<EntityIndex>& ElementOrder() const noexcept;
/// @brief Returns checked kernels in stable active element order.
const std::vector<ThermalShell>& Kernels() const noexcept;
/// @brief Returns typed views bound after the final kernel buffer is placed.
const std::vector<std::unique_ptr<ThermalElement>>& Elements() const noexcept;
/// @brief Returns node-major twenty-entry full DOF scatters in kernel order.
const std::vector<std::array<std::size_t, 20>>& Scatters() const noexcept;
/// @brief Checks source Domain identity and the complete supplied equation
@@ -79,7 +83,10 @@ class ThermalAssembly final {
SparseMatrix capacity_error_;
Vector input_error_;
std::vector<EntityIndex> element_order_;
// Never resized after construction. Moving the owning vector transfers its
// buffer, so bound references survive ThermalAssembly moves.
std::vector<ThermalShell> kernels_;
std::vector<std::unique_ptr<ThermalElement>> elements_;
std::vector<std::array<std::size_t, 20>> scatters_;
};
@@ -24,6 +24,16 @@ class EssentialConstraintPolicy {
Result<PartitionedStiffness> Partition(const SparseMatrix& full_stiffness,
const DofManager& dof_manager) const;
/// @brief Forms Ff-Kfc*dc in stable free/constrained order.
/// @return A finite RHS candidate or the established load diagnostic.
/// @note Preserves independent CSR products and ordered row reductions;
/// no factorization or solver operation is performed. Inputs are borrowed
/// only for this call and remain unchanged on failure.
Result<Vector> EffectiveFreeRhs(const Vector& full_load,
const SparseMatrix& kfc,
const Vector& prescribed_values,
const DofManager& dofs) const;
/// @brief Gathers a full vector in stable free-equation order.
Vector GatherFree(const Vector& full_values,
const DofManager& dof_manager) const;
@@ -5,7 +5,7 @@
#include "fesa/constraints/boundary_condition.h"
#include "fesa/core/diagnostic.h"
#include "fesa/elements/element.h"
#include "fesa/elements/element_dof_layout.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa {
+6
View File
@@ -2,6 +2,7 @@
#define FESA_CORE_ASCII_H_
#include <cstdint>
#include <string>
#include <string_view>
#include "fesa/core/status.h"
@@ -12,6 +13,11 @@ namespace fesa {
/// @return The lowercase ASCII byte, or the input byte when it is not A-Z.
char AsciiLower(char value) noexcept;
/// @brief Returns an uppercase ASCII copy without changing other source bytes.
/// @note Embedded nulls and non-ASCII bytes are preserved without locale
/// folding.
std::string AsciiUppercase(std::string value);
/// @brief Compares two byte strings with ASCII-only case folding.
/// @return True when the strings have equal length and equal ASCII-folded
/// bytes.
+8
View File
@@ -24,6 +24,14 @@ struct SourceEntityId {
std::string source_label_text;
};
/// @brief Compares all preserved source fields without canonicalizing spelling.
inline bool SourceIdentityEquals(const SourceEntityId& left,
const SourceEntityId& right) noexcept {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
} // namespace fesa
#endif // FESA_CORE_SOURCE_IDENTITY_H_
+2 -2
View File
@@ -98,11 +98,11 @@ class Result {
struct FailureTag {};
/// @brief Constructs the successful value alternative.
Result(SuccessTag, T value)
Result(SuccessTag, T&& value)
: value_{std::move(value)}, status_{Status::Ok()} {}
/// @brief Constructs the failed status alternative.
Result(FailureTag, Status status)
Result(FailureTag, Status&& status)
: value_{std::nullopt}, status_{std::move(status)} {}
std::optional<T> value_;
+1 -24
View File
@@ -1,13 +1,11 @@
#ifndef FESA_ELEMENTS_ELEMENT_H_
#define FESA_ELEMENTS_ELEMENT_H_
#include <cstdint>
#include <functional>
#include <variant>
#include <vector>
#include "fesa/core/source_identity.h"
#include "fesa/core/status.h"
#include "fesa/elements/element_dof_layout.h"
#include "fesa/math/matrix.h"
#include "fesa/math/vector.h"
#include "fesa/results/result_records.h"
@@ -15,23 +13,6 @@
namespace fesa {
/// @brief Identifies one component in the stable six-DOF node layout.
enum class DofComponent : std::uint8_t {
kUx,
kUy,
kUz,
kUrx,
kUry,
kUrz,
};
/// @brief Describes one runtime element's stable node and component order.
struct ElementDofLayout {
SourceEntityId source_id;
std::vector<EntityIndex> node_indices;
std::vector<DofComponent> components_per_node;
};
/// @brief Carries one element-local stiffness in its declared DOF order.
struct ElementStiffnessContribution {
ElementDofLayout layout;
@@ -78,10 +59,6 @@ class Element {
const Vector& element_displacement) const = 0;
};
/// @brief Provides non-owning runtime elements in stable owner order.
/// @note Every referenced element must outlive this view.
using ElementView = std::vector<std::reference_wrapper<const Element>>;
} // namespace fesa
#endif // FESA_ELEMENTS_ELEMENT_H_
@@ -0,0 +1,200 @@
#ifndef FESA_ELEMENTS_ELEMENT_CAPABILITIES_H_
#define FESA_ELEMENTS_ELEMENT_CAPABILITIES_H_
#include <array>
#include <cstddef>
#include "fesa/elements/element.h"
#include "fesa/elements/thermal_element_layout.h"
#include "fesa/math/vector3.h"
namespace fesa {
class Hex8Element;
class Mitc4Shell;
class NonlinearMitc4Shell;
class NonlinearMitc4Inertia;
class ThermalShell;
struct Hex8Response;
struct Hex8HighLowResponse;
struct Mitc4Stiffness;
struct Mitc4Mass;
struct NonlinearMitc4TrialDofs;
struct NonlinearMitc4InternalEquilibrium;
struct NonlinearMitc4ExternalDrillingConstraint;
struct NonlinearMitc4InertiaEvaluation;
struct ThermalShellRecovery;
/// @brief Evaluates immutable shell trials; accepted history belongs to the procedure.
class ShellTrialElement {
public:
virtual ~ShellTrialElement() = default;
/// @brief Returns stable source-local node/component order, never equations.
virtual const ElementDofLayout& DofLayout() const noexcept = 0;
/// @brief Returns the complete force/tangent/recovery at a fixed-global trial.
/// @param coordinate_direction Optional 24-vector for D2(f_int)[v,v].
/// @note Failure preserves the kernel, caller inputs and earlier responses.
virtual Result<NonlinearMitc4InternalEquilibrium> EvaluateTrial(
const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction = nullptr) const = 0;
/// @copydoc NonlinearMitc4Shell::EvaluateExternalDrillingConstraint
virtual Result<NonlinearMitc4ExternalDrillingConstraint>
EvaluateExternalDrillingConstraint(
std::size_t node, const Vector3& phi, const Vector3& phi_dot,
const Vector3& force, const Vector3& force_rate,
const Vector3& force_acceleration) const = 0;
};
/// @brief Borrows the same immutable shell kernel for every trial operation.
/// @note The kernel must outlive this adapter and remain at its bound address.
class Mitc4TrialAdapter final : public ShellTrialElement {
public:
Mitc4TrialAdapter(ElementDofLayout layout, const NonlinearMitc4Shell& kernel);
const ElementDofLayout& DofLayout() const noexcept override;
Result<NonlinearMitc4InternalEquilibrium> EvaluateTrial(
const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction = nullptr) const override;
Result<NonlinearMitc4ExternalDrillingConstraint>
EvaluateExternalDrillingConstraint(
std::size_t node, const Vector3& phi, const Vector3& phi_dot,
const Vector3& force, const Vector3& force_rate,
const Vector3& force_acceleration) const override;
private:
ElementDofLayout layout_;
const NonlinearMitc4Shell& kernel_;
};
/// @brief Keeps complete paired solid responses and reference mass typed.
class SolidTrialElement : public Element {
public:
~SolidTrialElement() override = default;
/// @copydoc Hex8Element::EvaluateLinear
virtual Result<Hex8Response> EvaluateLinear(const Vector& u) const = 0;
/// @copydoc Hex8Element::EvaluateNonlinear
virtual Result<Hex8Response> EvaluateNonlinear(const Vector& u) const = 0;
/// @copydoc Hex8Element::EvaluateLinearHighLow
virtual Result<Hex8HighLowResponse> EvaluateLinearHighLow(
const Vector& high, const Vector& low) const = 0;
/// @copydoc Hex8Element::EvaluateNonlinearHighLow
virtual Result<Hex8HighLowResponse> EvaluateNonlinearHighLow(
const Vector& high, const Vector& low) const = 0;
/// @copydoc Hex8Element::ComputeMass
virtual Result<ElementStiffnessContribution> ComputeMass() const = 0;
/// @brief Returns fixed Gauss weights used by the frequency mass bound.
virtual std::array<double, 8> ReferenceWeights() const noexcept = 0;
};
/// @brief Borrows one stable Hex8 kernel shared by all local operations.
/// @note The kernel must outlive this adapter and remain at its bound address.
class Hex8TrialAdapter final : public SolidTrialElement {
public:
explicit Hex8TrialAdapter(const Hex8Element& kernel);
const ElementDofLayout& DofLayout() const noexcept override;
Result<ElementStiffnessContribution> ComputeStiffness() const override;
Result<ElementResultBundle> Recover(const Vector& u) const override;
Result<Hex8Response> EvaluateLinear(const Vector& u) const override;
Result<Hex8Response> EvaluateNonlinear(const Vector& u) const override;
Result<Hex8HighLowResponse> EvaluateLinearHighLow(
const Vector& high, const Vector& low) const override;
Result<Hex8HighLowResponse> EvaluateNonlinearHighLow(
const Vector& high, const Vector& low) const override;
Result<ElementStiffnessContribution> ComputeMass() const override;
std::array<double, 8> ReferenceWeights() const noexcept override;
private:
const Hex8Element& kernel_;
};
/// @brief Returns the complete physical inertia response in fixed chart DOFs.
class ShellInertiaElement {
public:
virtual ~ShellInertiaElement() = default;
virtual const ElementDofLayout& DofLayout() const noexcept = 0;
/// @copydoc NonlinearMitc4Inertia::Evaluate
virtual Result<NonlinearMitc4InertiaEvaluation> Evaluate(
const NonlinearMitc4TrialDofs& trial, const Vector& velocity24,
const Vector& physical_acceleration24) const = 0;
};
/// @brief Borrows immutable inertia geometry, never shared mutable scratch.
/// @note The kernel must outlive this adapter and remain at its bound address.
class Mitc4InertiaAdapter final : public ShellInertiaElement {
public:
Mitc4InertiaAdapter(ElementDofLayout layout,
const NonlinearMitc4Inertia& kernel);
const ElementDofLayout& DofLayout() const noexcept override;
Result<NonlinearMitc4InertiaEvaluation> Evaluate(
const NonlinearMitc4TrialDofs& trial, const Vector& velocity24,
const Vector& physical_acceleration24) const override;
private:
ElementDofLayout layout_;
const NonlinearMitc4Inertia& kernel_;
};
/// @brief Keeps DS4 DOFs 11-15 and all four-by-six flux traces independent.
class ThermalElement {
public:
virtual ~ThermalElement() = default;
/// @brief Returns node-major stations 11-15 and original source identity.
virtual const ThermalElementDofLayout& DofLayout() const noexcept = 0;
/// @copydoc ThermalShell::Conductivity
virtual const Matrix& Conductivity() const noexcept = 0;
/// @copydoc ThermalShell::Capacity
virtual const Matrix& Capacity() const noexcept = 0;
/// @copydoc ThermalShell::ConductivityError
virtual const Matrix& ConductivityError() const noexcept = 0;
/// @copydoc ThermalShell::CapacityError
virtual const Matrix& CapacityError() const noexcept = 0;
/// @copydoc ThermalShell::Recover
virtual Result<ThermalShellRecovery> Recover(
const std::array<double, 20>& temperature) const = 0;
};
/// @brief Borrows the same DS4 kernel for matrices, intervals and recovery.
/// @note The kernel must outlive this adapter and remain at its bound address.
class Ds4ThermalAdapter final : public ThermalElement {
public:
explicit Ds4ThermalAdapter(const ThermalShell& kernel);
const ThermalElementDofLayout& DofLayout() const noexcept override;
const Matrix& Conductivity() const noexcept override;
const Matrix& Capacity() const noexcept override;
const Matrix& ConductivityError() const noexcept override;
const Matrix& CapacityError() const noexcept override;
Result<ThermalShellRecovery> Recover(
const std::array<double, 20>& temperature) const override;
private:
ThermalElementDofLayout layout_;
const ThermalShell& kernel_;
};
/// @brief Supplies shell frequency operators with their distinct mass contract.
class ShellReferenceElement : public Element {
public:
~ShellReferenceElement() override = default;
/// @copydoc Mitc4Shell::Stiffness
virtual Result<Mitc4Stiffness> Stiffness() const = 0;
/// @copydoc Mitc4Shell::Mass
virtual Result<Mitc4Mass> Mass(double density) const = 0;
};
/// @brief Borrows the same linear MITC4 kernel for stiffness and recovery.
/// @note The kernel must outlive this adapter and remain at its bound address.
class Mitc4ReferenceAdapter final : public ShellReferenceElement {
public:
explicit Mitc4ReferenceAdapter(const Mitc4Shell& kernel);
const ElementDofLayout& DofLayout() const noexcept override;
Result<ElementStiffnessContribution> ComputeStiffness() const override;
Result<ElementResultBundle> Recover(const Vector& u) const override;
Result<Mitc4Stiffness> Stiffness() const override;
Result<Mitc4Mass> Mass(double density) const override;
private:
const Mitc4Shell& kernel_;
};
} // namespace fesa
#endif // FESA_ELEMENTS_ELEMENT_CAPABILITIES_H_
@@ -0,0 +1,38 @@
#ifndef FESA_ELEMENTS_ELEMENT_DOF_LAYOUT_H_
#define FESA_ELEMENTS_ELEMENT_DOF_LAYOUT_H_
#include <cstdint>
#include <functional>
#include <vector>
#include "fesa/core/source_identity.h"
namespace fesa {
/// @brief Identifies one component in the stable six-DOF node layout.
enum class DofComponent : std::uint8_t {
kUx,
kUy,
kUz,
kUrx,
kUry,
kUrz,
};
/// @brief Describes one runtime element's stable node and component order.
struct ElementDofLayout {
SourceEntityId source_id;
std::vector<EntityIndex> node_indices;
std::vector<DofComponent> components_per_node;
};
class Element;
/// @brief Provides non-owning runtime elements in stable owner order.
/// @note Every referenced element must outlive this view.
using ElementView = std::vector<std::reference_wrapper<const Element>>;
} // namespace fesa
#endif // FESA_ELEMENTS_ELEMENT_DOF_LAYOUT_H_
+9
View File
@@ -5,6 +5,7 @@
#include "fesa/core/status.h"
#include "fesa/elements/element.h"
#include "fesa/elements/element_capabilities.h"
namespace fesa {
@@ -21,6 +22,14 @@ class ElementFactory {
Result<std::unique_ptr<Element>> Create(const ElementDefinition& definition,
const Domain& domain) const;
/// @brief Binds a typed view of an existing stable runtime kernel.
/// @note Returns null for a different family. The kernel must outlive the
/// returned adapter and remain at its address; no second kernel is created.
std::unique_ptr<SolidTrialElement> BindSolidTrial(const Element& kernel) const;
/// @copydoc BindSolidTrial
std::unique_ptr<ShellReferenceElement> BindShellReference(
const Element& kernel) const;
private:
/// @brief Creates one checked B33 runtime candidate.
Result<std::unique_ptr<Element>> CreateBeam(
+1 -34
View File
@@ -4,12 +4,11 @@
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
#include <vector>
#include "fesa/core/status.h"
#include "fesa/elements/element.h"
#include "fesa/elements/element_definition.h"
#include "fesa/elements/euler_beam_3d_definition.h"
#include "fesa/materials/isotropic_linear_elastic_material.h"
#include "fesa/math/matrix.h"
#include "fesa/math/vector.h"
@@ -17,40 +16,8 @@
namespace fesa {
class Domain;
struct Node;
/// @brief Defines one two-node B33 semantic element.
class EulerBeam3DDefinition final : public ElementDefinition {
public:
/// @brief Constructs a parser-validated semantic definition.
EulerBeam3DDefinition(SourceEntityId source_id,
std::array<EntityIndex, 2> node_indices,
EntityIndex material_index, EntityIndex section_index,
SourceLocation location);
ElementDefinitionKind Kind() const noexcept override;
const SourceEntityId& SourceId() const noexcept override;
std::string_view SourceElementType() const noexcept override;
const std::vector<EntityIndex>& NodeIndices() const noexcept override;
EntityIndex MaterialIndex() const noexcept override;
EntityIndex PropertyIndex() const noexcept override;
SourceEntityId source_id;
std::array<EntityIndex, 2> node_indices;
EntityIndex material_index;
EntityIndex section_index;
SourceLocation location;
private:
friend class Domain;
/// @brief Synchronizes the base view after parser-candidate construction.
void SynchronizeNodeIndices();
std::vector<EntityIndex> node_indices_view_;
};
/// @brief Stores constant line-load components in the beam local frame.
struct ConstantLocalLineLoad {
double px;
@@ -0,0 +1,48 @@
#ifndef FESA_ELEMENTS_EULER_BEAM_3D_DEFINITION_H_
#define FESA_ELEMENTS_EULER_BEAM_3D_DEFINITION_H_
#include <array>
#include <string_view>
#include <vector>
#include "fesa/elements/element_definition.h"
namespace fesa {
class Domain;
/// @brief Defines one two-node B33 semantic element.
class EulerBeam3DDefinition final : public ElementDefinition {
public:
/// @brief Constructs a parser-validated semantic definition.
EulerBeam3DDefinition(SourceEntityId source_id,
std::array<EntityIndex, 2> node_indices,
EntityIndex material_index, EntityIndex section_index,
SourceLocation location);
ElementDefinitionKind Kind() const noexcept override;
const SourceEntityId& SourceId() const noexcept override;
std::string_view SourceElementType() const noexcept override;
const std::vector<EntityIndex>& NodeIndices() const noexcept override;
EntityIndex MaterialIndex() const noexcept override;
EntityIndex PropertyIndex() const noexcept override;
SourceEntityId source_id;
std::array<EntityIndex, 2> node_indices;
EntityIndex material_index;
EntityIndex section_index;
SourceLocation location;
private:
friend class Domain;
/// @brief Synchronizes the base view after parser-candidate construction.
void SynchronizeNodeIndices();
std::vector<EntityIndex> node_indices_view_;
};
} // namespace fesa
#endif // FESA_ELEMENTS_EULER_BEAM_3D_DEFINITION_H_
+6 -4
View File
@@ -33,11 +33,13 @@ Matrix ExpSkew(const Vector3& phi);
/// @pre phi is finite and lies inside the approved principal chart.
Matrix LeftJacobianSO3(const Vector3& phi);
/// @brief Computes the inverse transpose of the SO(3) left Jacobian.
/// @param phi Fixed-global rotation coordinate in radians.
/// @return The 3-by-3 inverse-transpose left Jacobian.
/// @brief Solves J(phi)^T * moment = generalized for a spatial moment.
/// @param phi Fixed-global principal-chart rotation coordinate in radians.
/// @param generalized Work-conjugate moment in fixed-global chart coordinates.
/// @return A finite spatial moment in the same force-length units, or failure.
/// @pre phi is finite and lies inside the approved principal chart.
Matrix InverseTransposeLeftJacobianSO3(const Vector3& phi);
/// @note Inputs remain unchanged; no inverse Jacobian is constructed.
Result<Vector3> SpatialMomentSO3(const Vector3& phi, const Vector3& generalized);
/// @brief Builds a checked finite-rotation state from its reference frame.
/// @param phi Fixed-global principal-chart rotation coordinate in radians.
+2 -48
View File
@@ -3,12 +3,11 @@
#include <array>
#include <string>
#include <string_view>
#include <vector>
#include "fesa/core/status.h"
#include "fesa/elements/element.h"
#include "fesa/elements/element_definition.h"
#include "fesa/elements/mitc4_shell_definition.h"
#include "fesa/materials/isotropic_linear_elastic_material.h"
#include "fesa/math/matrix.h"
#include "fesa/math/vector.h"
@@ -17,52 +16,8 @@
namespace fesa {
class Domain;
struct Node;
/// @brief Preserves the source shell type independently of formulation.
enum class ShellSourceElementType { kS4, kS4r };
/// @brief Names the internal shell formulation selected by S4 and S4R.
inline constexpr std::string_view kMitc4InternalFormulation{"FESA-MITC4"};
/// @brief Defines one four-node S4/S4R semantic element.
class Mitc4ShellDefinition final : public ElementDefinition {
public:
/// @brief Constructs a parser-validated semantic definition.
Mitc4ShellDefinition(SourceEntityId source_id,
ShellSourceElementType source_type,
std::array<EntityIndex, 4> node_indices,
EntityIndex material_index, EntityIndex section_index,
SourceLocation location);
ElementDefinitionKind Kind() const noexcept override;
const SourceEntityId& SourceId() const noexcept override;
std::string_view SourceElementType() const noexcept override;
const std::vector<EntityIndex>& NodeIndices() const noexcept override;
EntityIndex MaterialIndex() const noexcept override;
EntityIndex PropertyIndex() const noexcept override;
SourceEntityId source_id;
ShellSourceElementType source_type;
std::array<EntityIndex, 4> node_indices;
EntityIndex material_index;
EntityIndex section_index;
SourceLocation location;
private:
friend class Domain;
/// @brief Rebinds a shell-local section index to the unified property view.
void SetPropertyIndex(EntityIndex property_index) noexcept;
/// @brief Synchronizes the base view after parser-candidate construction.
void SynchronizeNodeIndices();
EntityIndex property_index_;
std::vector<EntityIndex> node_indices_view_;
};
/// @brief Stores bilinear shape values and natural-coordinate derivatives.
struct Mitc4ShapeFunctions {
std::array<double, 4> values;
@@ -213,11 +168,10 @@ class Mitc4Shell final : public Element {
void BindRuntime(ElementDofLayout layout, EntityIndex element_index,
SourceLocation location);
/// @brief Stores covariant, reciprocal, frame, and Jacobian data at one
/// @brief Stores covariant, frame, and Jacobian data at one
/// point.
struct GeometryData {
std::array<Vector3, 3> covariant;
std::array<Vector3, 3> reciprocal;
Mitc4LocalFrame frame;
double jacobian;
};
@@ -0,0 +1,60 @@
#ifndef FESA_ELEMENTS_MITC4_SHELL_DEFINITION_H_
#define FESA_ELEMENTS_MITC4_SHELL_DEFINITION_H_
#include <array>
#include <string_view>
#include <vector>
#include "fesa/elements/element_definition.h"
namespace fesa {
class Domain;
/// @brief Preserves the source shell type independently of formulation.
enum class ShellSourceElementType { kS4, kS4r };
/// @brief Names the internal shell formulation selected by S4 and S4R.
inline constexpr std::string_view kMitc4InternalFormulation{"FESA-MITC4"};
/// @brief Defines one four-node S4/S4R semantic element.
class Mitc4ShellDefinition final : public ElementDefinition {
public:
/// @brief Constructs a parser-validated semantic definition.
Mitc4ShellDefinition(SourceEntityId source_id,
ShellSourceElementType source_type,
std::array<EntityIndex, 4> node_indices,
EntityIndex material_index, EntityIndex section_index,
SourceLocation location);
ElementDefinitionKind Kind() const noexcept override;
const SourceEntityId& SourceId() const noexcept override;
std::string_view SourceElementType() const noexcept override;
const std::vector<EntityIndex>& NodeIndices() const noexcept override;
EntityIndex MaterialIndex() const noexcept override;
EntityIndex PropertyIndex() const noexcept override;
SourceEntityId source_id;
ShellSourceElementType source_type;
std::array<EntityIndex, 4> node_indices;
EntityIndex material_index;
EntityIndex section_index;
SourceLocation location;
private:
friend class Domain;
/// @brief Rebinds a shell-local section index to the unified property view.
void SetPropertyIndex(EntityIndex property_index) noexcept;
/// @brief Synchronizes the base view after parser-candidate construction.
void SynchronizeNodeIndices();
EntityIndex property_index_;
std::vector<EntityIndex> node_indices_view_;
};
} // namespace fesa
#endif // FESA_ELEMENTS_MITC4_SHELL_DEFINITION_H_
@@ -0,0 +1,20 @@
#ifndef FESA_ELEMENTS_THERMAL_DOF_COMPONENT_H_
#define FESA_ELEMENTS_THERMAL_DOF_COMPONENT_H_
namespace fesa {
/// @brief Identifies the fixed bottom-to-top thermal station source DOFs.
/// @note This is separate from the six-component structural DofComponent.
enum class ThermalDofComponent : int {
kBottom = 11,
kLower = 12,
kMiddle = 13,
kUpper = 14,
kTop = 15
};
} // namespace fesa
#endif // FESA_ELEMENTS_THERMAL_DOF_COMPONENT_H_
@@ -3,7 +3,8 @@
#include <array>
#include "fesa/model/model_types.h"
#include "fesa/core/source_identity.h"
#include "fesa/elements/thermal_dof_component.h"
namespace fesa {
+3 -8
View File
@@ -6,25 +6,20 @@
#include <optional>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/constraints/boundary_condition.h"
#include "fesa/elements/element.h"
#include "fesa/elements/element_dof_layout.h"
#include "fesa/elements/thermal_element_layout.h"
#include "fesa/math/sparse_pattern.h"
#include "fesa/math/vector.h"
namespace fesa {
class AnalysisModel;
class DofManagerTestAccess;
/// @brief Distinguishes six-component, Solid translation, and thermal maps.
enum class DofProfile { kStructural, kThermal, kSolidStructural };
/// @brief Stores the stable full-space CSR pattern.
struct SparsePattern {
std::vector<std::size_t> row_offsets;
std::vector<std::size_t> column_indices;
};
/// @brief Owns full/free/constrained numbering, scatter maps, and CSR pattern.
class DofManager {
public:
+20
View File
@@ -32,6 +32,26 @@ class Matrix {
/// @brief Returns the column count.
std::size_t Columns() const noexcept;
/// @brief Returns mutable row-major contiguous storage.
/// @note Borrows Rows()*Columns() entries until storage is moved, assigned
/// or destroyed.
double* Data() noexcept;
/// @brief Returns immutable row-major contiguous storage.
/// @note Borrows Rows()*Columns() entries until storage is moved, assigned
/// or destroyed.
const double* Data() const noexcept;
/// @brief Computes the robust Frobenius norm through the dense backend.
/// @note Empty storage has zero norm; nonfinite values propagate as in Vector.
/// @throws std::length_error if storage exceeds the backend integer range.
double Norm() const;
/// @brief Scales every entry by alpha through the dense backend.
/// @note Empty storage is unchanged; nonfinite values follow BLAS semantics.
/// @throws std::length_error if storage exceeds the backend integer range.
void Scale(double alpha);
/// @brief Returns a bounds-checked mutable entry.
/// @throws std::out_of_range if the index is outside the matrix.
double& operator()(std::size_t row, std::size_t column);
+1 -2
View File
@@ -5,12 +5,11 @@
#include <vector>
#include "fesa/core/status.h"
#include "fesa/math/sparse_pattern.h"
#include "fesa/math/vector.h"
namespace fesa {
struct SparsePattern;
/// @brief Carries one deterministic element-local COO contribution.
struct CooContribution {
std::size_t row;
+18
View File
@@ -0,0 +1,18 @@
#ifndef FESA_MATH_SPARSE_PATTERN_H_
#define FESA_MATH_SPARSE_PATTERN_H_
#include <cstddef>
#include <vector>
namespace fesa {
/// @brief Stores the stable full-space CSR pattern.
struct SparsePattern {
std::vector<std::size_t> row_offsets;
std::vector<std::size_t> column_indices;
};
} // namespace fesa
#endif // FESA_MATH_SPARSE_PATTERN_H_
+2
View File
@@ -11,6 +11,8 @@
#include "fesa/constraints/boundary_condition.h"
#include "fesa/constraints/prescribed_displacement.h"
#include "fesa/core/status.h"
#include "fesa/elements/ds4_thermal.h"
#include "fesa/materials/isotropic_thermal_material.h"
#include "fesa/loads/concentrated_nodal_load.h"
#include "fesa/loads/load.h"
#include "fesa/model/model_types.h"
+3 -14
View File
@@ -13,12 +13,11 @@
#include "fesa/core/diagnostic.h"
#include "fesa/core/source_identity.h"
#include "fesa/elements/ds4_thermal.h"
#include "fesa/elements/euler_beam_3d.h"
#include "fesa/elements/euler_beam_3d_definition.h"
#include "fesa/elements/hex8_definition.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/elements/mitc4_shell_definition.h"
#include "fesa/elements/thermal_dof_component.h"
#include "fesa/materials/isotropic_linear_elastic_material.h"
#include "fesa/materials/isotropic_thermal_material.h"
#include "fesa/properties/general_beam_section.h"
#include "fesa/properties/shell_section.h"
#include "fesa/properties/solid_section.h"
@@ -302,16 +301,6 @@ struct ThermalInitialConditionDefinition {
SourceLocation location;
};
/// @brief Identifies the fixed bottom-to-top thermal station source DOFs.
/// @note This is separate from the six-component structural DofComponent.
enum class ThermalDofComponent : int {
kBottom = 11,
kLower = 12,
kMiddle = 13,
kUpper = 14,
kTop = 15
};
/// @brief Stores one prescribed thermal source DOF value.
struct ThermalBoundaryConditionDefinition {
EntityIndex node_index;
@@ -38,6 +38,14 @@ class SourceTargetIndex {
/// @return An owning index that preserves Domain declaration order.
static SourceTargetIndex FromDomain(const Domain& domain);
/// @brief Copies node and set identities before Domain ownership begins.
/// @param nodes Semantic nodes in stable expanded declaration order.
/// @param node_sets Node memberships in stable source order.
/// @return An owning node-only index preserving raw spelling and scope.
/// @note The returned index outlives its inputs; no Domain is constructed.
static SourceTargetIndex FromNodeDefinitions(
const std::vector<Node>& nodes, const std::vector<NodeSet>& node_sets);
/// @brief Returns owned entries without exposing mutable index state.
const std::vector<SourceTargetIndexEntry>& Entries() const noexcept;
@@ -0,0 +1,39 @@
#ifndef FESA_RESULTS_PREPARED_RESULTS_WRITER_H_
#define FESA_RESULTS_PREPARED_RESULTS_WRITER_H_
#include <filesystem>
#include <vector>
#include "fesa/analysis/prepared_analysis_outcome.h"
#include "fesa/core/diagnostic.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
class ResultsWriter;
class NonlinearResultsWriter;
class EigenResultsWriter;
class DynamicResultsWriter;
class ThermalResultsWriter;
class SolidResultsWriter;
/// @brief Routes one completed typed outcome to its authoritative writer.
/// @pre Domain, outcome, diagnostics, and adapters remain alive for this call.
/// The caller has closed its analysis log successfully before publication.
/// @param results_path Explicit output path, retaining each writer's contract.
/// @note Optional adapters are required only for their matching outcome family.
/// @return The selected writer's Status after candidate validation/finalization,
/// or the existing missing-adapter output failure without invoking a writer.
Status WritePreparedAnalysisResult(
const std::filesystem::path& results_path, const Domain& domain,
const PreparedAnalysisOutcome& outcome,
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
DynamicResultsWriter* dynamic_writer = nullptr,
ThermalResultsWriter* thermal_writer = nullptr,
SolidResultsWriter* solid_writer = nullptr);
} // namespace fesa
#endif // FESA_RESULTS_PREPARED_RESULTS_WRITER_H_
+99
View File
@@ -0,0 +1,99 @@
#ifndef FESA_SOLVERS_EIGEN_EIGEN_SOLVER_H_
#define FESA_SOLVERS_EIGEN_EIGEN_SOLVER_H_
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "fesa/core/status.h"
#include "fesa/math/matrix.h"
namespace fesa {
class SparseMatrix;
/// @brief Records one deterministic private-backend invocation.
struct FeastBackendCall {
std::size_t ordinal;
std::string kind;
std::string purpose;
double lower;
double upper;
std::size_t allocated_m0;
int info;
bool count_available;
std::size_t count;
bool loop_available;
int loop;
bool epsout_available;
double epsout;
std::string outcome;
std::string decision_reason;
};
/// @brief Owns the complete validated FEAST interval result before public N
/// selection.
struct FeastEigenResult {
std::size_t requested_modes;
std::vector<double> eigenvalues;
Matrix eigenvectors;
double operator_scale;
std::vector<std::size_t> column_ids;
std::vector<bool> backend_res_available;
std::vector<double> backend_res;
std::vector<double> normalized_eigenvalues;
std::vector<double> whitened_residuals;
double gram_error;
std::vector<FeastBackendCall> backend_calls;
std::vector<std::int64_t> base_fpm;
std::string runtime_version;
std::string integer_abi;
std::size_t thread_count;
std::uint64_t seed;
double enclosure_lower;
double enclosure_upper;
double solve_lower;
double solve_upper;
std::string upper_search_termination;
};
/// @brief Solves symmetric standard or definite generalized eigenproblems.
/// @note Inputs remain caller-owned and are borrowed only for each call.
class EigenSolver {
public:
/// @brief Destroys a backend-neutral eigen solver.
virtual ~EigenSolver() = default;
/// @brief Computes all validated interval pairs for a requested lowest-mode
/// prefix.
/// @param whitened_operator Symmetric physical-lambda operator after
/// whitening.
/// @param requested_modes Number of lowest modes eventually exposed by the
/// caller.
/// @param zero_operator_scale Positive physical scale used only when A is
/// exactly zero.
/// @return Sorted all-c eigenpairs in physical lambda units or a solver
/// failure.
virtual Result<FeastEigenResult> Solve(
const SparseMatrix& whitened_operator, std::size_t requested_modes,
double zero_operator_scale) const = 0;
/// @brief Solves the original sparse symmetric definite K/M pencil.
/// @param mass_lower_bound Certified positive lower spectral bound of M,
/// including floating-point assembly uncertainty; never a diagonal estimate.
/// @param classification_scale Positive max |Kii|/Mii in physical units.
/// @note Eigenvectors are mass-normalized. For this API the historical
/// whitened_residuals field contains original-pencil eta_r; no whitening
/// is performed. Intervals/eigenvalues in normalized backend evidence use
/// classification_scale, while eigenvalues are returned in physical units.
virtual Result<FeastEigenResult> SolveGeneralized(
const SparseMatrix& stiffness, const SparseMatrix& mass,
std::size_t requested_modes, double mass_lower_bound,
double classification_scale) const = 0;
};
} // namespace fesa
#endif // FESA_SOLVERS_EIGEN_EIGEN_SOLVER_H_
@@ -1,65 +1,12 @@
#ifndef FESA_SOLVERS_EIGEN_MKL_FEAST_EIGEN_SOLVER_H_
#define FESA_SOLVERS_EIGEN_MKL_FEAST_EIGEN_SOLVER_H_
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "fesa/core/status.h"
#include "fesa/math/matrix.h"
#include "fesa/solvers/eigen/eigen_solver.h"
namespace fesa {
class SparseMatrix;
/// @brief Records one deterministic private-backend invocation.
struct FeastBackendCall {
std::size_t ordinal;
std::string kind;
std::string purpose;
double lower;
double upper;
std::size_t allocated_m0;
int info;
bool count_available;
std::size_t count;
bool loop_available;
int loop;
bool epsout_available;
double epsout;
std::string outcome;
std::string decision_reason;
};
/// @brief Owns the complete validated FEAST interval result before public N
/// selection.
struct FeastEigenResult {
std::size_t requested_modes;
std::vector<double> eigenvalues;
Matrix eigenvectors;
double operator_scale;
std::vector<std::size_t> column_ids;
std::vector<bool> backend_res_available;
std::vector<double> backend_res;
std::vector<double> normalized_eigenvalues;
std::vector<double> whitened_residuals;
double gram_error;
std::vector<FeastBackendCall> backend_calls;
std::vector<std::int64_t> base_fpm;
std::string runtime_version;
std::string integer_abi;
std::size_t thread_count;
std::uint64_t seed;
double enclosure_lower;
double enclosure_upper;
double solve_lower;
double solve_upper;
std::string upper_search_termination;
};
/// @brief Adapts standard and symmetric-definite sparse pencils to FEAST.
class MklFeastEigenSolver final {
class MklFeastEigenSolver final : public EigenSolver {
public:
/// @brief Computes all validated interval pairs for a requested lowest-mode
/// prefix.
@@ -74,8 +21,8 @@ class MklFeastEigenSolver final {
/// @note The implementation fixes oneMKL local thread count to one during
/// every call.
Result<FeastEigenResult> Solve(const SparseMatrix& whitened_operator,
std::size_t requested_modes,
double zero_operator_scale) const;
std::size_t requested_modes,
double zero_operator_scale) const override;
/// @brief Solves the original sparse symmetric definite K/M pencil.
/// @param mass_lower_bound Certified positive lower spectral bound of M,
/// including floating-point assembly uncertainty; never a diagonal estimate.
@@ -86,16 +33,16 @@ class MklFeastEigenSolver final {
/// classification_scale, while eigenvalues are returned in physical units.
Result<FeastEigenResult> SolveGeneralized(const SparseMatrix& stiffness,
const SparseMatrix& mass,
std::size_t requested_modes,
std::size_t requested_modes,
double mass_lower_bound,
double classification_scale) const;
double classification_scale) const override;
private:
/// @brief Shares deterministic count/search controls without changing
/// pencils.
Result<FeastEigenResult> SolveImpl(const SparseMatrix& stiffness,
const SparseMatrix* mass,
std::size_t requested_modes,
std::size_t requested_modes,
double physical_scale,
double mass_lower_bound) const;
};
+17
View File
@@ -0,0 +1,17 @@
# 직접 유지하는 MSVC 빌드
이 영역은 FESA 솔루션의 solver library·CLI·별도 테스트 프로젝트와 공통 옵션·DLL 준비를
소유한다. CMake가 생성한 프로젝트로 교체하거나 빌드 전에 CMake를 숨겨서 호출하지 않는다.
요소 수식·검사 본문·기준 데이터는 이 영역의 소유가 아니다.
- Source 추가·이동·제거는 대응하는 CMake target 목록과 같은 변경 단위로 처리한다.
한 owner가 두 목록을 대조하며 solver와 검사 본문을 빌드 경로별로 복제하지 않는다.
- `Fesa.Common.props`와 `Fesa.Runtime.targets`의 CRT·산술·define·link·runtime staging을
함께 확인한다. 개인 설치 경로는 환경변수 또는 ignored `Fesa.Local.props`로 주입한다.
- AVX2 옵션은 전용 Hex8 source에만 둔다. Dispatch source와 나머지 target으로 확장하지 않는다.
외부 GoogleTest source에 FESA 제품의 `/W4 /WX` 정책을 강제하지 않는다.
- `MSBuild FESA.sln /m:2 /p:Configuration=Debug /p:Platform=x64`는 저장소 root의
VS x64 개발 환경에서 실행한다. 별도 테스트 executable이 같은 solver library를 연결하고
실행 파일 곁의 configuration별 DLL도 준비됐는지 확인한다. 빌드 성공은 검사 통과가 아니다.
- 빌드 설정을 바꾸면 CMake와 실제 source·옵션·link·DLL 및 발견한 검사 identity를 대조한다.
기존 실행 로그와 바이너리를 덮어쓰지 말고 변경 전후 근거를 구분한다.
+75
View File
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Override installation roots with environment variables or this ignored sheet. -->
<Import Project="Fesa.Local.props" Condition="Exists('Fesa.Local.props')" />
<PropertyGroup>
<FesaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\'))</FesaRoot>
<FesaBuildRoot>$(FesaRoot).build\native\$(Configuration)\</FesaBuildRoot>
<FesaRootForward>$(FesaRoot.Replace('\','/').TrimEnd('/'))</FesaRootForward>
<FesaBuildRootForward>$(FesaBuildRoot.Replace('\','/').TrimEnd('/'))</FesaBuildRootForward>
<OutDir>$(FesaBuildRoot)tests\</OutDir>
<OutDir Condition="'$(MSBuildProjectName)'=='fesa_cli'">$(FesaBuildRoot)src\fesa\</OutDir>
<OutDir Condition="'$(ConfigurationType)'=='StaticLibrary'">$(FesaBuildRoot)lib\</OutDir>
<IntDir>$(FesaBuildRoot)obj\$(MSBuildProjectName)\</IntDir>
<TargetName Condition="'$(MSBuildProjectName)'=='fesa_cli'">fesa</TargetName>
<TbbSuffix Condition="'$(Configuration)'=='Debug'">_debug</TbbSuffix>
</PropertyGroup>
<PropertyGroup>
<FesaExternalOptions Condition="'$(MSBuildProjectName)'=='fesa_solver'">/external:I&quot;$(FESA_MKL_ROOT)\include&quot; /external:I&quot;$(FESA_TBB_ROOT)\include&quot; /external:I&quot;$(FESA_HDF5_ROOT)\include&quot; /external:W0</FesaExternalOptions>
<FesaExternalOptions Condition="'$(MSBuildProjectName)'=='fesa_unit_tests' Or '$(MSBuildProjectName)'=='fesa_integration_tests' Or '$(MSBuildProjectName)'=='fesa_reference_tests'">/external:I&quot;$(FESA_HDF5_ROOT)\include&quot; /external:I&quot;$(FESA_GTEST_SOURCE_DIR)\googletest\include&quot; /external:I&quot;$(FESA_GTEST_SOURCE_DIR)\googletest&quot; /external:W0</FesaExternalOptions>
<FesaExternalOptions Condition="'$(MSBuildProjectName)'=='fesa_unit_tests'">$(FesaExternalOptions) /external:I&quot;$(FESA_MKL_ROOT)\include&quot;</FesaExternalOptions>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<LanguageStandard>stdcpp17</LanguageStandard>
<SupportJustMyCode>false</SupportJustMyCode>
<ExceptionHandling>Sync</ExceptionHandling>
<WarningLevel>Level3</WarningLevel>
<TreatWarningAsError>false</TreatWarningAsError>
<WarningLevel Condition="'$(MSBuildProjectName)'=='fesa_solver' Or '$(MSBuildProjectName)'=='fesa_cli'">Level4</WarningLevel>
<TreatWarningAsError Condition="'$(MSBuildProjectName)'=='fesa_solver' Or '$(MSBuildProjectName)'=='fesa_cli'">true</TreatWarningAsError>
<RuntimeLibrary Condition="'$(Configuration)'=='Debug'">MultiThreadedDebugDLL</RuntimeLibrary>
<RuntimeLibrary Condition="'$(Configuration)'=='Release'">MultiThreadedDLL</RuntimeLibrary>
<Optimization Condition="'$(Configuration)'=='Debug'">Disabled</Optimization>
<InlineFunctionExpansion Condition="'$(Configuration)'=='Debug'">Disabled</InlineFunctionExpansion>
<BasicRuntimeChecks Condition="'$(Configuration)'=='Debug'">EnableFastChecks</BasicRuntimeChecks>
<Optimization Condition="'$(Configuration)'=='Release'">MaxSpeed</Optimization>
<InlineFunctionExpansion Condition="'$(Configuration)'=='Release'">AnySuitable</InlineFunctionExpansion>
<DebugInformationFormat Condition="'$(Configuration)'=='Debug'">ProgramDatabase</DebugInformationFormat>
<DebugInformationFormat Condition="'$(Configuration)'=='Release' And ('$(MSBuildProjectName)'=='fesa_solver' Or '$(MSBuildProjectName)'=='fesa_cli')">ProgramDatabase</DebugInformationFormat>
<FloatingPointModel>Precise</FloatingPointModel>
<PreprocessorDefinitions>WIN32;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(Configuration)'=='Release'">NDEBUG;WIN32;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(FesaRoot)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(MSBuildProjectName)'=='fesa_solver' Or '$(MSBuildProjectName)'=='fesa_unit_tests' Or '$(MSBuildProjectName)'=='fesa_integration_tests'">$(FesaRoot)include;$(FesaRoot)src\fesa;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>$(FesaExternalOptions) /MP2 %(AdditionalOptions)</AdditionalOptions>
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
</ClCompile>
<Link>
<TargetMachine>MachineX64</TargetMachine>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation Condition="'$(Configuration)'=='Release' And '$(MSBuildProjectName)'=='fesa_cli'">true</GenerateDebugInformation>
<OptimizeReferences Condition="'$(Configuration)'=='Release' And '$(MSBuildProjectName)'=='fesa_cli'">true</OptimizeReferences>
<EnableCOMDATFolding Condition="'$(Configuration)'=='Release' And '$(MSBuildProjectName)'=='fesa_cli'">true</EnableCOMDATFolding>
<GenerateDebugInformation Condition="'$(Configuration)'=='Debug'">true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(FESA_MKL_ROOT)\lib;$(FESA_OMP_ROOT)\lib;$(FESA_TBB_ROOT)\lib;$(FESA_HDF5_ROOT)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>mkl_intel_ilp64_dll.lib;mkl_intel_thread_dll.lib;mkl_core_dll.lib;libiomp5md.lib;tbb12$(TbbSuffix).lib;hdf5.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(MSBuildProjectName)'=='fesa_solver'">
<ClCompile><PreprocessorDefinitions>MKL_ILP64;H5_BUILT_AS_DYNAMIC_LIB;__TBB_NO_IMPLICIT_LINKAGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions></ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(MSBuildProjectName)'=='fesa_unit_tests'">
<ClCompile><PreprocessorDefinitions>MKL_ILP64;%(PreprocessorDefinitions)</PreprocessorDefinitions></ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(MSBuildProjectName)'=='fesa_unit_tests' Or '$(MSBuildProjectName)'=='fesa_integration_tests' Or '$(MSBuildProjectName)'=='fesa_reference_tests'">
<ClCompile><PreprocessorDefinitions>H5_BUILT_AS_DYNAMIC_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions></ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(MSBuildProjectName)'=='gtest'">
<ClCompile>
<AdditionalIncludeDirectories>$(FESA_GTEST_SOURCE_DIR)\googletest\include;$(FESA_GTEST_SOURCE_DIR)\googletest</AdditionalIncludeDirectories>
<AdditionalOptions>/utf-8 /J %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>GTEST_HAS_PTHREAD=0;_HAS_EXCEPTIONS=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
</Project>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="ValidateFesaDependencies" BeforeTargets="PrepareForBuild">
<Error Condition="'$(Platform)'!='x64'" Text="FESA requires MSVC x64." />
<Error Condition="!Exists('$(FESA_GTEST_SOURCE_DIR)\googletest\src\gtest-all.cc')" Text="Set FESA_GTEST_SOURCE_DIR to the approved local GoogleTest source checkout." />
<Error Condition="!Exists('$(FESA_MKL_ROOT)\lib\mkl_intel_ilp64_dll.lib')" Text="Set FESA_MKL_ROOT to the oneMKL installation root." />
<Error Condition="!Exists('$(FESA_TBB_ROOT)\lib\tbb12$(TbbSuffix).lib')" Text="Set FESA_TBB_ROOT to the oneTBB installation root." />
<Error Condition="!Exists('$(FESA_HDF5_ROOT)\lib\hdf5.lib')" Text="Set FESA_HDF5_ROOT to the shared HDF5 installation root." />
<Error Condition="!Exists('$(FESA_OMP_ROOT)\bin\libiomp5md.dll') Or !Exists('$(FESA_OMP_ROOT)\bin\libmmd.dll')" Text="Set FESA_OMP_ROOT to the Intel compiler runtime installation root." />
</Target>
<Target Name="StageFesaRuntime" AfterTargets="Build" Condition="'$(ConfigurationType)'=='Application'">
<ItemGroup>
<FesaMklThread Include="$(FESA_MKL_ROOT)\bin\mkl_intel_thread.*.dll" />
<FesaMklCore Include="$(FESA_MKL_ROOT)\bin\mkl_core.*.dll" />
<FesaMklDispatch Include="$(FESA_MKL_ROOT)\bin\mkl_def.*.dll" />
<FesaRuntime Include="@(FesaMklThread);@(FesaMklCore);@(FesaMklDispatch);$(FESA_TBB_ROOT)\bin\tbb12$(TbbSuffix).dll;$(FESA_OMP_ROOT)\bin\libiomp5md.dll;$(FESA_OMP_ROOT)\bin\libmmd.dll;$(FESA_HDF5_ROOT)\bin\hdf5.dll" />
</ItemGroup>
<Error Condition="'@(FesaMklThread)'==''" Text="The oneMKL Intel-thread runtime DLL was not resolved." />
<Error Condition="'@(FesaMklCore)'==''" Text="The oneMKL core runtime DLL was not resolved." />
<Error Condition="'@(FesaMklDispatch)'==''" Text="The oneMKL default-dispatch runtime DLL was not resolved." />
<Copy SourceFiles="@(FesaRuntime)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" />
</Target>
</Project>
+18
View File
@@ -0,0 +1,18 @@
<?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>{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}</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" />
<ItemGroup>
<ClCompile Include="..\src\fesa\app\main.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>
+30
View File
@@ -0,0 +1,30 @@
<?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>{7770C26B-DBFE-505D-8788-8B7EFD9A5A70}</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" />
<ItemGroup>
<ClCompile Include="..\tests\integration\analysis\linear_static_analysis_test.cpp" />
<ClCompile Include="..\tests\integration\analysis\linear_frequency_procedure_test.cpp" />
<ClCompile Include="..\tests\integration\analysis\shell_surface_pressure_linear_test.cpp" />
<ClCompile Include="..\tests\integration\analysis\shell_surface_pressure_nonlinear_test.cpp" />
<ClCompile Include="..\tests\integration\analysis\nonlinear_static_procedure_test.cpp" />
<ClCompile Include="..\tests\integration\analysis\thermal_physics_test.cpp" />
<ClCompile Include="..\tests\integration\app\fesa_application_test.cpp" />
<ClCompile Include="..\tests\integration\app\eigen_application_test.cpp" />
<ClCompile Include="..\tests\integration\app\shell_surface_pressure_application_test.cpp" />
<ClCompile Include="..\tests\integration\app\thermal_application_test.cpp" />
<ClCompile Include="..\tests\integration\app\solid_application_test.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="fesa_solver.vcxproj"><Project>{3AEC39D9-4641-5161-9107-265EB50CA882}</Project><LinkLibraryDependencies>true</LinkLibraryDependencies></ProjectReference>
<ProjectReference Include="gtest.vcxproj"><Project>{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}</Project><LinkLibraryDependencies>true</LinkLibraryDependencies></ProjectReference>
<ProjectReference Include="fesa_cli.vcxproj"><Project>{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}</Project><LinkLibraryDependencies>false</LinkLibraryDependencies></ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="Fesa.Runtime.targets" />
</Project>
+35
View File
@@ -0,0 +1,35 @@
<?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>{330D2DFB-E57D-517D-A795-F1EA620B69F0}</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><PreprocessorDefinitions>FESA_TEST_SOURCE_DIR="$(FesaRootForward)";FESA_TEST_BINARY_DIR="$(FesaBuildRootForward)";%(PreprocessorDefinitions)</PreprocessorDefinitions></ClCompile></ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\tests\reference\reference_tolerance_policy.cpp" />
<ClCompile Include="..\tests\reference\eigen_reference_comparison.cpp" />
<ClCompile Include="..\tests\reference\eigen_reference_comparison_test.cpp" />
<ClCompile Include="..\tests\reference\eigen_reference_cases_test.cpp" />
<ClCompile Include="..\tests\reference\reference_tolerance_policy_test.cpp" />
<ClCompile Include="..\tests\reference\reference_comparison.cpp" />
<ClCompile Include="..\tests\reference\reference_comparison_test.cpp" />
<ClCompile Include="..\tests\reference\b33_reference_comparison_test.cpp" />
<ClCompile Include="..\tests\reference\mitc4_reference_comparison.cpp" />
<ClCompile Include="..\tests\reference\mitc4_reference_comparison_test.cpp" />
<ClCompile Include="..\tests\reference\mitc4_reference_cases_test.cpp" />
<ClCompile Include="..\tests\reference\nonlinear_static_reference_comparison.cpp" />
<ClCompile Include="..\tests\reference\nonlinear_static_reference_cases_test.cpp" />
<ClCompile Include="..\tests\reference\shell_surface_pressure_reference_test.cpp" />
<ClCompile Include="..\tests\reference\thermal_reference_comparison.cpp" />
<ClCompile Include="..\tests\reference\thermal_reference_comparison_test.cpp" />
<ClCompile Include="..\tests\reference\thermal_reference_cases_test.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="fesa_solver.vcxproj"><Project>{3AEC39D9-4641-5161-9107-265EB50CA882}</Project><LinkLibraryDependencies>true</LinkLibraryDependencies></ProjectReference>
<ProjectReference Include="gtest.vcxproj"><Project>{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}</Project><LinkLibraryDependencies>true</LinkLibraryDependencies></ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="Fesa.Runtime.targets" />
</Project>
+118
View File
@@ -0,0 +1,118 @@
<?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>{3AEC39D9-4641-5161-9107-265EB50CA882}</ProjectGuid><Keyword>Win32Proj</Keyword><WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion></PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration"><ConfigurationType>StaticLibrary</ConfigurationType><PlatformToolset>v145</PlatformToolset><UseDebugLibraries Condition="'$(Configuration)'=='Debug'">true</UseDebugLibraries></PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="Fesa.Common.props" />
<ItemDefinitionGroup><ClCompile><PreprocessorDefinitions>FESA_HEX8_PAIR_AVX2=1;%(PreprocessorDefinitions)</PreprocessorDefinitions></ClCompile></ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\src\fesa\analysis\analysis_model.cpp" />
<ClCompile Include="..\src\fesa\analysis\analysis_state.cpp" />
<ClCompile Include="..\src\fesa\analysis\dynamic_analysis_state.cpp" />
<ClCompile Include="..\src\fesa\analysis\eigen_reduction.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_time_integration.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_algebra.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_assembly.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_reduced_operators.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_reduced_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\linear_frequency_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\linear_static_analysis.cpp" />
<ClCompile Include="..\src\fesa\analysis\linear_static_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\newton_iteration_log.cpp" />
<ClCompile Include="..\src\fesa\analysis\nonlinear_analysis_state.cpp" />
<ClCompile Include="..\src\fesa\analysis\nonlinear_static_control.cpp" />
<ClCompile Include="..\src\fesa\analysis\nonlinear_static_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\prepared_analysis.cpp" />
<ClCompile Include="..\src\fesa\analysis\solid_dynamic_analysis_state.cpp" />
<ClCompile Include="..\src\fesa\analysis\solid_frequency_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\solid_hht_dynamic_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\solid_static_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\solid_static_state.cpp" />
<ClCompile Include="..\src\fesa\analysis\thermal_analysis_state.cpp" />
<ClCompile Include="..\src\fesa\analysis\thermal_time_grid.cpp" />
<ClCompile Include="..\src\fesa\analysis\thermal_transient_algebra.cpp" />
<ClCompile Include="..\src\fesa\analysis\transient_heat_procedure.cpp" />
<ClCompile Include="..\src\fesa\app\fesa_application.cpp" />
<ClCompile Include="..\src\fesa\app\output_paths.cpp" />
<ClCompile Include="..\src\fesa\assembly\load_assembler.cpp" />
<ClCompile Include="..\src\fesa\assembly\parallel_for.cpp" />
<ClCompile Include="..\src\fesa\assembly\sparse_assembler.cpp" />
<ClCompile Include="..\src\fesa\assembly\surface_pressure_assembler.cpp" />
<ClCompile Include="..\src\fesa\assembly\thermal_assembler.cpp" />
<ClCompile Include="..\src\fesa\build_info.cpp" />
<ClCompile Include="..\src\fesa\constraints\essential_constraint_policy.cpp" />
<ClCompile Include="..\src\fesa\constraints\prescribed_displacement.cpp" />
<ClCompile Include="..\src\fesa\core\ascii.cpp" />
<ClCompile Include="..\src\fesa\core\diagnostic.cpp" />
<ClCompile Include="..\src\fesa\core\status.cpp" />
<ClCompile Include="..\src\fesa\elements\element_capabilities.cpp" />
<ClCompile Include="..\src\fesa\elements\element_factory.cpp" />
<ClCompile Include="..\src\fesa\elements\hex8_element.cpp" />
<ClCompile Include="..\src\fesa\elements\hex8_high_low.cpp" />
<ClCompile Include="..\src\fesa\elements\hex8_pair_simd_dispatch.cpp" />
<ClCompile Include="..\src\fesa\elements\hex8_pair_simd_avx2.cpp"><EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet></ClCompile>
<ClCompile Include="..\src\fesa\elements\hex8_geometry.cpp" />
<ClCompile Include="..\src\fesa\elements\euler_beam_3d.cpp" />
<ClCompile Include="..\src\fesa\elements\finite_rotation.cpp" />
<ClCompile Include="..\src\fesa\elements\mitc4_shell.cpp" />
<ClCompile Include="..\src\fesa\elements\nonlinear_mitc4_shell.cpp" />
<ClCompile Include="..\src\fesa\elements\nonlinear_mitc4_inertia.cpp" />
<ClCompile Include="..\src\fesa\elements\thermal_shell.cpp" />
<ClCompile Include="..\src\fesa\elements\thermal_shell_geometry.cpp" />
<ClCompile Include="..\src\fesa\fem\dof_manager.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\domain_builder.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\domain_mapping_context.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\domain_mapper.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\input_reader.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\material_property_mapper.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\step_definition_mapper.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\surface_definition_mapper.cpp" />
<ClCompile Include="..\src\fesa\io\abaqus\topology_mapper.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\hdf5_atomic_file.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\dynamic_hdf5_results_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\hdf5_model_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\nonlinear_hdf5_results_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\hdf5_primitives.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\hdf5_result_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\hdf5_results_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\eigen_hdf5_results_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\eigen_hdf5_self_check.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\hdf5_self_check.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\thermal_hdf5_results_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\thermal_hdf5_self_check.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\solid_hdf5_results_writer.cpp" />
<ClCompile Include="..\src\fesa\io\hdf5\solid_hdf5_self_check.cpp" />
<ClCompile Include="..\src\fesa\loads\concentrated_nodal_load.cpp" />
<ClCompile Include="..\src\fesa\loads\shell_surface_pressure.cpp" />
<ClCompile Include="..\src\fesa\materials\isotropic_linear_elastic_material.cpp" />
<ClCompile Include="..\src\fesa\materials\isotropic_thermal_material.cpp" />
<ClCompile Include="..\src\fesa\math\dense_blas_internal.cpp" />
<ClCompile Include="..\src\fesa\math\matrix.cpp" />
<ClCompile Include="..\src\fesa\math\sparse_matrix.cpp" />
<ClCompile Include="..\src\fesa\math\vector.cpp" />
<ClCompile Include="..\src\fesa\model\domain.cpp" />
<ClCompile Include="..\src\fesa\model\shell_geometry.cpp" />
<ClCompile Include="..\src\fesa\model\source_target_resolver.cpp" />
<ClCompile Include="..\src\fesa\properties\general_beam_section.cpp" />
<ClCompile Include="..\src\fesa\properties\shell_section.cpp" />
<ClCompile Include="..\src\fesa\results\analysis_state_commit.cpp" />
<ClCompile Include="..\src\fesa\results\beam_result_recovery.cpp" />
<ClCompile Include="..\src\fesa\results\global_equilibrium_recovery.cpp" />
<ClCompile Include="..\src\fesa\results\prepared_results_writer.cpp" />
<ClCompile Include="..\src\fesa\results\recovery_candidate.cpp" />
<ClCompile Include="..\src\fesa\results\result_recovery.cpp" />
<ClCompile Include="..\src\fesa\results\shell_result_recovery.cpp" />
<ClCompile Include="..\src\fesa\solvers\eigen\mkl_feast_eigen_solver.cpp" />
<ClCompile Include="..\src\fesa\solvers\linear\mkl_pardiso_solver.cpp" />
<ClCompile Include="..\src\fesa\solvers\linear\mkl_pardiso_tangent_solver.cpp" />
<ClCompile Include="..\src\fesa\solvers\linear\pardiso_session.cpp" />
</ItemGroup>
<ItemGroup>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="Fesa.Runtime.targets" />
</Project>
+108
View File
@@ -0,0 +1,108 @@
<?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>{42558F77-73F2-5CA7-8E3C-BB0E1646CD45}</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" />
<ItemGroup>
<ClCompile Include="..\tests\unit\build_info_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\analysis_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\analysis_model_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\analysis_state_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\dynamic_analysis_state_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\eigen_reduction_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\eigen_mass_assembly_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_time_integration_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_shell_physics_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_energy_verification_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_temporal_verification_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_dynamic_algebra_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_reduced_shell_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_dynamic_procedure_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\nonlinear_analysis_state_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\nonlinear_static_control_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\newton_iteration_log_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\solid_frequency_procedure_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\solid_hht_dynamic_procedure_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\solid_hht_temporal_verification_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\solid_static_procedure_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\solid_static_state_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\thermal_time_grid_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\thermal_analysis_state_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\transient_heat_procedure_test.cpp" />
<ClCompile Include="..\tests\unit\assembly\parallel_for_test.cpp" />
<ClCompile Include="..\tests\unit\assembly\load_assembler_test.cpp" />
<ClCompile Include="..\tests\unit\assembly\surface_pressure_assembler_test.cpp" />
<ClCompile Include="..\tests\unit\assembly\sparse_assembler_test.cpp" />
<ClCompile Include="..\tests\unit\assembly\thermal_assembler_test.cpp" />
<ClCompile Include="..\tests\unit\app\output_paths_test.cpp" />
<ClCompile Include="..\tests\unit\constraints\boundary_condition_test.cpp" />
<ClCompile Include="..\tests\unit\constraints\essential_constraints_test.cpp" />
<ClCompile Include="..\tests\unit\core\ascii_test.cpp" />
<ClCompile Include="..\tests\unit\core\diagnostic_test.cpp" />
<ClCompile Include="..\tests\unit\core\source_identity_test.cpp" />
<ClCompile Include="..\tests\unit\core\status_test.cpp" />
<ClCompile Include="..\tests\unit\elements\element_capabilities_test.cpp" />
<ClCompile Include="..\tests\unit\elements\element_factory_test.cpp" />
<ClCompile Include="..\tests\unit\elements\hex8_element_test.cpp" />
<ClCompile Include="..\tests\unit\elements\hex8_high_low_test.cpp" />
<ClCompile Include="..\tests\unit\elements\euler_beam_3d_test.cpp" />
<ClCompile Include="..\tests\unit\elements\finite_rotation_test.cpp" />
<ClCompile Include="..\tests\unit\elements\mitc4_shell_test.cpp" />
<ClCompile Include="..\tests\unit\elements\mitc4_shell_mass_contract_test.cpp" />
<ClCompile Include="..\tests\unit\elements\nonlinear_mitc4_shell_test.cpp" />
<ClCompile Include="..\tests\unit\elements\nonlinear_mitc4_inertia_test.cpp" />
<ClCompile Include="..\tests\unit\elements\nonlinear_mitc4_shell_tangent_test.cpp" />
<ClCompile Include="..\tests\unit\elements\thermal_shell_test.cpp" />
<ClCompile Include="..\tests\unit\fem\dof_manager_test.cpp" />
<ClCompile Include="..\tests\unit\math\dense_blas_internal_test.cpp" />
<ClCompile Include="..\tests\unit\math\high_low_arithmetic_test.cpp" />
<ClCompile Include="..\tests\unit\math\matrix_test.cpp" />
<ClCompile Include="..\tests\unit\math\sparse_matrix_test.cpp" />
<ClCompile Include="..\tests\unit\math\vector3_test.cpp" />
<ClCompile Include="..\tests\unit\math\vector_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\domain_mapping_components_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\domain_mapper_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\hex8_semantics_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\eigen_domain_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\hht_dynamic_domain_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\thermal_domain_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\input_reader_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\input_syntax_test.cpp" />
<ClCompile Include="..\tests\unit\io\abaqus\shell_surface_pressure_domain_test.cpp" />
<ClCompile Include="..\tests\unit\io\hdf5\hdf5_results_writer_test.cpp" />
<ClCompile Include="..\tests\unit\io\hdf5\dynamic_hdf5_results_writer_test.cpp" />
<ClCompile Include="..\tests\unit\io\hdf5\eigen_hdf5_results_writer_test.cpp" />
<ClCompile Include="..\tests\unit\io\hdf5\nonlinear_hdf5_results_writer_test.cpp" />
<ClCompile Include="..\tests\unit\io\hdf5\hdf5_writer_components_test.cpp" />
<ClCompile Include="..\tests\unit\io\hdf5\thermal_hdf5_results_writer_test.cpp" />
<ClCompile Include="..\tests\unit\io\hdf5\solid_hdf5_results_writer_test.cpp" />
<ClCompile Include="..\tests\unit\loads\load_test.cpp" />
<ClCompile Include="..\tests\unit\loads\shell_surface_pressure_test.cpp" />
<ClCompile Include="..\tests\unit\model\domain_test.cpp" />
<ClCompile Include="..\tests\unit\model\thermal_semantics_test.cpp" />
<ClCompile Include="..\tests\unit\model\thermal_shell_geometry_test.cpp" />
<ClCompile Include="..\tests\unit\model\model_types_test.cpp" />
<ClCompile Include="..\tests\unit\model\shell_geometry_test.cpp" />
<ClCompile Include="..\tests\unit\model\source_target_resolver_test.cpp" />
<ClCompile Include="..\tests\unit\materials\material_test.cpp" />
<ClCompile Include="..\tests\unit\properties\element_property_test.cpp" />
<ClCompile Include="..\tests\unit\results\result_records_test.cpp" />
<ClCompile Include="..\tests\unit\results\result_recovery_components_test.cpp" />
<ClCompile Include="..\tests\unit\results\result_recovery_test.cpp" />
<ClCompile Include="..\tests\unit\results\results_writer_test.cpp" />
<ClCompile Include="..\tests\unit\solvers\eigen\mkl_feast_eigen_solver_test.cpp" />
<ClCompile Include="..\tests\unit\solvers\linear\linear_solver_test.cpp" />
<ClCompile Include="..\tests\unit\solvers\linear\mkl_pardiso_solver_test.cpp" />
<ClCompile Include="..\tests\unit\solvers\linear\tangent_solver_test.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="fesa_solver.vcxproj"><Project>{3AEC39D9-4641-5161-9107-265EB50CA882}</Project><LinkLibraryDependencies>true</LinkLibraryDependencies></ProjectReference>
<ProjectReference Include="gtest.vcxproj"><Project>{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}</Project><LinkLibraryDependencies>true</LinkLibraryDependencies></ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="Fesa.Runtime.targets" />
</Project>
+19
View File
@@ -0,0 +1,19 @@
<?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>{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}</ProjectGuid><Keyword>Win32Proj</Keyword><WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion></PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration"><ConfigurationType>StaticLibrary</ConfigurationType><PlatformToolset>v145</PlatformToolset><UseDebugLibraries Condition="'$(Configuration)'=='Debug'">true</UseDebugLibraries></PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="Fesa.Common.props" />
<ItemGroup>
<ClCompile Include="$(FESA_GTEST_SOURCE_DIR)\googletest\src\gtest-all.cc" />
<ClCompile Include="$(FESA_GTEST_SOURCE_DIR)\googletest\src\gtest_main.cc" />
</ItemGroup>
<ItemGroup>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="Fesa.Runtime.targets" />
</Project>
+11
View File
@@ -0,0 +1,11 @@
# 비교 보조 도구
`verification/compare_hex8.py`와 `compare_hht_dynamic.py`는 입력·reference·결과를
읽고 요청한 JSON report만 기록한다. Solver를 실행하지 않는다.
- 실행 전에 argv, 의존성, report 경로와 덮어쓰기 방지 검사를 확인한다.
- 원본 입력·CSV·TXT·결과 bytes와 frozen hash를 변경하지 않는다. 자료 부재나
hash 충돌은 실패 또는 미실행으로 보고하며 원본 보정으로 해결하지 않는다.
- identity·schema·finite 사전 gate와 승인된 family별 판정 영향을 유지한다.
Synthetic HDF5 fixture의 comparator 통과를 실제 solver의 물리 검증으로 보고하지 않는다.
- 실행 근거는 고유한 작업 directory에 남기고 기존 report와 로그를 덮어쓰지 않는다.
+807
View File
@@ -0,0 +1,807 @@
"""Compare original Hex8 CSV/TXT results with Solid HDF5; never launch a solver.
This entry point is intentionally restricted to reference/hexa's fixed original
models. Identity/schema checks precede finite checks and the common tolerance.
Rounded HDF5 observables are compared; internal high+low solver gates are not
recomputed from them. Missing original modal Value data remain NOT RUN.
"""
import argparse
import csv
from decimal import Decimal, InvalidOperation
import hashlib
import json
import math
import os
from pathlib import Path
import re
from typing import Any
import h5py
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
REFERENCE_DIRECTORY = ROOT / "reference/hexa"
STEP = "Step-1"
INSTANCE = "PART-1-1"
NODE_COUNT = 442
ELEMENT_COUNT = 300
POLICY = {"relative_tolerance": 0.05, "relative_rms_tolerance": 0.01,
"near_zero_ratio": 0.01, "zero_scale": "exact-zero"}
CASES = {
"hexa_linearstatic": "linear-static",
"hexa_nonlinearstatic": "nonlinear-static-full-newton",
"hexa_dynlinear": "hht-linear-dynamics",
"hexa_dynnonlinear": "hht-nonlinear-dynamics",
"hexa_freq": "linear-frequency",
}
# Working-tree bytes (Windows CRLF), not Git's normalized LF blobs.
SOURCE_SHA256 = {
"hexa_dynlinear.inp": "f8db81b8ea56b689ab0ad892e6a84b7c6a0162c9d3ff95328ea99722f3de078e",
"hexa_dynlinear_U.csv": "623c20489e8cc18d23fa1728563ce5a26059f321861514ef8569e3d204ef6cb5",
"hexa_dynnonlinear.inp": "1685f86f844e8020bf46e1747b949699ac9aff0d7570bd21ec95109399365ff9",
"hexa_dynnonlinear_U.csv": "f5832a24577f54838bb49dec86a0dc9668cf43e78953f718a9d3635e0f574226",
"hexa_freq.inp": "42cbcec2a8b738f8b6d2c1e13114b8840dccdde6fc3e6f8a43a3272e85d82347",
"hexa_freq.txt": "3d2dacbee13cd2cc8d3f7c1a38210611a72de41d5e31d169e636bf40e00d4370",
"hexa_linearstatic.inp": "67183909c5dc962a4d4b65ffb81f7bd65b72e89a5d015c8369a8e0226347a647",
"hexa_linearstatic_RF.csv": "3e04e71ec075ffef098cf4870d77f9c400557fccb375369da8bf67cf67c1e36b",
"hexa_linearstatic_S.csv": "8840a502b9d28a91e5685349301a56fed37d2ac62327b71304716b714db577e8",
"hexa_linearstatic_U.csv": "1ff49aea3ec601cfd796d6a2fb6a4f91be3fc3de9426cdf3744d981216d4bd5f",
"hexa_nonlinearstatic.inp": "7858254025b96c6bfe3fb6498605ecf9c473425c2bc3593f5fbdc6902fd7c6cb",
"hexa_nonlinearstatic_RF.csv": "3ba67a4fb75b2ce3edee13d35b2ea66ac7d52fc16e612c086ed1122a834fee7b",
"hexa_nonlinearstatic_S.csv": "a8866a730f0d65b1e3921d504d255b72158b4a2b9ee30fc691712d94982b07ba",
"hexa_nonlinearstatic_U.csv": "bd6c2fe2b2bb2ffb0bc2ea52f67513c3967a396b5707bbebc9533fde7be25ac6",
}
BASE_COLUMNS = ["ODB Name", "Step", "Frame", "Part Instance Name"]
TAIL_COLUMNS = ["X", "Y", "Z", "Section Name", "Material Name", "Section Point"]
COMPONENTS = {"U": ["U1", "U2", "U3"], "RF": ["RF1", "RF2", "RF3"],
"S": ["S11", "S22", "S33", "S12", "S13", "S23"]}
STRESS_DERIVED = ["Mises", "Max. Principal", "Max. Principal (Abs)",
"Mid. Principal", "Min. Principal", "Tresca", "Pressure", "Third Invariant"]
NATURAL_NODES = np.array([[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],
[-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1]])
NATURAL_POINTS = np.array([[-1, -1, -1], [1, -1, -1], [-1, 1, -1], [1, 1, -1],
[-1, -1, 1], [1, -1, 1], [-1, 1, 1], [1, 1, 1]]) / math.sqrt(3.0)
FRAME_PATTERN = re.compile(
r"Increment\s+(\d+):\s*Step Time\s*=\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)"
)
class ComparisonError(ValueError):
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def require(condition: bool, code: str, message: str):
if not condition:
raise ComparisonError(code, message)
def canonical(value: str) -> str:
"""Abaqus ASCII case folding only; distinct instances never coalesce."""
return value.translate(str.maketrans("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
def label(value: str) -> int:
require(re.fullmatch(r"[0-9]+", value) is not None, "invalid-source-label", value)
number = int(value)
require(0 < number <= 2**63 - 1, "invalid-source-label", value)
return number
def lexical_absolute(path: Path) -> str:
"""Match C++ absolute.lexically_normal without resolving physical aliases."""
return Path(os.path.abspath(path)).as_posix()
def sha256(path: Path) -> str:
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def fnv1a64(raw: bytes) -> str:
value = 14695981039346656037
for byte in raw:
value = ((value ^ byte) * 1099511628211) & (2**64 - 1)
return f"fnv1a64:{value:016x}"
def input_inventory(path: Path, case: str) -> dict[str, Any]:
"""Read only the frozen single-part identity-instance original subset."""
require(case in CASES, "unknown-source-case", case)
require(sha256(path) == SOURCE_SHA256[f"{case}.inp"], "source-input-hash-mismatch", str(path))
nodes, elements, sets, boundaries = {}, {}, {}, []
keyword, options, in_assembly = "", {}, False
model = {"case": case, "nodes": nodes, "elements": elements, "node_sets": sets,
"section_line": None, "source_path": lexical_absolute(path),
"content_identity": fnv1a64(path.read_bytes())}
for line_number, raw in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1):
line = raw.strip()
if not line or line.startswith("**"):
continue
if line.startswith("*"):
tokens = [token.strip() for token in line[1:].split(",")]
keyword = canonical(tokens[0])
options = {canonical(token.split("=", 1)[0]): token.split("=", 1)[1].strip()
for token in tokens[1:] if "=" in token}
if keyword == "ASSEMBLY":
in_assembly = True
elif keyword == "END ASSEMBLY":
in_assembly = False
elif keyword == "INSTANCE":
require(canonical(options["NAME"]) == INSTANCE and len(tokens) == 3,
"unsupported-source-instance", line)
elif keyword == "SOLID SECTION":
model["section_line"] = line_number
model["section_elset"] = options["ELSET"]
model["material"] = options["MATERIAL"]
elif keyword == "STEP":
require(options["NAME"] == STEP, "wrong-source-step", line)
continue
tokens = [token.strip() for token in line.split(",") if token.strip()]
if keyword == "NODE":
node = label(tokens[0])
require(node not in nodes and len(tokens) == 4, "source-node-inventory", line)
nodes[node] = tuple(float(value) for value in tokens[1:])
elif keyword == "ELEMENT":
element = label(tokens[0])
require(element not in elements and len(tokens) == 9 and options["TYPE"] == "C3D8",
"source-element-inventory", line)
elements[element] = tuple(label(value) for value in tokens[1:])
elif keyword == "NSET" and in_assembly:
name = canonical(options["NSET"])
require(canonical(options["INSTANCE"]) == INSTANCE, "wrong-source-instance", line)
sets.setdefault(name, []).extend(label(value) for value in tokens)
elif keyword == "BOUNDARY":
first, last = int(tokens[1]), int(tokens[2])
require(len(tokens) == 3 or float(tokens[3]) == 0.0, "unsupported-source-boundary", line)
boundaries.append((canonical(tokens[0]), first, last))
elif keyword == "DENSITY":
model["density"] = float(tokens[0])
elif keyword == "ELASTIC":
model["elastic"] = tuple(float(value) for value in tokens)
require(set(nodes) == set(range(1, NODE_COUNT + 1))
and set(elements) == set(range(1, ELEMENT_COUNT + 1)),
"source-model-inventory", "Expected original 442 nodes and 300 elements.")
constrained = set()
for name, first, last in boundaries:
require(name in sets and 1 <= first <= last <= 6, "source-boundary-identity", name)
constrained.update((node, component) for node in sets[name]
for component in range(first, min(last, 3) + 1))
model["constrained"] = constrained
return model
def point_coordinates(model, element: int, point: int):
xi = NATURAL_POINTS[point - 1]
shape = np.prod(1.0 + NATURAL_NODES * xi, axis=1) / 8.0
coordinates = np.array([model["nodes"][node] for node in model["elements"][element]])
terms = shape[:, None] * coordinates
return np.sum(terms, axis=0), np.sum(np.abs(terms), axis=0)
def read_reference(path: Path, case: str, quantity: str, model):
"""Validate all row identities before converting any physical values."""
require(quantity in COMPONENTS, "unknown-quantity", quantity)
dynamic = case.startswith("hexa_dyn")
require(not dynamic or quantity == "U", "wrong-reference-quantity", quantity)
extra = (["Element Label", "IntPt"] if quantity == "S" else ["Node Label"])
suffix = ([f"S-{name}" for name in STRESS_DERIVED] if quantity == "S"
else [f"{quantity}-Magnitude"])
headers = BASE_COLUMNS + extra + TAIL_COLUMNS + suffix + [f"{quantity}-{c}" for c in COMPONENTS[quantity]]
records = {}
with path.open(newline="", encoding="utf-8-sig") as stream:
reader = csv.reader(stream)
fields = [field.strip() for field in next(reader, [])]
require(len(fields) == len(set(fields)), "duplicate-reference-header", str(path))
require(set(fields) == set(headers), "wrong-reference-columns", str(fields))
for row_number, values in enumerate(reader, 2):
require(len(values) == len(fields), "wrong-reference-row-width", str(row_number))
row = dict(zip(fields, [value.strip() for value in values]))
require(row["ODB Name"].replace("\\", "/").rsplit("/", 1)[-1] == f"{case}.odb",
"wrong-reference-case", str(row_number))
require(row["Step"] == STEP, "wrong-reference-step", str(row_number))
require(canonical(row["Part Instance Name"]) == INSTANCE,
"wrong-reference-instance", str(row_number))
match = FRAME_PATTERN.fullmatch(row["Frame"])
require(match is not None, "invalid-reference-frame", str(row_number))
increment = int(match[1])
time = Decimal(match[2])
require(time.is_finite() and increment in (range(101) if dynamic else (10,))
and time == (Decimal(increment) / 10 if dynamic else Decimal(1)),
"wrong-reference-frame-time", str(row_number))
entity = label(row["Element Label" if quantity == "S" else "Node Label"])
point = label(row["IntPt"]) if quantity == "S" else 0
require(entity in model["elements" if quantity == "S" else "nodes"],
"extra-reference-entity", str(row_number))
require(quantity != "S" or 1 <= point <= 8, "wrong-reference-point", str(row_number))
require(row["Section Point"] == ""
and canonical(row["Material Name"]) == ("MATERIAL-1" if quantity == "S" else "NA")
and canonical(row["Section Name"]) == ("ASSEMBLY_PART-1-1_SET-1" if quantity == "S" else "NA"),
"wrong-reference-section", str(row_number))
key = (increment, entity, point)
require(key not in records, "duplicate-reference-identity", str(key))
records[key] = row
entities = model["elements" if quantity == "S" else "nodes"]
expected = {(n, entity, p) for n in (range(101) if dynamic else (10,))
for entity in entities for p in (range(1, 9) if quantity == "S" else (0,))}
require(records.keys() == expected, "reference-inventory", "Missing reference identities.")
return records
def reference_values(records, quantity, model):
result = {}
for key in sorted(records):
row = records[key]
values = [float(row[f"{quantity}-{component}"]) for component in COMPONENTS[quantity]]
coordinates = np.array([float(row[axis]) for axis in ("X", "Y", "Z")])
# Every exported numeric column is checked, including unused derived values.
derived = STRESS_DERIVED if quantity == "S" else ["Magnitude"]
other = [float(row[f"{quantity}-{name}"]) for name in derived]
require(all(math.isfinite(value) for value in values + other) and np.isfinite(coordinates).all(),
"nonfinite-reference-value", str(key))
if quantity == "S":
expected, scale = point_coordinates(model, key[1], key[2])
else:
expected = np.array(model["nodes"][key[1]])
scale = np.abs(expected)
budget = 16.0 * np.finfo(np.float32).eps * scale
require((np.abs(coordinates - expected) <= budget).all(),
"reference-coordinate-convention", str(key))
result[key] = values
return result
def evaluate_family(identity, result, reference):
require(len(result) == len(reference) == len(identity["rows"]) and len(result) > 0,
"scalar-inventory", identity["quantity"])
require(all(math.isfinite(value) for value in (*result, *reference)),
"nonfinite-comparison-value", "Comparison values must be finite.")
scale = max(abs(value) for value in reference)
band = POLICY["near_zero_ratio"] * scale
rows, error_norm = [], 0.0
for key, actual, expected in zip(identity["rows"], result, reference):
error = abs(actual - expected)
require(math.isfinite(error), "nonfinite-comparison-value", str(key))
error_norm = math.hypot(error_norm, error)
require(math.isfinite(error_norm), "nonfinite-comparison-value", str(key))
relative = None
if scale == 0.0:
branch, threshold, passed = "zero-scale-exact", 0.0, error == 0.0
elif abs(expected) <= band:
branch, threshold, passed = "near-zero", band, error <= band
else:
branch, threshold = "relative", POLICY["relative_tolerance"] * abs(expected)
relative = error / abs(expected)
require(math.isfinite(relative), "nonfinite-comparison-value", str(key))
passed = relative <= POLICY["relative_tolerance"]
rows.append({**key, "fesa_value": actual, "reference_value": expected,
"absolute_error": error, "relative_error": relative, "branch": branch,
"threshold": threshold, "passed": passed})
rows_passed = all(row["passed"] for row in rows)
absolute_rms = error_norm / math.sqrt(len(rows))
relative_rms = absolute_rms / scale if scale else None
require(relative_rms is None or math.isfinite(relative_rms), "nonfinite-comparison-value", "RMS")
rms_passed = rows_passed if not scale else relative_rms <= POLICY["relative_rms_tolerance"]
return {**{k: v for k, v in identity.items() if k != "rows"}, "impact": "blocking",
"row_count": len(rows), "reference_scale": scale, "near_zero_band": band,
"maximum_absolute_error": max(row["absolute_error"] for row in rows),
"absolute_rms": absolute_rms, "relative_rms": relative_rms,
"policy": dict(POLICY), "rows": rows, "rows_passed": rows_passed,
"rms_passed": rms_passed, "passed": rows_passed and rms_passed,
"diagnostic_code": "zero-reference-scale-nonzero-error" if not scale and not rows_passed else None}
def read_modal_reference(path: Path):
"""Read the supplied hexa_freq.txt Value=lambda, Freq=cycles/time format.
This artifact has no step field: its fixed case is bound to the sole Step-1
of the hash-checked hexa_freq.inp. Freq is finite auxiliary data; comparison
uses the printed Value directly, without frequency conversion or rounding.
"""
mapping = {}
for line in path.read_text(encoding="utf-8-sig").splitlines():
if not line.strip():
continue
match = re.fullmatch(
r"\s*Mode\s+([0-9]+):\s+Value\s*=\s*(\S+)\s+Freq\s*=\s*(\S+)\s+\(cycles/time\)\s*", line)
require(match is not None, "wrong-modal-reference-format", line)
mode = int(match[1])
require(mode in range(1, 11), "wrong-modal-mode", str(mode))
require(mode not in mapping, "duplicate-modal-mode", str(mode))
mapping[mode] = (match[2], match[3])
require(set(mapping) == set(range(1, 11)), "modal-mode-inventory", "Lowest ten modes required.")
rows = []
for mode in range(1, 11):
value, frequency = (float(token) for token in mapping[mode])
require(math.isfinite(value) and math.isfinite(frequency), "nonfinite-modal-reference", str(mode))
require(value >= 0 and frequency >= 0, "invalid-modal-reference-value", str(mode))
rows.append({"step": STEP, "mode": mode, "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": value})
require(all(a["value"] <= b["value"] for a, b in zip(rows, rows[1:])),
"wrong-modal-order", str(path))
return rows
def compare_modal(reference, result, quantity: str, unit: str):
"""Compare explicitly typed modal rows using the common tolerance.
Each row explicitly supplies step, mode, quantity, unit_dimensions and value.
Callers must obtain those semantics from a genuine supplied artifact first.
Eigenvalues and frequencies are never silently converted into one another.
"""
units = {"eigenvalue": "1/time^2", "frequency": "1/time"}
require(quantity in units and unit == units[quantity], "wrong-modal-unit", unit)
inventories = []
for rows in (reference, result):
mapping = {}
for row in rows:
require(row["quantity"] == quantity, "wrong-modal-quantity", str(row["mode"]))
require(row["unit_dimensions"] == unit, "wrong-modal-unit", str(row["mode"]))
require(row["step"] == STEP, "wrong-modal-step", str(row["mode"]))
mode = row["mode"]
require(type(mode) is int and mode in range(1, 11), "wrong-modal-mode", str(mode))
require(mode not in mapping, "duplicate-modal-mode", str(mode))
mapping[mode] = row["value"]
require(set(mapping) == set(range(1, 11)), "modal-mode-inventory", "Lowest ten modes required.")
inventories.append(mapping)
families = [evaluate_family(
{"identity": f"hexa_freq:{STEP}:mode-{mode}:{quantity}",
"case": "hexa_freq", "step": STEP, "mode": mode, "quantity": quantity,
"unit_dimensions": unit, "rows": [{"mode": mode, "component": quantity}]},
[inventories[1][mode]], [inventories[0][mode]]) for mode in range(1, 11)]
return {"families": families, "passed": all(family["passed"] for family in families)}
def text(value) -> str:
return value.decode("utf-8", errors="strict") if isinstance(value, bytes) else str(value)
def string_attribute(obj, name, expected=None):
require(name in obj.attrs, "missing-result-attribute", f"{obj.name}:{name}")
attr = obj.attrs.get_id(name)
string = h5py.check_string_dtype(attr.dtype)
require(attr.shape == () and string is not None and string.length is None
and string.encoding == "utf-8", "wrong-result-attribute-type", f"{obj.name}:{name}")
value = text(obj.attrs[name])
require(bool(value) and (expected is None or value == expected),
"wrong-result-attribute", f"{obj.name}:{name}={value}")
return value
def numeric_attribute(obj, name, dtype, expected):
require(name in obj.attrs, "missing-result-attribute", f"{obj.name}:{name}")
attr = obj.attrs.get_id(name)
require(attr.shape == () and attr.dtype == np.dtype(dtype),
"wrong-result-attribute-type", f"{obj.name}:{name}")
require(obj.attrs[name] == expected, "wrong-result-attribute", f"{obj.name}:{name}")
def dataset(file, path, shape):
require(path in file and isinstance(file[path], h5py.Dataset), "missing-result-dataset", path)
value = file[path]
require(value.shape == shape and value.chunks is None and value.compression is None,
"wrong-result-shape", path)
return value
def table(file, path, count, fields):
value = dataset(file, path, (count,))
require(value.dtype.names == tuple(name for name, _ in fields), "wrong-result-table-schema", path)
for name, dtype in fields:
actual = value.dtype[name]
if dtype == "string":
info = h5py.check_string_dtype(actual)
valid = info is not None and info.length is None and info.encoding == "utf-8"
else:
valid = actual == np.dtype(dtype)
require(valid, "wrong-result-table-schema", f"{path}:{name}")
return value[:]
def read_results(path: Path, model):
"""Read complete official schema/identities; defer physical finite checks."""
case = model["case"]
dynamic, nonlinear = case.startswith("hexa_dyn"), "nonlinear" in case
frequency = case == "hexa_freq"
arrays, frames, modal = [], {}, {}
with h5py.File(path, "r") as file:
require("metadata" in file and isinstance(file["metadata"], h5py.Group),
"missing-result-metadata", str(path))
metadata = file["metadata"]
numeric_attribute(metadata, "schema_version", "<u8", 1)
for name, expected in {
"feature_id": "hex8-structural-analysis", "analysis_procedure": CASES[case],
"element_formulation": "FESA-Hex8", "integration_rule": "gauss-2x2x2",
"mass_formulation": "lumped-reference", "source_step_name": STEP,
"unit_system_label": "user-consistent-unspecified",
"coordinate_convention": "global-cartesian; reference-strain; current-cauchy-stress",
"component_order": "node-major; UX,UY,UZ",
"source_input_identity": f"path={model['source_path']};content_identity={model['content_identity']}",
}.items():
string_attribute(metadata, name, expected)
string_attribute(metadata, "solver_version")
nodes = table(file, "model/nodes", NODE_COUNT, [
("internal_node_id", "<u8"), ("instance_name", "string"),
("source_label", "string"), ("coordinates", ("<f8", (3,)))])
node_map = {}
for i, row in enumerate(nodes):
node = label(text(row["source_label"]))
require(int(row["internal_node_id"]) == i and canonical(text(row["instance_name"])) == INSTANCE,
"wrong-result-node-identity", str(i))
require(node not in node_map and node in model["nodes"], "result-node-inventory", str(node))
node_map[node] = i
require(node_map.keys() == model["nodes"].keys(), "result-node-inventory", str(path))
elements = table(file, "model/elements", ELEMENT_COUNT, [
("internal_element_id", "<u8"), ("instance_name", "string"), ("source_label", "string"),
("source_element_type", "string"), ("internal_formulation", "string"),
("node_internal_ids", ("<u8", (8,))), ("solid_section_internal_id", "<u8"),
("material_internal_id", "<u8")])
element_map = {}
for i, row in enumerate(elements):
element = label(text(row["source_label"]))
require(int(row["internal_element_id"]) == i and canonical(text(row["instance_name"])) == INSTANCE,
"wrong-result-element-identity", str(i))
require(element not in element_map and element in model["elements"],
"result-element-inventory", str(element))
require(text(row["source_element_type"]) == "C3D8"
and text(row["internal_formulation"]) == "FESA-Hex8"
and tuple(row["node_internal_ids"]) == tuple(node_map[n] for n in model["elements"][element])
and row["solid_section_internal_id"] == 0 and row["material_internal_id"] == 0,
"wrong-result-element-connectivity", str(element))
element_map[element] = i
materials = table(file, "model/solid/materials", 1, [
("internal_material_id", "<u8"), ("name", "string"), ("youngs_modulus", "<f8"),
("poisson_ratio", "<f8"), ("density_present", "u1"), ("density", "<f8")])
material = materials[0]
require(material["internal_material_id"] == 0 and canonical(text(material["name"])) == "MATERIAL-1"
and material["density_present"] == 1, "wrong-result-material-identity", str(path))
sections = table(file, "model/solid/sections", 1, [
("internal_section_id", "<u8"), ("source_file", "string"), ("source_line", "<u8"),
("source_elset", "string"), ("material_internal_id", "<u8")])
section = sections[0]
require(section["internal_section_id"] == 0 and section["material_internal_id"] == 0
and text(section["source_file"]) == model["source_path"]
and section["source_line"] == model["section_line"]
and canonical(text(section["source_elset"])) == canonical(model["section_elset"]),
"wrong-result-section-identity", str(path))
points = table(file, "model/solid_integration_points", ELEMENT_COUNT * 8, [
("internal_element_id", "<u8"), ("point_id", "u1"),
("natural_coordinates", ("<f8", (3,))), ("reference_coordinates", ("<f8", (3,)))])
for i, row in enumerate(points):
require(row["internal_element_id"] == i // 8 and row["point_id"] == i % 8 + 1,
"wrong-result-point-identity", str(i))
mask = dataset(file, "model/nodal_constraint_mask", (NODE_COUNT, 3))
require(mask.dtype == np.dtype("u1"), "wrong-result-mask-type", str(path))
string_attribute(mask, "component_names", "UX,UY,UZ")
mask = mask[:]
expected_mask = np.array([[(node, c) in model["constrained"] for c in (1, 2, 3)]
for node in node_map], dtype="u1")
require(np.array_equal(mask, expected_mask), "wrong-result-constraint-identity", str(path))
require("steps" in file and set(file["steps"]) == {STEP}, "wrong-result-step", str(path))
step = file[f"steps/{STEP}"]
expected_step_groups = {"eigen"} if frequency else ({"frames", "accepted_frames"} if dynamic or nonlinear else {"frames"})
require(set(step) == expected_step_groups, "wrong-result-step-inventory", str(path))
if frequency:
modal = read_modal_tables(file, arrays, int((mask == 0).sum()))
else:
count = 101 if dynamic else (10 if nonlinear else 1)
accepted = None
if dynamic or nonlinear:
coordinate = "time" if dynamic else "load_factor"
flag = "is_initial" if dynamic else "is_factor_one"
accepted = table(file, f"steps/{STEP}/accepted_frames", count, [
("frame_index", "<u8"), ("increment_index", "<u8"), (coordinate, "<f8"),
("iteration_count", "<u8"), (flag, "u1")])
for frame, row in enumerate(accepted):
increment = frame if dynamic else frame + 1
value = increment * 0.1
expected_flag = int(frame == (0 if dynamic else count - 1))
require(row["frame_index"] == frame and row["increment_index"] == increment
and row[coordinate] == value and row[flag] == expected_flag
and (row["iteration_count"] == 0 if dynamic and frame == 0
else 1 <= row["iteration_count"] <= 100),
"wrong-result-frame-schedule", str(frame))
frame_root = f"steps/{STEP}/frames"
require(set(file[frame_root]) == {str(i) for i in range(count)},
"result-frame-inventory", str(path))
for frame in range(count):
increment = frame if dynamic else (frame + 1 if nonlinear else 10)
value = increment * 0.1 if dynamic or nonlinear else 1.0
frames[increment] = read_frame(file, frame, increment, value, dynamic, nonlinear, arrays)
# Identity and schema checks above deliberately finish before physical checks.
arrays.extend([("model/nodes/coordinates", nodes["coordinates"]),
("model/points/natural", points["natural_coordinates"]),
("model/points/reference", points["reference_coordinates"]),
("model/material", np.array([material[n] for n in ("youngs_modulus", "poisson_ratio", "density")]))])
evidence = {"source_input_identity": string_attribute(metadata, "source_input_identity"),
"source_node_count": NODE_COUNT, "source_element_count": ELEMENT_COUNT,
"accepted_frame_count": len(frames), "constrained_scalar_count": int(mask.sum()),
"result_node_map": node_map, "result_element_map": element_map}
return {"frames": frames, "modal": modal, "nodes": nodes, "points": points,
"material": material, "arrays": arrays, "mask": mask, "evidence": evidence}
def physical_dataset(file, path, shape, names, units, location, frame=None,
increment=None, coordinate=None, dynamic=False, extra=None):
value = dataset(file, path, shape)
require(value.dtype == np.dtype("<f8"), "wrong-result-value-type", path)
attributes = {"component_names": names, "unit_dimensions": units,
"coordinate_system": "global-cartesian", "location": location}
if frame is not None:
attributes["step_name"] = STEP
attributes.update(extra or {})
for name, expected in attributes.items():
string_attribute(value, name, expected)
if frame is not None:
numeric_attribute(value, "frame_index", "<u8", frame)
numeric_attribute(value, "increment_index", "<u8", increment)
numeric_attribute(value, "time" if dynamic else "load_factor", "<f8", coordinate)
return value[()]
def read_frame(file, frame, increment, coordinate, dynamic, nonlinear, arrays):
root = f"steps/{STEP}/frames/{frame}"
require(set(file[root]) == {"nodal", "solid", "global"}, "wrong-result-frame-inventory", root)
definitions = {
"nodal/displacement": ((NODE_COUNT, 3), "U1,U2,U3", "length", "nodal", {}),
"nodal/reaction": ((NODE_COUNT, 3), "RF1,RF2,RF3", "force", "nodal", {}),
"nodal/residual": ((NODE_COUNT, 3), "R1,R2,R3", "force", "nodal", {}),
"solid/stress": ((ELEMENT_COUNT, 8, 6), "S11,S22,S33,S12,S13,S23", "force/length^2",
"integration-point", {"stress_measure": "cauchy"}),
"solid/strain": ((ELEMENT_COUNT, 8, 6), "E11,E22,E33,E12,E13,E23", "dimensionless",
"integration-point", {"strain_measure": "green-lagrange" if nonlinear else "infinitesimal",
"shear_convention": "tensor"}),
"global/strain_energy": ((), "scalar", "force*length", "global",
{"definition": "sum(W*detJ0)" if nonlinear else "0.5*u^T*K0*u"}),
}
if dynamic:
definitions.update({
"nodal/velocity": ((NODE_COUNT, 3), "V1,V2,V3", "length/time", "nodal", {}),
"nodal/acceleration": ((NODE_COUNT, 3), "A1,A2,A3", "length/time^2", "nodal", {"acceleration_kind": "physical"}),
"nodal/algorithmic_acceleration": ((NODE_COUNT, 3), "A1,A2,A3", "length/time^2", "nodal", {"acceleration_kind": "algorithmic"}),
"global/kinetic_energy": ((), "scalar", "force*length", "global", {"definition": "0.5*v^T*M*v"}),
"global/external_work": ((), "scalar", "force*length", "global", {"definition": "sum(0.5*(F_old+F_new)*delta_u)"}),
"global/energy_balance_error": ((), "scalar", "force*length", "global", {"definition": "T+U-T0-U0-Wext"}),
"global/linear_momentum": ((3,), "P1,P2,P3", "mass*length/time", "global", {}),
"global/angular_momentum": ((3,), "L1,L2,L3", "mass*length^2/time", "global", {"origin": "global0"}),
})
for group in ("nodal", "solid", "global"):
expected = {key.split("/", 1)[1] for key in definitions if key.startswith(group + "/")}
require(set(file[f"{root}/{group}"]) == expected, "wrong-result-quantity-inventory", root + "/" + group)
values = {}
for quantity, (shape, names, units, location, extra) in definitions.items():
path = root + "/" + quantity
values[quantity] = physical_dataset(file, path, shape, names, units, location,
frame, increment, coordinate, dynamic, extra)
arrays.append((path, values[quantity]))
return values
def read_modal_tables(file, arrays, free_count):
root = f"steps/{STEP}/eigen"
require(set(file[root]) == {"modes", "frequencies", "vectors", "verification"},
"wrong-modal-result-inventory", root)
modes = table(file, root + "/modes", 10, [("mode_id", "<u8"), ("eigenvalue", "<f8"),
("classification", "u1"), ("cluster_id", "<u8")])
for name, component, units in (("modes", "eigenvalue", "1/time^2"),
("frequencies", "frequency", "1/time")):
obj = file[root + "/" + name]
for attr, expected in {"component_names": component, "unit_dimensions": units,
"coordinate_system": "global-cartesian", "location": "global"}.items():
string_attribute(obj, attr, expected)
frequencies = table(file, root + "/frequencies", sum(row["eigenvalue"] >= 0 for row in modes),
[("mode_id", "<u8"), ("frequency", "<f8")])
require(list(modes["mode_id"]) == list(range(1, 11)) and (modes["classification"] <= 1).all()
and modes["cluster_id"][0] == 1
and all(b in (a, a + 1) for a, b in zip(modes["cluster_id"], modes["cluster_id"][1:])),
"wrong-modal-mode", root)
require(list(frequencies["mode_id"]) == [row["mode_id"] for row in modes if row["eigenvalue"] >= 0],
"wrong-modal-frequency-inventory", root)
vectors = physical_dataset(file, root + "/vectors", (10, NODE_COUNT, 3),
"UX,UY,UZ", "mass-normalized", "nodal",
extra={"normalization": "phi^T*M*phi=1"})
verification = root + "/verification"
require(set(file[verification]) == {"pairs", "eta_gram", "max_eta_orth"},
"wrong-modal-verification-inventory", verification)
pair_object = file[verification + "/pairs"]
require(isinstance(pair_object, h5py.Dataset) and len(pair_object.shape) == 1,
"wrong-modal-pair-shape", verification + "/pairs")
pair_count = pair_object.shape[0]
require(10 <= pair_count <= free_count, "wrong-modal-pair-count", str(pair_count))
pairs = table(file, verification + "/pairs", pair_count, [
("sorted_rank", "<u8"), ("solver_column", "<u8"), ("eigenvalue", "<f8"),
("modal_mass", "<f8"), ("rayleigh_quotient", "<f8"), ("eta_r", "<f8"),
("eta_norm", "<f8"), ("eta_energy", "<f8")])
require(list(pairs["sorted_rank"]) == list(range(1, pair_count + 1))
and len(set(pairs["solver_column"])) == pair_count
and (pairs["solver_column"] < pair_count).all(), "wrong-modal-pair-identity", verification)
for name in ("eta_gram", "max_eta_orth"):
value = dataset(file, verification + "/" + name, ())
require(value.dtype == np.dtype("<f8"), "wrong-result-value-type", name)
arrays.append((verification + "/" + name, value[()]))
arrays.extend([(root + "/vectors", vectors), (root + "/lambda", modes["eigenvalue"]),
(root + "/frequency", frequencies["frequency"])])
arrays.extend([(verification + "/" + name, pairs[name]) for name in pairs.dtype.names[2:]])
return {"modes": modes, "frequencies": frequencies, "pairs": pairs}
def check_result_values(result, model):
for path, values in result["arrays"]:
require(np.isfinite(values).all(), "nonfinite-result-value", path)
for row in result["nodes"]:
node = label(text(row["source_label"]))
require(tuple(row["coordinates"]) == model["nodes"][node], "wrong-result-coordinate", str(node))
material = result["material"]
require(tuple(material[name] for name in ("youngs_modulus", "poisson_ratio")) == model["elastic"]
and material["density"] == model["density"], "wrong-result-material-value", model["case"])
for i, row in enumerate(result["points"]):
element = list(result["evidence"]["result_element_map"])[i // 8]
expected, scale = point_coordinates(model, element, i % 8 + 1)
require(np.array_equal(row["natural_coordinates"], NATURAL_POINTS[i % 8])
and (np.abs(row["reference_coordinates"] - expected) <= 16 * np.finfo(float).eps * scale).all(),
"result-coordinate-convention", str(i))
for increment, values in result["frames"].items():
require((values["nodal/reaction"][result["mask"] == 0] == 0.0).all(),
"nonzero-free-result-reaction", str(increment))
if model["case"].startswith("hexa_dyn") and increment == 0:
require((values["nodal/displacement"] == 0.0).all() and (values["nodal/velocity"] == 0.0).all(),
"nonzero-initial-result-state", str(increment))
if result["modal"]:
modes, frequencies, pairs = (result["modal"][key] for key in ("modes", "frequencies", "pairs"))
require((np.diff(modes["eigenvalue"]) >= 0).all() and (np.diff(pairs["eigenvalue"]) >= 0).all(),
"wrong-modal-order", model["case"])
for row in frequencies:
expected = math.sqrt(modes[row["mode_id"] - 1]["eigenvalue"]) / (2 * math.pi)
require(row["frequency"] == expected, "wrong-modal-frequency", str(row["mode_id"]))
def compare(case: str, input_path: Path, results_path: Path, reference_directory: Path = REFERENCE_DIRECTORY,
execution_record: Path | None = None):
report = {"schema": "hex8-reference-comparison-v1", "case": case, "policy": dict(POLICY),
"identities": {}, "precheck": {"passed": False}, "families": [],
"blocking_failures": [], "blocking_passed": False, "verdict": "failed"}
try:
require(case in CASES, "unknown-source-case", case)
quantities = ["U"] if case.startswith("hexa_dyn") else (["U", "RF", "S"] if case != "hexa_freq" else [])
paths = {"input": input_path, "results": results_path}
paths.update({quantity: reference_directory / f"{case}_{quantity}.csv" for quantity in quantities})
modal_path = reference_directory / "hexa_freq.txt"
if case == "hexa_freq" and modal_path.exists():
paths["Value"] = modal_path
for name, path in paths.items():
report["identities"][name] = {"path": lexical_absolute(path), "sha256": sha256(path)}
model = input_inventory(input_path, case)
report["input_copy_matches_approved_bytes"] = True
report["approved_input_sha256"] = SOURCE_SHA256[f"{case}.inp"]
report["execution"] = {"verified": False, "scope": "file comparison only; execution record not supplied"}
if execution_record is not None:
execution = json.loads(execution_record.read_text(encoding="utf-8-sig"))
command = execution.get("command")
require(execution.get("case") == case and execution.get("native_exit") == 0
and type(execution.get("native_exit")) is int
and execution.get("original_sha256") == report["identities"]["input"]["sha256"]
and execution.get("scratch_sha256") == report["identities"]["input"]["sha256"]
and execution.get("result_sha256") == report["identities"]["results"]["sha256"]
and execution.get("source_unchanged") is True
and execution.get("scratch_unchanged") is True
and execution.get("result_exists") is True
and isinstance(command, list) and len(command) == 2
and all(isinstance(item, str) for item in command)
and Path(lexical_absolute(Path(command[1]))) == Path(lexical_absolute(input_path)),
"invalid-execution-evidence", str(execution_record))
report["execution"] = {"verified": True, "native_exit": 0, "command": command,
"record_path": lexical_absolute(execution_record),
"record_sha256": sha256(execution_record)}
records = {}
modal_reference = None
if "Value" in paths:
require(report["identities"]["Value"]["sha256"] == SOURCE_SHA256["hexa_freq.txt"],
"reference-hash-mismatch", str(modal_path))
modal_reference = read_modal_reference(modal_path)
for quantity in quantities:
require(report["identities"][quantity]["sha256"] == SOURCE_SHA256[f"{case}_{quantity}.csv"],
"reference-hash-mismatch", str(paths[quantity]))
records[quantity] = read_reference(paths[quantity], case, quantity, model)
result = read_results(results_path, model)
references = {quantity: reference_values(rows, quantity, model) for quantity, rows in records.items()}
check_result_values(result, model)
report["precheck"] = {"passed": True, **result["evidence"],
"reference_row_counts": {q: len(rows) for q, rows in records.items()},
"coordinate_check": "auxiliary; fixed identity; 16*float32-eps uncancelled source-coordinate budget"}
# Internal maps are useful to validation but unnecessary in the public report.
del report["precheck"]["result_node_map"], report["precheck"]["result_element_map"]
if case == "hexa_freq":
if modal_reference is None:
report["verdict"] = "not-run"
report["reason"] = "Original modal Value reference is absent; no format/quantity/unit assumption made."
return report
actual = [{"step": STEP, "mode": int(row["mode_id"]), "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": float(row["eigenvalue"])}
for row in result["modal"]["modes"]]
modal = compare_modal(modal_reference, actual, "eigenvalue", "1/time^2")
report["precheck"]["reference_row_counts"]["Value"] = len(modal_reference)
report["modal_reference"] = {"format": "Mode n: Value = lambda Freq = f (cycles/time)",
"step_binding": "sole Step-1 of approved hexa_freq.inp",
"quantity": "eigenvalue", "unit_dimensions": "1/time^2",
"frequency_role": "finite auxiliary; no conversion"}
report["families"] = modal["families"]
for family in report["families"]:
family["reference_sha256"] = report["identities"]["Value"]["sha256"]
report["blocking_failures"] = [f["identity"] for f in modal["families"] if not f["passed"]]
report["blocking_passed"] = modal["passed"]
report["verdict"] = "passed" if modal["passed"] else "failed"
return report
families = []
for increment in sorted(result["frames"]):
if not case.startswith("hexa_dyn") and increment != 10:
continue
values = result["frames"][increment]
for quantity in quantities:
actual, expected, identities = [], [], []
for key in sorted(references[quantity]):
if key[0] != increment:
continue
_, entity, point = key
index = result["evidence"]["result_element_map" if quantity == "S" else "result_node_map"][entity]
row = values[{"U": "nodal/displacement", "RF": "nodal/reaction", "S": "solid/stress"}[quantity]][index]
if quantity == "S":
row = row[point - 1]
for c, component in enumerate(COMPONENTS[quantity]):
if quantity == "RF" and (entity, c + 1) not in model["constrained"]:
continue
identities.append({"instance": INSTANCE, "component": component,
"element_label" if quantity == "S" else "node_label": entity,
**({"integration_point": point} if quantity == "S" else {})})
actual.append(float(row[c]))
expected.append(references[quantity][key][c])
identity = {"identity": f"{case}:{STEP}:increment-{increment}:{quantity}-global",
"case": case, "step": STEP, "increment": increment, "quantity": quantity,
"unit_dimensions": {"U": "length", "RF": "force", "S": "force/length^2"}[quantity],
"coordinate_system": "global-cartesian", "rows": identities,
"reference_sha256": report["identities"][quantity]["sha256"]}
if case.startswith("hexa_dyn"):
identity.update({"hdf5_time": increment * 0.1,
"reference_decimal_time": str(Decimal(increment) / 10)})
families.append(evaluate_family(identity, actual, expected))
report["families"] = families
report["blocking_failures"] = [f["identity"] for f in families if not f["passed"]]
report["blocking_passed"] = not report["blocking_failures"]
report["verdict"] = "passed" if report["blocking_passed"] else "failed"
except (ComparisonError, OSError, ValueError, InvalidOperation, UnicodeError, KeyError, TypeError) as error:
report["precheck"] = {"passed": False, "code": getattr(error, "code", "invalid-comparison-input"),
"message": str(error)}
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--case", choices=CASES, required=True)
parser.add_argument("--input", type=Path, required=True, help="Actual byte-identical scratch input read by the solver.")
parser.add_argument("--results", type=Path, required=True)
parser.add_argument("--references", type=Path, default=REFERENCE_DIRECTORY)
parser.add_argument("--execution-record", type=Path, help="Closed native CLI record with input/result hashes; never executed here.")
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
protected = {args.input.resolve(), args.results.resolve()}
if args.execution_record is not None:
protected.add(args.execution_record.resolve())
protected.update(path.resolve() for path in args.references.glob("*"))
protected.update(path.resolve() for path in REFERENCE_DIRECTORY.glob("*"))
report_path = args.report.resolve()
hardlink_alias = args.report.exists() and any(
path.exists() and args.report.samefile(path) for path in protected)
if report_path in protected or hardlink_alias:
parser.error("The report path must not overwrite an input, reference or result.")
report = compare(args.case, args.input, args.results, args.references, args.execution_record)
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, sort_keys=True, indent=2, allow_nan=False) + "\n", encoding="utf-8")
print(json.dumps({"case": args.case, "verdict": report["verdict"], "precheck": report["precheck"],
"blocking_failure_count": len(report["blocking_failures"]), "report": str(args.report)}, allow_nan=False))
return 2 if not report["precheck"]["passed"] or report["verdict"] == "not-run" else (0 if report["blocking_passed"] else 1)
if __name__ == "__main__":
raise SystemExit(main())
+436
View File
@@ -0,0 +1,436 @@
"""Compare the approved 101-frame HHT reference without modifying artifacts.
Run with ``uv run --with h5py python -B scripts/verification/compare_hht_dynamic.py``.
Only the documented displacement/rotation reference quantities are compared;
this is not an independent validation of inertia, energy, or convergence.
"""
from __future__ import annotations
import argparse
import csv
from decimal import Decimal, InvalidOperation
import hashlib
import json
import math
from pathlib import Path
import re
from typing import Any
import h5py
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
CASE = "shellNL-dyn-meshfine"
REFERENCE_DIRECTORY = ROOT / "reference" / CASE
NODE_LABELS = (2, 21, 22, 69, 70, 165, 309, 442, 586)
INSTANCE = "PART-1-1"
STEP = "Step-1"
NODE_COUNT = 729
FRAME_COUNT = 101
INCREMENT = 0.1
COMPONENTS = ("U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3")
CSV_COLUMNS = ("Frame", "Part Instance Name", "Node Label", *COMPONENTS)
FRAME_PATTERN = re.compile(
r"Increment\s+(\d+):\s*Step Time\s*=\s*"
r"([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)"
)
POLICY = {
"policy_id": "common-reference-tolerance",
"near_zero_ratio": 0.01,
"relative_tolerance": 0.05,
"relative_rms_tolerance": 0.01,
}
class ComparisonError(ValueError):
"""An identity/schema/finite failure that must precede tolerance evaluation."""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def require(condition: bool, code: str, message: str) -> None:
if not condition:
raise ComparisonError(code, message)
def positive_label(text: str) -> int:
require(bool(re.fullmatch(r"\d+", text)), "invalid-node-label", repr(text))
label = int(text)
require(0 < label <= 2**63 - 1, "invalid-node-label", repr(text))
return label
def canonical_instance(text: str) -> str:
return text.translate(str.maketrans("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
def input_inventory(path: Path) -> tuple[dict[int, tuple[float, ...]], str]:
"""Read identity and schedule authority from this specific approved INP."""
raw = path.read_bytes()
nodes: dict[int, tuple[float, ...]] = {}
parts: list[str] = []
instances: list[tuple[str, str]] = []
steps: list[dict[str, str]] = []
dynamic_rows: list[tuple[float, float]] = []
keyword = ""
in_part = False
for line_number, line in enumerate(raw.decode("utf-8-sig").splitlines(), 1):
text = line.strip()
if not text or text.startswith("**"):
continue
fields = [field.strip() for field in text.split(",")]
if text.startswith("*"):
keyword = fields[0].upper()
parameters = dict(
(name.upper(), value.strip())
for field in fields[1:] if "=" in field
for name, value in [field.split("=", 1)]
)
if keyword == "*PART":
in_part = True
parts.append(canonical_instance(parameters.get("NAME", "")))
elif keyword == "*END PART":
in_part = False
elif keyword == "*INSTANCE":
instances.append((canonical_instance(parameters.get("NAME", "")),
canonical_instance(parameters.get("PART", ""))))
elif keyword == "*STEP":
steps.append(parameters)
elif keyword == "*DYNAMIC":
require([field.upper() for field in fields] == ["*DYNAMIC", "DIRECT"],
"wrong-input-procedure", f"line {line_number}")
continue
if keyword == "*NODE":
require(in_part and len(fields) == 4, "invalid-input-node", f"line {line_number}")
label = positive_label(fields[0])
require(label not in nodes, "duplicate-input-node", str(label))
coordinates = tuple(float(value) for value in fields[1:])
require(all(math.isfinite(value) for value in coordinates),
"nonfinite-input-node", str(label))
nodes[label] = coordinates
elif keyword == "*DYNAMIC":
if fields[-1] == "":
fields.pop()
require(len(fields) == 2, "wrong-input-schedule", f"line {line_number}")
dynamic_rows.append((float(fields[0]), float(fields[1])))
require(parts == ["PART-1"] and instances == [(INSTANCE, "PART-1")],
"wrong-input-instance", "Expected the original single identity instance.")
require(len(nodes) == NODE_COUNT and set(NODE_LABELS) <= nodes.keys(),
"wrong-input-node-inventory", f"Expected {NODE_COUNT} source nodes.")
require(len(steps) == 1 and steps[0].get("NAME") == STEP
and steps[0].get("NLGEOM", "").upper() == "YES"
and steps[0].get("INC") == "1000", "wrong-input-step", str(steps))
require(dynamic_rows == [(INCREMENT, 10.0)], "wrong-input-schedule", str(dynamic_rows))
value = 14695981039346656037
for byte in raw:
value = ((value ^ byte) * 1099511628211) & (2**64 - 1)
return nodes, f"fnv1a64:{value:016x}"
def read_reference(path: Path) -> dict[tuple[int, int], tuple[float, ...]]:
rows: dict[tuple[int, int], tuple[float, ...]] = {}
with path.open(encoding="utf-8-sig", newline="") as stream:
reader = csv.reader(stream)
header = [field.strip() for field in next(reader, [])]
require(len(header) == len(set(header)), "duplicate-reference-column", str(header))
require(set(CSV_COLUMNS) <= set(header), "missing-reference-column", str(header))
for line_number, fields in enumerate(reader, 2):
if not fields or all(not field.strip() for field in fields):
continue
require(len(fields) == len(header), "invalid-reference-row", f"line {line_number}")
row = dict(zip(header, (field.strip() for field in fields)))
match = FRAME_PATTERN.fullmatch(row["Frame"])
require(match is not None, "invalid-reference-frame", f"line {line_number}")
increment = int(match.group(1))
time = Decimal(match.group(2))
require(time.is_finite() and time == Decimal(increment) / Decimal(10),
"wrong-reference-time", f"line {line_number}: {row['Frame']}")
require(canonical_instance(row["Part Instance Name"]) == INSTANCE,
"wrong-reference-instance", f"line {line_number}")
label = positive_label(row["Node Label"])
key = (increment, label)
require(key not in rows, "duplicate-reference-row", str(key))
values = tuple(float(row[component]) for component in COMPONENTS)
require(all(math.isfinite(value) for value in values),
"nonfinite-reference-value", str(key))
rows[key] = values
expected = {(frame, label) for frame in range(FRAME_COUNT) for label in NODE_LABELS}
require(rows.keys() == expected, "reference-row-inventory",
f"missing={sorted(expected - rows.keys())}; extra={sorted(rows.keys() - expected)}")
return rows
def text_value(value: Any) -> str:
return value.decode("utf-8") if isinstance(value, bytes) else str(value)
def string_attribute(obj: Any, name: str, expected: str | None = None) -> str:
require(name in obj.attrs, "missing-hdf5-attribute", f"{obj.name}@{name}")
attribute = obj.attrs.get_id(name)
string = h5py.check_string_dtype(attribute.dtype)
require(attribute.shape == () and string is not None and string.length is None
and string.encoding == "utf-8", "wrong-hdf5-attribute-type", f"{obj.name}@{name}")
value = text_value(obj.attrs[name])
require(bool(value) and (expected is None or value == expected),
"wrong-hdf5-attribute-value", f"{obj.name}@{name}: {value!r}")
return value
def numeric_attribute(obj: Any, name: str, dtype: str, expected: int | float) -> None:
require(name in obj.attrs, "missing-hdf5-attribute", f"{obj.name}@{name}")
attribute = obj.attrs.get_id(name)
require(attribute.shape == () and attribute.dtype == np.dtype(dtype),
"wrong-hdf5-attribute-type", f"{obj.name}@{name}")
value = obj.attrs[name]
require(math.isfinite(value) and value == expected, "wrong-hdf5-attribute-value",
f"{obj.name}@{name}: {value!r}")
def dataset(file: h5py.File, path: str, shape: tuple[int, ...]) -> h5py.Dataset:
require(path in file and isinstance(file[path], h5py.Dataset), "missing-hdf5-dataset", path)
result = file[path]
require(result.shape == shape and result.chunks is None and result.compression is None,
"wrong-hdf5-dataset-layout", path)
return result
def read_results(path: Path, nodes: dict[int, tuple[float, ...]],
content_identity: str) -> tuple[list[np.ndarray], dict[str, Any]]:
"""Precheck every result node/frame/component, then project by source key."""
projected: list[np.ndarray] = []
with h5py.File(path, "r") as file:
require("metadata" in file and isinstance(file["metadata"], h5py.Group),
"missing-hdf5-metadata", "/metadata")
metadata = file["metadata"]
for name, expected in {
"feature_id": "hht-implicit-dynamics", "analysis_procedure": "hht-implicit-dynamics",
"mass_formulation": "nodal-lumped-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
"initial_state": "zero-q-v-step-load-0+",
"acceleration_convention": "physical-endpoint-and-auxiliary-separated",
"source_step_name": STEP, "step_name": STEP, "unit_system_label": "consistent-input-units",
}.items():
string_attribute(metadata, name, expected)
string_attribute(metadata, "solver_version")
source_identity = string_attribute(metadata, "source_input_identity")
require(source_identity.endswith(";content_identity=" + content_identity),
"wrong-input-content-identity", source_identity)
require("schema_version" in metadata.attrs, "missing-hdf5-attribute", "/metadata@schema_version")
version_attribute = metadata.attrs.get_id("schema_version")
require(version_attribute.shape == () and version_attribute.dtype == np.dtype("<u8"),
"wrong-hdf5-attribute-type", "/metadata@schema_version")
schema_version = int(metadata.attrs["schema_version"])
require(schema_version in (1, 2), "wrong-hdf5-attribute-value", "/metadata@schema_version")
if schema_version == 2:
string_attribute(metadata, "time_integration_formulation",
"reduced-coordinate-HHT-with-physical-rate-reconstruction")
elif "time_integration_formulation" in metadata.attrs:
string_attribute(metadata, "time_integration_formulation", "full-coordinate-HHT")
for name, dtype, value in (
("schema_version", "<u8", schema_version), ("increment_count", "<u8", 100),
("maximum_increments", "<u8", 1000), ("alpha", "<f8", -0.05),
("beta", "<f8", 0.275625), ("gamma", "<f8", 0.55),
("initial_increment", "<f8", INCREMENT), ("time_period", "<f8", 10.0),
):
numeric_attribute(metadata, name, dtype, value)
table = dataset(file, "model/nodes", (NODE_COUNT,))
require(table.dtype.names == ("internal_node_id", "instance_name", "source_label", "coordinates"),
"wrong-node-schema", str(table.dtype))
require(table.dtype["internal_node_id"] == np.dtype("<u8")
and table.dtype["coordinates"] == np.dtype(("<f8", (3,))),
"wrong-node-schema", str(table.dtype))
for name in ("instance_name", "source_label"):
string = h5py.check_string_dtype(table.dtype[name])
require(string is not None and string.length is None and string.encoding == "utf-8",
"wrong-node-schema", name)
node_indices: dict[int, int] = {}
for index, node in enumerate(table[:]):
require(int(node["internal_node_id"]) == index, "wrong-internal-node-identity", str(index))
instance = canonical_instance(text_value(node["instance_name"]))
label = positive_label(text_value(node["source_label"]))
require(instance == INSTANCE, "wrong-result-instance", instance)
require(label not in node_indices, "duplicate-result-node", str(label))
require(label in nodes, "extra-result-node", str(label))
require(np.isfinite(node["coordinates"]).all(), "nonfinite-result-node", str(label))
require(tuple(node["coordinates"]) == nodes[label], "wrong-result-coordinate", str(label))
node_indices[label] = index
require(node_indices.keys() == nodes.keys(), "result-node-inventory", "Source node inventories differ.")
require("steps" in file and isinstance(file["steps"], h5py.Group)
and set(file["steps"]) == {STEP}, "wrong-result-step", "Expected one accepted step.")
accepted = dataset(file, f"steps/{STEP}/accepted_frames", (FRAME_COUNT,))
expected_fields = (("frame_index", "<u8"), ("increment_index", "<u8"),
("time", "<f8"), ("iteration_count", "<u8"), ("is_initial", "u1"))
require(accepted.dtype.names == tuple(name for name, _ in expected_fields)
and all(accepted.dtype[name] == np.dtype(dtype) for name, dtype in expected_fields),
"wrong-frame-table-schema", str(accepted.dtype))
frame_path = f"steps/{STEP}/frames"
require(frame_path in file and isinstance(file[frame_path], h5py.Group)
and set(file[frame_path]) == {str(i) for i in range(FRAME_COUNT)},
"result-frame-inventory", "Missing, extra, or noncanonical frame group.")
selection = [node_indices[label] for label in NODE_LABELS]
for frame, row in enumerate(accepted[:]):
scheduled_time = float(frame) * INCREMENT
require(int(row["frame_index"]) == frame and int(row["increment_index"]) == frame
and math.isfinite(row["time"]) and float(row["time"]) == scheduled_time
and int(row["is_initial"]) == int(frame == 0)
and 0 <= int(row["iteration_count"]) <= 100
and (frame != 0 or (int(row["iteration_count"]) == 0 and not np.signbit(row["time"]))),
"wrong-result-frame-time", str(frame))
displacement = dataset(file, f"{frame_path}/{frame}/nodal/displacement", (NODE_COUNT, 6))
require(displacement.dtype == np.dtype("<f8"), "wrong-displacement-type", str(frame))
for name, value in {
"component_names": "UX,UY,UZ,URX,URY,URZ",
"component_unit_dimensions": "length,length,length,1,1,1",
"coordinate_system": "global-cartesian", "location": "nodal",
"step_name": STEP, "source_step_name": STEP,
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "global-translation-and-total-rotation-vector",
}.items():
string_attribute(displacement, name, value)
for name, dtype, value in (
("schema_version", "<u8", schema_version), ("frame_index", "<u8", frame),
("increment_index", "<u8", frame), ("time", "<f8", scheduled_time),
("is_initial", "u1", int(frame == 0)),
):
numeric_attribute(displacement, name, dtype, value)
if schema_version == 2:
auxiliary = dataset(file, f"{frame_path}/{frame}/nodal/algorithmic_acceleration", (NODE_COUNT, 6))
require(auxiliary.dtype == np.dtype("<f8"), "wrong-auxiliary-type", str(frame))
for name, value in {
"component_names": "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6",
"component_unit_dimensions": "length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2",
"coordinate_system": "fixed-initial-global-chart", "location": "nodal",
"step_name": STEP, "source_step_name": STEP,
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "full-chart-HHT-filter-combination-not-predictor-driver",
}.items():
string_attribute(auxiliary, name, value)
for name, dtype, value in (
("schema_version", "<u8", schema_version), ("frame_index", "<u8", frame),
("increment_index", "<u8", frame), ("time", "<f8", scheduled_time),
("is_initial", "u1", int(frame == 0)),
):
numeric_attribute(auxiliary, name, dtype, value)
require(np.isfinite(auxiliary[:]).all(), "nonfinite-auxiliary-value", str(frame))
values = displacement[:]
require(np.isfinite(values).all(), "nonfinite-result-value", str(frame))
if frame == 0:
require((values == 0.0).all(), "nonzero-initial-displacement", "Frame zero must have q0=0.")
projected.append(values[selection])
return projected, {"source_input_identity": source_identity, "source_node_count": NODE_COUNT,
"accepted_frame_count": FRAME_COUNT, "checked_result_scalars": NODE_COUNT * FRAME_COUNT * 6,
"projected_node_labels": list(NODE_LABELS)}
def evaluate_family(increment: int, quantity: str, result: list[float],
reference: list[float]) -> dict[str, Any]:
"""Apply the common policy exactly, including zero-scale and family RMS."""
require(len(result) == len(reference) and len(result) > 0, "scalar-inventory", quantity)
require(all(math.isfinite(value) for value in (*result, *reference)),
"nonfinite-comparison-value", quantity)
scale = max(abs(value) for value in reference)
band = POLICY["near_zero_ratio"] * scale
rows = []
error_norm = 0.0
for index, (actual, expected) in enumerate(zip(result, reference)):
error = abs(actual - expected)
require(math.isfinite(error), "nonfinite-comparison-value", quantity)
error_norm = math.hypot(error_norm, error)
require(math.isfinite(error_norm), "nonfinite-comparison-value", quantity)
relative_error = None
if scale == 0.0:
branch, threshold, passed = "zero-scale-exact", 0.0, error == 0.0
elif abs(expected) <= band:
branch, threshold, passed = "near-zero", band, error <= band
else:
branch = "relative"
threshold = POLICY["relative_tolerance"] * abs(expected)
relative_error = error / abs(expected)
require(math.isfinite(relative_error), "nonfinite-comparison-value", quantity)
passed = relative_error <= POLICY["relative_tolerance"]
rows.append({"instance": INSTANCE, "node_label": NODE_LABELS[index // 3],
"component": f"{quantity}{index % 3 + 1}", "fesa_value": actual,
"reference_value": expected, "absolute_error": error,
"relative_error": relative_error, "branch": branch,
"threshold": threshold, "passed": passed})
rows_passed = all(row["passed"] for row in rows)
rms = error_norm / math.sqrt(len(rows)) / scale if scale else None
require(rms is None or math.isfinite(rms), "nonfinite-comparison-value", quantity)
rms_passed = rows_passed if scale == 0.0 else rms <= POLICY["relative_rms_tolerance"]
return {"identity": f"{quantity}-global:{STEP}:increment-{increment}", "case": CASE,
"step": STEP, "increment": increment, "hdf5_time": float(increment) * INCREMENT,
"reference_decimal_time": str(Decimal(increment) / Decimal(10)),
"quantity": quantity, "components": [f"{quantity}{i}" for i in (1, 2, 3)],
"unit_dimension": "length" if quantity == "U" else "rad",
"coordinate_system": "global-cartesian" if quantity == "U" else "fixed-initial-global-total-rotation-vector",
"impact": "blocking" if quantity == "U" else "warning-only", "row_count": len(rows),
"reference_scale": scale, "near_zero_band": band, "rows": rows,
"rows_passed": rows_passed, "relative_rms": rms, "rms_passed": rms_passed,
"diagnostic_code": "zero-reference-scale-nonzero-error" if scale == 0.0 and not rows_passed else None,
"passed": rows_passed and rms_passed}
def compare(input_path: Path, reference_path: Path, results_path: Path) -> dict[str, Any]:
report: dict[str, Any] = {
"schema": "hht-reference-comparison-v1", "case": CASE, "policy": dict(POLICY),
"identities": {}, "precheck": {"passed": False}, "families": [],
"blocking_failures": [], "warnings": [], "blocking_passed": False, "verdict": "failed",
}
try:
for name, path in (("input", input_path), ("reference", reference_path), ("results", results_path)):
report["identities"][name] = {"path": str(path.absolute())}
with path.open("rb") as stream:
report["identities"][name]["sha256"] = hashlib.file_digest(stream, "sha256").hexdigest()
nodes, content_identity = input_inventory(input_path)
reference = read_reference(reference_path)
result, evidence = read_results(results_path, nodes, content_identity)
families = []
for frame in range(FRAME_COUNT):
for quantity, first in (("U", 0), ("UR", 3)):
actual = [float(value) for row in result[frame] for value in row[first:first + 3]]
expected = [value for label in NODE_LABELS for value in reference[(frame, label)][first:first + 3]]
families.append(evaluate_family(frame, quantity, actual, expected))
report["precheck"] = {"passed": True, "reference_row_count": len(reference),
"reference_frame_count": FRAME_COUNT, **evidence}
report["families"] = families
for family in families:
if family["passed"]:
continue
if family["impact"] == "blocking":
report["blocking_failures"].append(family["identity"])
else:
if not family["rows_passed"]:
report["warnings"].append({"family": family["identity"], "code": "row-tolerance-failure"})
if not family["rms_passed"]:
report["warnings"].append({"family": family["identity"], "code": "relative-rms-failure"})
report["blocking_passed"] = not report["blocking_failures"]
report["verdict"] = "passed" if report["blocking_passed"] else "failed"
except (ComparisonError, OSError, ValueError, InvalidOperation, UnicodeError) as error:
report["precheck"] = {"passed": False, "code": getattr(error, "code", "invalid-comparison-input"),
"message": str(error)}
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", type=Path, default=REFERENCE_DIRECTORY / f"{CASE}.inp")
parser.add_argument("--reference", type=Path, default=REFERENCE_DIRECTORY / f"{CASE} displacements.csv")
parser.add_argument("--results", type=Path)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
results = args.results or args.input.with_name(args.input.stem + "_results.h5")
if args.report.resolve() in {args.input.resolve(), args.reference.resolve(), results.resolve()}:
parser.error("The report path must not overwrite an input, reference, or solver result.")
report = compare(args.input, args.reference, results)
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2, sort_keys=True, allow_nan=False) + "\n", encoding="utf-8")
print(json.dumps({"verdict": report["verdict"], "precheck": report["precheck"],
"blocking_failure_count": len(report["blocking_failures"]),
"warning_count": len(report["warnings"]), "report": str(args.report)}, allow_nan=False))
return 2 if not report["precheck"]["passed"] else (0 if report["blocking_passed"] else 1)
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -3,7 +3,7 @@
이 영역은 fesa_solver와 fesa_cli의 production 구현을 소유한다. Public interface와 vendor
resource 사이의 구현 책임을 유지하며 테스트 실행기나 문서 workflow를 solver에 연결하지 않는다.
- CMake target에 새 source와 필요한 private dependency를 명시한다. Link 성공뿐 아니라
- 직접 MSVC 프로젝트와 대응 CMake target에 같은 source와 필요한 private dependency를 명시한다. Link 성공뿐 아니라
Windows runtime staging의 영향을 확인한다.
- Public consumer가 implementation header를 include하도록 경로를 공개하지 않는다.
MKL·TBB·HDF5·Win32는 private adapter 뒤에 둔다.
+3
View File
@@ -41,6 +41,7 @@ add_library(
core/ascii.cpp
core/diagnostic.cpp
core/status.cpp
elements/element_capabilities.cpp
elements/element_factory.cpp
elements/hex8_element.cpp
elements/hex8_high_low.cpp
@@ -93,12 +94,14 @@ add_library(
results/analysis_state_commit.cpp
results/beam_result_recovery.cpp
results/global_equilibrium_recovery.cpp
results/prepared_results_writer.cpp
results/recovery_candidate.cpp
results/result_recovery.cpp
results/shell_result_recovery.cpp
solvers/eigen/mkl_feast_eigen_solver.cpp
solvers/linear/mkl_pardiso_solver.cpp
solvers/linear/mkl_pardiso_tangent_solver.cpp
solvers/linear/pardiso_session.cpp
)
target_include_directories(
+6
View File
@@ -14,3 +14,9 @@ Analysis는 준비된 Domain을 procedure별로 실행하고 equation·backend·
- Frequency reduction은 null/finite 공간과 공개 mode 수·내부 검증 수를 구분한다.
- 초기화, nonzero BC, all-constrained, 증분·반복 종료 경계, 실패 보존, temporal convergence,
반복 결정론과 backend failure를 검증한다. 한 reference의 U 일치로 모든 physics를 통과시키지 않는다.
- 공통 AnalysisProcedure 선택과 family별 typed outcome을 유지한다. EquilibriumAlgorithm은
iteration만 조정하고 typed candidate는 procedure-local EquilibriumStep이 소유한다.
Static1/HHT0 시작·inclusive endpoint와 실패 보존을 유지한다.
- ShellHhtIntegrator는 predictor/rate 복원만 담당한다. Chart/root/solver/callback은 borrowed
call view이며 출력 rate buffer는 입력·서로와 alias하지 않는다. Accepted history는 소유하지 않는다.
+170
View File
@@ -0,0 +1,170 @@
#ifndef FESA_ANALYSIS_ANALYSIS_PROCEDURES_H_
#define FESA_ANALYSIS_ANALYSIS_PROCEDURES_H_
#include "fesa/analysis/analysis_procedure.h"
#include "fesa/analysis/hht_dynamic_procedure.h"
#include "fesa/analysis/linear_frequency_procedure.h"
#include "fesa/analysis/linear_static_procedure.h"
#include "fesa/analysis/nonlinear_static_procedure.h"
#include "fesa/analysis/prepared_analysis_outcome.h"
#include "fesa/analysis/solid_frequency_procedure.h"
#include "fesa/analysis/solid_hht_dynamic_procedure.h"
#include "fesa/analysis/solid_static_procedure.h"
#include "fesa/analysis/transient_heat_procedure.h"
namespace fesa {
class EigenSolver;
/// @brief Adapts the LinearStatic lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class LinearStaticProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
LinearStaticProcedure(const ParallelFor& parallel_for,
LinearSolver& linear_solver)
: executor_(parallel_for, linear_solver) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit LinearStaticProcedure(LinearStaticProcedureExecutor executor)
: executor_(executor) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
LinearStaticProcedureExecutor executor_;
};
/// @brief Adapts the NonlinearStatic lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class NonlinearStaticProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
NonlinearStaticProcedure(const ParallelFor& parallel_for,
TangentSolver& tangent_solver,
NewtonIterationLogWriter& log)
: executor_(parallel_for, tangent_solver), log_(log) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit NonlinearStaticProcedure(NonlinearStaticProcedureExecutor executor,
NewtonIterationLogWriter& log)
: executor_(executor), log_(log) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
NonlinearStaticProcedureExecutor executor_;
NewtonIterationLogWriter& log_;
};
/// @brief Adapts the LinearFrequency lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class LinearFrequencyProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
LinearFrequencyProcedure(const EigenSolver& eigen_solver)
: executor_(eigen_solver) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit LinearFrequencyProcedure(LinearFrequencyProcedureExecutor executor)
: executor_(executor) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
LinearFrequencyProcedureExecutor executor_;
};
/// @brief Adapts the HhtDynamic lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class HhtDynamicProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
HhtDynamicProcedure(const ParallelFor& parallel_for,
LinearSolver& positive_solver,
TangentSolver& tangent_solver,
NewtonIterationLogWriter& log)
: executor_(parallel_for, positive_solver, tangent_solver), log_(log) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit HhtDynamicProcedure(HhtDynamicProcedureExecutor executor,
NewtonIterationLogWriter& log)
: executor_(executor), log_(log) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
HhtDynamicProcedureExecutor executor_;
NewtonIterationLogWriter& log_;
};
/// @brief Adapts the SolidStatic lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class SolidStaticProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
SolidStaticProcedure(const ParallelFor& parallel_for,
LinearSolver& linear_solver,
TangentSolver& tangent_solver,
NewtonIterationLogWriter& log)
: executor_(parallel_for, linear_solver, tangent_solver), log_(log) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit SolidStaticProcedure(SolidStaticProcedureExecutor executor,
NewtonIterationLogWriter& log)
: executor_(executor), log_(log) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
SolidStaticProcedureExecutor executor_;
NewtonIterationLogWriter& log_;
};
/// @brief Adapts the SolidFrequency lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class SolidFrequencyProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
SolidFrequencyProcedure(const EigenSolver& eigen_solver)
: executor_(eigen_solver) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit SolidFrequencyProcedure(SolidFrequencyProcedureExecutor executor)
: executor_(executor) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
SolidFrequencyProcedureExecutor executor_;
};
/// @brief Adapts the SolidHhtDynamic lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class SolidHhtDynamicProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
SolidHhtDynamicProcedure(const ParallelFor& parallel_for,
LinearSolver& positive_solver,
TangentSolver& tangent_solver,
NewtonIterationLogWriter& log)
: executor_(parallel_for, positive_solver, tangent_solver), log_(log) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit SolidHhtDynamicProcedure(SolidHhtDynamicProcedureExecutor executor,
NewtonIterationLogWriter& log)
: executor_(executor), log_(log) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
SolidHhtDynamicProcedureExecutor executor_;
NewtonIterationLogWriter& log_;
};
/// @brief Adapts the TransientHeat lifecycle to prepared execution.
/// @note Referenced backend and log adapters must outlive this procedure.
class TransientHeatProcedure final : public AnalysisProcedure {
public:
/// @brief Injects only the adapters required by this family.
TransientHeatProcedure(const ParallelFor& parallel_for,
LinearSolver& linear_solver)
: executor_(parallel_for, linear_solver) {}
/// @brief Retains compatibility with an already configured typed executor.
explicit TransientHeatProcedure(TransientHeatProcedureExecutor executor)
: executor_(executor) {}
Result<PreparedAnalysisOutcome> Execute(const Domain& domain) override;
private:
TransientHeatProcedureExecutor executor_;
};
} // namespace fesa
#endif // FESA_ANALYSIS_ANALYSIS_PROCEDURES_H_
+7 -26
View File
@@ -7,9 +7,15 @@
#include <string>
#include <utility>
#include "fesa/fem/dof_manager.h"
#include "math/finite_values.h"
#include "results/shell_result_recovery.h"
namespace fesa {
namespace {
using math_internal::IsFinite;
constexpr std::size_t kShellLocationsPerElement = 4U;
/// @brief Creates a structured failure without mutating existing state.
@@ -21,31 +27,6 @@ Status ShellCandidateFailure(const std::string& code,
{{Severity::kError, code, {}, "ANALYSIS_STATE", identity, message}});
}
template <std::size_t Size>
/// @brief Tests one fixed-size candidate component array for finite values.
bool IsFinite(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](const double value) { return std::isfinite(value); });
}
/// @brief Tests every physical shell row component for finite values.
bool IsFinite(const ShellResultRow& row) {
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
!IsFinite(row.section_resultant)) {
return false;
}
for (const auto& axis : row.local_frame) {
if (!IsFinite(axis)) {
return false;
}
}
return std::all_of(row.stress.begin(), row.stress.end(),
[](const ShellSectionStressRow& stress) {
return std::isfinite(stress.zeta) &&
IsFinite(stress.components);
});
}
} // namespace
AnalysisState AnalysisState::Create(const DofManager& dofs,
@@ -170,7 +151,7 @@ Status AnalysisState::CommitShellResults(
"Shell stress rows require BOTTOM, MIDDLE, TOP identity.");
}
}
if (!IsFinite(row)) {
if (!results_internal::IsFiniteShellRow(row)) {
return ShellCandidateFailure(
"nonfinite-shell-state-value", std::to_string(row.element),
"Shell result rows must contain only finite values.");
+9 -56
View File
@@ -1,18 +1,24 @@
#include "fesa/analysis/dynamic_analysis_state.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include "fesa/core/source_identity.h"
#include "fesa/elements/element_definition.h"
#include "fesa/elements/finite_rotation.h"
#include "math/finite_values.h"
#include "results/shell_result_recovery.h"
#include "analysis/shell_state_inventory.h"
namespace fesa {
namespace {
using analysis_internal::ShellElementOrder;
using math_internal::IsFinite;
constexpr std::size_t kDofsPerNode = 6U;
constexpr std::size_t kAngularDofsPerNode = 3U;
constexpr std::size_t kShellLocationsPerElement = 4U;
@@ -28,59 +34,6 @@ Status Failure(const FailureCategory category, const std::string& message) {
message}});
}
/// @brief Compares canonical and raw source identity exactly.
bool SameId(const SourceEntityId& left, const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
/// @brief Checks all components of one fixed-size physical record.
template <std::size_t Size>
bool IsFinite(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](double value) { return std::isfinite(value); });
}
/// @brief Checks every vector component before state acceptance.
bool IsFinite(const Vector& values) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return false;
}
}
return true;
}
/// @brief Checks all physical recovery quantities for finite values.
bool IsFinite(const ShellResultRow& row) {
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
!IsFinite(row.section_resultant)) {
return false;
}
for (const auto& axis : row.local_frame) {
if (!IsFinite(axis)) {
return false;
}
}
return std::all_of(row.stress.begin(), row.stress.end(),
[](const ShellSectionStressRow& stress) {
return std::isfinite(stress.zeta) &&
IsFinite(stress.components);
});
}
/// @brief Collects stable Domain indices for recovery identity.
std::vector<EntityIndex> ShellElementOrder(const Domain& domain) {
std::vector<EntityIndex> order;
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) {
order.push_back(static_cast<EntityIndex>(index));
}
}
return order;
}
/// @brief Requires complete ordered Gauss and section-position rows.
Status ValidateShellRows(const DynamicAcceptedFrameState& frame,
const std::vector<EntityIndex>& shell_order) {
@@ -110,7 +63,7 @@ Status ValidateShellRows(const DynamicAcceptedFrameState& frame,
frame.shell_results[element * kShellLocationsPerElement + point];
if (row.element != shell_order[element] ||
row.location != kLocations[point] ||
row.natural_coordinates != natural[point] || !IsFinite(row)) {
row.natural_coordinates != natural[point] || !results_internal::IsFiniteShellRow(row)) {
return Failure(FailureCategory::kSolver,
"Shell recovery rows have invalid identity or values.");
}
@@ -228,7 +181,7 @@ Status DynamicAnalysisState::CommitAcceptedFrame(
}
for (std::size_t node = 0U; node < node_count; ++node) {
if (frame.nodal_internal_order[node] != node ||
!SameId(frame.nodal_source_ids[node], nodal_source_ids_[node]) ||
!SourceIdentityEquals(frame.nodal_source_ids[node], nodal_source_ids_[node]) ||
!IsFinite(frame.nodal_triad[node][0U]) ||
!IsFinite(frame.nodal_triad[node][1U]) ||
!IsFinite(frame.nodal_triad[node][2U]) ||
+18 -23
View File
@@ -162,54 +162,49 @@ Result<NodalLift> MakeLift(const Matrix& mass,
Failure("eigen-mass-conditioning", node,
"A positive-space mass pivot is not resolvable."));
}
Matrix lower(positive, positive);
Matrix inverse_lower(positive, positive);
if (!dense_blas_internal::CholeskyLowerAndInverse(normalized_mp, lower,
inverse_lower)) {
const auto factor = dense_blas_internal::DenseFactor::Cholesky(normalized_mp);
if (!factor.HasValue()) {
return Result<NodalLift>::Failure(
Failure("eigen-mass-conditioning", node,
"A positive-space mass pivot is not resolvable."));
}
const Matrix lower = std::move(factor.Value().Lower().Value());
Matrix transposed_lower(positive, positive);
Matrix scaled_inverse_transpose(positive, positive);
const double root_scale = std::sqrt(scale);
for (std::size_t i = 0; i < positive; ++i)
for (std::size_t j = 0; j < positive; ++j) {
for (std::size_t j = 0; j < positive; ++j)
transposed_lower(i, j) = lower(j, i);
scaled_inverse_transpose(i, j) = inverse_lower(j, i) / root_scale;
}
const auto inverse_mass = dense_blas_internal::MultiplyTransposeLeft(
inverse_lower, inverse_lower);
const auto condition = factor.Value().ReciprocalConditionInfinity();
const auto rebuilt_mass = lower.Multiply(transposed_lower);
double norm_mp = 0.0;
double norm_inverse = 0.0;
double reconstruction = 0.0;
double scaled_mass_norm = 0.0;
double rebuilt_mass_norm = 0.0;
for (std::size_t i = 0; i < positive; ++i) {
double row_mp = 0.0;
double row_inverse = 0.0;
for (std::size_t j = 0; j < positive; ++j) {
const double inverse = inverse_mass(i, j);
const double rebuilt = rebuilt_mass(i, j);
row_inverse += std::abs(inverse);
row_mp += std::abs(mp(i, j) / scale);
reconstruction = std::hypot(reconstruction, rebuilt - mp(i, j) / scale);
scaled_mass_norm = std::hypot(scaled_mass_norm, mp(i, j) / scale);
rebuilt_mass_norm = std::hypot(rebuilt_mass_norm, rebuilt);
}
norm_mp = (std::max)(norm_mp, row_mp);
norm_inverse = (std::max)(norm_inverse, row_inverse);
}
result.record.rcond_inf = (1.0 / norm_mp) / norm_inverse;
if (!(result.record.rcond_inf >= kConditionLimit) ||
if (!condition.HasValue() || !(condition.Value() >= kConditionLimit) ||
!(Ratio(reconstruction, scaled_mass_norm + rebuilt_mass_norm) <=
kAlgebraLimit)) {
return Result<NodalLift>::Failure(Failure(
"eigen-mass-conditioning", node,
"Nodal mass fails rcond_inf >= 2^-32 or Cholesky reconstruction."));
}
result.scaled_lift = basis.Multiply(scaled_inverse_transpose);
result.record.rcond_inf = condition.Value();
// X L^T = basis/sqrt(scale), so X^T M_scaled X = I.
Matrix scaled_basis = basis;
const double root_scale = std::sqrt(scale);
for (std::size_t c = 0; c < 6U; ++c)
for (std::size_t j = 0; j < positive; ++j)
scaled_basis(c, j) /= root_scale;
auto whitened = factor.Value().SolveLowerRight(scaled_basis, true);
if (!whitened.HasValue())
return Result<NodalLift>::Failure(Failure(
"eigen-mass-conditioning", node, "Nodal whitening is nonfinite."));
result.scaled_lift = std::move(whitened.Value());
for (std::size_t c = 0; c < 6U; ++c) {
for (std::size_t j = 0; j < positive; ++j) {
if (!std::isfinite(result.scaled_lift(c, j))) {
+86
View File
@@ -0,0 +1,86 @@
#ifndef FESA_ANALYSIS_EQUILIBRIUM_ALGORITHM_H_
#define FESA_ANALYSIS_EQUILIBRIUM_ALGORITHM_H_
#include <cstdint>
#include <functional>
#include <optional>
#include <utility>
#include "fesa/core/status.h"
namespace fesa {
/// @brief Controls evaluated iteration order without owning a family candidate.
/// @note Evaluation and its referenced trial context remain alive and unmoved
/// throughout this call. A successful evaluation value means candidate ready.
class EquilibriumAlgorithm {
public:
using Evaluation = std::function<Result<bool>(std::uint32_t)>;
virtual ~EquilibriumAlgorithm() = default;
virtual Status SolveCurrentStep(std::uint32_t first, std::uint32_t last,
const Evaluation& evaluate) = 0;
};
/// @brief Preserves the original inclusive full-Newton evaluation sequence.
class NewtonStepAlgorithm final : public EquilibriumAlgorithm {
public:
Status SolveCurrentStep(std::uint32_t first, std::uint32_t last,
const Evaluation& evaluate) override {
for (std::uint32_t iteration = first; iteration <= last; ++iteration) {
auto result = evaluate(iteration);
if (!result.HasValue()) return result.GetStatus();
if (result.Value()) return Status::Ok();
}
// Family evaluators retain their original final-evaluation failure.
return Status::Ok();
}
};
/// @brief Owns one procedure-local typed evaluator and unpublished candidate.
/// @note This value is not an algorithm interface. The injected algorithm never
/// owns or type-erases Candidate, and neither callback nor context may escape.
template <typename Candidate, std::uint32_t kFirstIteration>
class EquilibriumStep {
public:
using Evaluation =
std::function<Result<std::optional<Candidate>>(std::uint32_t)>;
EquilibriumStep(Evaluation evaluate, std::uint32_t maximum)
: evaluate_(std::move(evaluate)), maximum_(maximum) {}
EquilibriumStep(const EquilibriumStep&) = delete;
EquilibriumStep& operator=(const EquilibriumStep&) = delete;
Status Solve(EquilibriumAlgorithm& algorithm) {
const EquilibriumAlgorithm::Evaluation evaluate =
[this](std::uint32_t iteration) -> Result<bool> {
auto result = evaluate_(iteration);
if (!result.HasValue()) return Result<bool>::Failure(result.GetStatus());
if (result.Value().has_value()) {
pending_ = std::move(result.Value());
return Result<bool>::Success(true);
}
return Result<bool>::Success(false);
};
return algorithm.SolveCurrentStep(kFirstIteration, maximum_, evaluate);
}
bool HasCandidate() const { return pending_.has_value(); }
/// @brief Transfers the validated candidate to the procedure commit boundary.
Candidate TakeCandidate() { return std::move(pending_.value()); }
private:
Evaluation evaluate_;
std::uint32_t maximum_;
std::optional<Candidate> pending_;
};
// Static evaluations start at one; mechanical HHT corrections start at zero.
// Evaluation at the maximum still performs the existing endpoint validation.
template <typename Candidate>
using ShellFullNewtonStep = EquilibriumStep<Candidate, 1U>;
template <typename Candidate>
using SolidFullNewtonStep = EquilibriumStep<Candidate, 1U>;
template <typename Candidate>
using ReducedShellHhtStep = EquilibriumStep<Candidate, 0U>;
template <typename Candidate>
using SolidHhtStep = EquilibriumStep<Candidate, 0U>;
} // namespace fesa
#endif // FESA_ANALYSIS_EQUILIBRIUM_ALGORITHM_H_
+36 -60
View File
@@ -16,11 +16,14 @@
#include "fesa/solvers/linear/linear_solver.h"
#include "fesa/solvers/linear/tangent_solver.h"
#include "math/dense_blas_internal.h"
#include "math/compensated_sum.h"
namespace fesa {
namespace hht_internal {
namespace {
using math_internal::AddCompensated;
/// @brief Attaches a fail-closed dynamic diagnostic to the source model.
Status Failure(const Domain& domain, const std::string& code,
const std::string& message) {
@@ -148,48 +151,23 @@ Status CheckLocalMass(const Domain& domain, const SparseMatrix& matrix,
return Failure(domain, "dynamic-positive-mass",
"A nonempty physical mass block has no positive scale.");
}
double norm = 0.0;
for (std::size_t i = 0; i < size; ++i) {
double sum = 0.0;
for (std::size_t j = 0; j < size; ++j) {
block(i, j) /= scale;
sum += std::abs(block(i, j));
}
norm = std::max(norm, sum);
}
Matrix lower{size, size};
Matrix inverse_lower{size, size};
auto factor_failure = dense_blas_internal::CholeskyFailure::kNone;
for (std::size_t i = 0; i < size; ++i)
for (std::size_t j = 0; j < size; ++j) block(i, j) /= scale;
try {
if (!dense_blas_internal::CholeskyLowerAndInverse(
block, lower, inverse_lower, &factor_failure)) {
if (factor_failure == dense_blas_internal::CholeskyFailure::kInverse)
return Failure(
domain, "dynamic-positive-mass-conditioning",
"A nodal free physical block reciprocal condition is below 2^-32.");
const auto factor = dense_blas_internal::DenseFactor::Cholesky(block);
if (!factor.HasValue())
return Failure(
domain, "dynamic-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
const auto rcond = factor.Value().ReciprocalConditionInfinity();
if (!rcond.HasValue() || !std::isfinite(rcond.Value()) ||
!(rcond.Value() >= std::ldexp(1.0, -32)))
return Failure(
domain, "dynamic-positive-mass-conditioning",
"A nodal free physical block reciprocal condition is below 2^-32.");
} catch (const std::exception& error) {
return Failure(domain, "dynamic-positive-mass", error.what());
}
const Matrix inverse =
dense_blas_internal::MultiplyTransposeLeft(inverse_lower, inverse_lower);
std::array<double, 6> sums{};
for (std::size_t column = 0; column < size; ++column) {
for (std::size_t i = 0; i < size; ++i) {
sums[i] += std::abs(inverse(i, column));
}
}
const double inverse_norm = *std::max_element(sums.begin(), sums.end());
const double rcond = 1.0 / norm / inverse_norm;
if (!std::isfinite(inverse_norm) || !std::isfinite(rcond) ||
!(rcond >= std::ldexp(1.0, -32))) {
return Failure(
domain, "dynamic-positive-mass-conditioning",
"A nodal free physical block reciprocal condition is below 2^-32.");
}
return Status::Ok();
}
@@ -468,15 +446,6 @@ Result<SparseMatrix> ConstraintGradient(const DynamicAssembly& assembly,
std::move(entries));
}
/// @brief Accumulates one deterministic sparse contraction without lost sums.
void AddCompensated(const double term, double* sum, double* correction) {
const double next = *sum + term;
*correction += std::abs(*sum) >= std::abs(term)
? (*sum - next) + term
: (term - next) + *sum;
*sum = next;
}
/// @brief Stores W-scaled regularity with exact inverse-action infinity norms.
struct Regularity {
SparseMatrix matrix;
@@ -553,23 +522,10 @@ Result<Regularity> CheckRegularity(const Domain& domain,
sum += std::abs(matrix.Value().Values()[index]);
norm = std::max(norm, sum);
}
Vector inverse_sums{nodes.size()};
// Columns are discarded after their absolute row sums are accumulated;
// a global dense inverse is never retained.
for (std::size_t column = 0; column < nodes.size(); ++column) {
Vector rhs{nodes.size()};
rhs[column] = 1.0;
Vector solution{nodes.size()};
const Status solved = solver.SolveCorrection(rhs, solution);
if (!solved.IsOk()) return Result<Regularity>::Failure(solved);
const Status checked = CheckLinearSolve(matrix.Value(), rhs, solution);
if (!checked.IsOk()) return Result<Regularity>::Failure(checked);
for (std::size_t row = 0; row < nodes.size(); ++row)
inverse_sums[row] += std::abs(solution[row]);
}
double inverse_norm = 0.0;
for (std::size_t row = 0; row < nodes.size(); ++row)
inverse_norm = std::max(inverse_norm, inverse_sums[row]);
const Status accumulated =
AccumulateCheckedCanonicalNorm(matrix.Value(), solver, inverse_norm);
if (!accumulated.IsOk()) return Result<Regularity>::Failure(accumulated);
const double condition = 1.0 / norm / inverse_norm;
if (!std::isfinite(norm) || !std::isfinite(inverse_norm) ||
!std::isfinite(condition) || !(condition >= std::ldexp(1.0, -32))) {
@@ -662,6 +618,26 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
} // namespace
Status AccumulateCheckedCanonicalNorm(const SparseMatrix& matrix,
TangentSolver& solver, double& norm) {
const auto size = matrix.Rows();
Vector row_sums{size};
for (std::size_t column = 0U; column < size; ++column) {
Vector rhs{size};
rhs[column] = 1.0;
Vector solution{size};
const Status solved = solver.SolveCorrection(rhs, solution);
if (!solved.IsOk()) return solved;
const Status checked = CheckLinearSolve(matrix, rhs, solution);
if (!checked.IsOk()) return checked;
for (std::size_t row = 0U; row < size; ++row)
row_sums[row] += std::abs(solution[row]);
}
for (std::size_t row = 0U; row < size; ++row)
norm = (std::max)(norm, row_sums[row]);
return Status::Ok();
}
double OrderedNullMetric(const SparseMatrix& metric, const std::size_t node,
const Vector3& direction) {
Vector left{9U};
+28 -11
View File
@@ -151,9 +151,24 @@ Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
*material, density);
if (!inertia.HasValue())
return Result<std::vector<RuntimeShell>>::Failure(inertia.GetStatus());
shells.push_back({static_cast<EntityIndex>(index),
dofs.ShellElementScatter(static_cast<EntityIndex>(index)),
std::move(internal.Value()), std::move(inertia.Value())});
auto internal_kernel =
std::make_unique<NonlinearMitc4Shell>(std::move(internal.Value()));
auto inertia_kernel =
std::make_unique<NonlinearMitc4Inertia>(std::move(inertia.Value()));
ElementDofLayout layout{
definition->SourceId(), definition->NodeIndices(),
{DofComponent::kUx, DofComponent::kUy, DofComponent::kUz,
DofComponent::kUrx, DofComponent::kUry, DofComponent::kUrz}};
auto trial = std::make_unique<Mitc4TrialAdapter>(layout, *internal_kernel);
auto kinetic = std::make_unique<Mitc4InertiaAdapter>(layout, *inertia_kernel);
auto mapped = dofs.ElementScatter(kinetic->DofLayout());
if (!mapped.HasValue())
return Result<std::vector<RuntimeShell>>::Failure(mapped.GetStatus());
std::array<std::size_t, 24> scatter{};
std::copy(mapped.Value().begin(), mapped.Value().end(), scatter.begin());
shells.push_back({static_cast<EntityIndex>(index), scatter,
std::move(internal_kernel), std::move(inertia_kernel),
std::move(trial), std::move(kinetic)});
}
return Result<std::vector<RuntimeShell>>::Success(std::move(shells));
}
@@ -431,8 +446,8 @@ Result<DynamicAssembly> EvaluateAssembly(
}
const auto before = retain_constraints ? ReadTrialEvaluationContext()
: TrialEvaluationContext{};
auto f = EvaluateInternalResidualAndTangent(
shells[e].internal, trial, directional_third ? &v : nullptr);
auto f = shells[e].internal->EvaluateTrial(
trial, directional_third ? &v : nullptr);
if (!f.HasValue()) {
failures[e] = f.GetStatus();
return;
@@ -441,7 +456,7 @@ Result<DynamicAssembly> EvaluateAssembly(
const auto after = ReadTrialEvaluationContext();
if (ReusableTrialContext(before, after)) evaluation_contexts[e] = after;
}
auto m = shells[e].inertia.Evaluate(trial, v, a);
auto m = shells[e].inertia->Evaluate(trial, v, a);
if (!m.HasValue()) {
failures[e] = m.GetStatus();
return;
@@ -499,7 +514,7 @@ Result<DynamicAssembly> EvaluateAssembly(
external_done[node] = true;
const auto first = 6U * node + 3U;
const auto external =
shells[e].internal.EvaluateExternalDrillingConstraint(
shells[e].internal->EvaluateExternalDrillingConstraint(
local, Triple(frame.generalized_coordinates, first),
Triple(frame.velocity, first),
Triple(frame.external_force, first), {}, {});
@@ -660,7 +675,7 @@ Result<DynamicAssembly> EvaluateAssembly(
record.scatter = shells[e].scatter;
record.configuration =
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(
shells[e].internal);
*shells[e].internal_kernel);
record.evaluation_context = evaluation_contexts[e];
for (std::size_t i = 0U; i < 24U; ++i) {
record.q[i] = frame.generalized_coordinates[shells[e].scatter[i]];
@@ -703,11 +718,13 @@ Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
const auto first = 6U * node;
const Vector3 arm = Vector3(domain.Nodes()[node].coordinates) +
Triple(frame->generalized_coordinates, first);
const Matrix transform = InverseTransposeLeftJacobianSO3(
Triple(frame->generalized_coordinates, first + 3U));
const Vector3 phi = Triple(frame->generalized_coordinates, first + 3U);
for (const Vector* values : {&frame->external_force, &frame->reaction}) {
const Vector3 force = Triple(*values, first);
const Vector3 moment = Transform(transform, Triple(*values, first + 3U));
const auto spatial = SpatialMomentSO3(phi, Triple(*values, first + 3U));
if (!spatial.HasValue())
return Failure("Dynamic physical force or moment balance exceeds 1e-8.");
const Vector3 moment = spatial.Value();
total_force = total_force + force;
total_moment = total_moment + arm.Cross(force) + moment;
force_scale += force.Norm();
+12 -2
View File
@@ -4,11 +4,13 @@
#include <array>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <vector>
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/analysis/hht_time_integration.h"
#include "fesa/elements/element_capabilities.h"
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/math/sparse_matrix.h"
@@ -25,8 +27,10 @@ namespace hht_internal {
struct RuntimeShell {
EntityIndex element_index;
std::array<std::size_t, 24> scatter;
NonlinearMitc4Shell internal;
NonlinearMitc4Inertia inertia;
std::unique_ptr<NonlinearMitc4Shell> internal_kernel;
std::unique_ptr<NonlinearMitc4Inertia> inertia_kernel;
std::unique_ptr<ShellTrialElement> internal;
std::unique_ptr<ShellInertiaElement> inertia;
};
/// @brief Captures only controls used by the current MSVC x64 local evaluator.
@@ -138,6 +142,12 @@ Result<SparseMatrix> ScaledFree(const SparseMatrix& matrix,
Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs,
const Vector& solution);
/// @brief Streams checked canonical solves into the caller's infinity norm.
/// @note The caller owns factorization and the initial norm. Each column is
/// discarded after ascending absolute row-sum accumulation; failures preserve norm.
Status AccumulateCheckedCanonicalNorm(const SparseMatrix& matrix,
TangentSolver& solver, double& norm);
/// @brief Finds analytic admissible node null directions without rank cuts.
std::vector<std::size_t> NullNodes(
const DofManager& dofs,
@@ -0,0 +1,74 @@
#ifndef FESA_ANALYSIS_HHT_REDUCED_INTEGRATOR_H_
#define FESA_ANALYSIS_HHT_REDUCED_INTEGRATOR_H_
#include <utility>
#include "analysis/hht_reduced_internal.h"
namespace fesa::hht_reduced_internal {
struct ReducedHistory {
Vector p{0U};
Vector velocity{0U};
Vector acceleration{0U};
Vector algorithmic_acceleration{0U};
Vector z{0U};
};
/// @brief Keeps every private predictor compensation channel before a trial.
struct ReducedShellPredictor {
Vector pp{0U};
Vector vpp{0U};
Vector coordinate_low{0U};
Vector increment_trace{0U};
Vector increment_trace_compensation{0U};
Vector increment_trace_absolute_sum{0U};
};
struct ReducedShellRateRequest {
const FixedChart& chart;
const ReducedHistory& history;
const Vector& trial_algorithmic;
const Vector& vpp;
bool copy_translation_rates;
const Vector& retained_velocity;
const Vector& retained_acceleration;
double h;
const HhtParameters& parameters;
};
struct ReducedShellTrialRates {
Vector velocity{0U};
Vector acceleration{0U};
};
/// @brief Handles private shell predictors and exact sparse rate
/// reconstruction.
/// @note Chart/root/solver are non-owning call views; no history is committed.
class ShellHhtIntegrator {
public:
virtual ~ShellHhtIntegrator() = default;
virtual ReducedShellPredictor
Predict(const ReducedHistory& history, double h,
const HhtParameters& parameters) const = 0;
/// @note Output buffers have history.p.Size() entries and do not alias any
/// request input or each other. The caller owns their allocation/lifetime.
virtual void EvaluateRates(const ReducedShellRateRequest& request,
Vector& velocity, Vector& acceleration) const = 0;
virtual Result<ReconstructedRates>
ReconstructPhysicalRates(const FixedChart& chart, const EmbeddingRoot& root,
const Vector& velocity, const Vector& acceleration,
TangentSolver& solver) const = 0;
};
/// @brief Sequences one private predictor, pending endpoint and commit.
/// @note Callables are synchronous borrowed views. They retain all original
/// evaluation/diagnostic/commit policy; no accepted history is owned here.
template <typename Evaluate, typename Commit>
Status RunReducedHhtStep(const ShellHhtIntegrator& integrator,
const ReducedHistory& history, double h,
const HhtParameters& parameters,
Evaluate&& evaluate, Commit&& commit) {
auto predictor = integrator.Predict(history, h, parameters);
auto pending = evaluate(integrator, std::move(predictor));
if (!pending.HasValue()) return pending.GetStatus();
return commit(std::move(pending.Value()));
}
} // namespace fesa::hht_reduced_internal
#endif // FESA_ANALYSIS_HHT_REDUCED_INTEGRATOR_H_
+18 -60
View File
@@ -10,13 +10,18 @@
#include <string>
#include <utility>
#include "fesa/core/source_identity.h"
#include "fesa/solvers/linear/tangent_solver.h"
#include "math/dense_blas_internal.h"
#include "math/high_low_arithmetic.h"
#include "math/finite_values.h"
#include "math/compensated_sum.h"
namespace fesa::hht_reduced_internal {
namespace {
using RateSum = math_internal::CompensatedSum;
Status Failure(const std::string& message,
const std::string& code = "dynamic-invalid-state") {
return Status::Failure(FailureCategory::kSolver,
@@ -25,27 +30,9 @@ Status Failure(const std::string& message,
}
bool Finite(const Vector& values) {
for (std::size_t i = 0U; i < values.Size(); ++i)
if (!std::isfinite(values[i])) return false;
return true;
return math_internal::IsFinite(values);
}
class RateSum {
public:
void Add(const double term) {
const double next = sum_ + term;
correction_ += std::abs(sum_) >= std::abs(term)
? (sum_ - next) + term
: (term - next) + sum_;
sum_ = next;
}
double Value() const { return sum_ + correction_; }
private:
double sum_ = 0.0;
double correction_ = 0.0;
};
bool ExactlyEqual(const Vector& left, const Vector& right) {
if (left.Size() != right.Size()) return false;
for (std::size_t i = 0U; i < left.Size(); ++i)
@@ -65,13 +52,6 @@ bool SameBits(const double left, const double right) {
return std::memcmp(&left, &right, sizeof(double)) == 0;
}
/// @brief Compares semantic and original source identity without object keys.
bool SameSource(const SourceEntityId& left, const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
/// @brief Finds only the current complete directional response, otherwise miss.
const hht_internal::DirectionalShellConstraintData* MatchingConstraints(
const Domain& domain, const hht_internal::RuntimeShell& shell,
@@ -98,7 +78,7 @@ const hht_internal::DirectionalShellConstraintData* MatchingConstraints(
hht_internal::ReadTrialEvaluationContext()))
return nullptr;
const auto configuration =
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(shell.internal);
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(*shell.internal_kernel);
if (record.configuration.source.file != configuration.source.file ||
record.configuration.source.line != configuration.source.line ||
record.configuration.identity != configuration.identity)
@@ -116,7 +96,7 @@ const hht_internal::DirectionalShellConstraintData* MatchingConstraints(
for (std::size_t node = 0U; node < 4U; ++node) {
const auto global_node = shell.scatter[6U * node] / 6U;
if (global_node >= domain.Nodes().size() ||
!SameSource(record.node_source_ids[node],
!SourceIdentityEquals(record.node_source_ids[node],
domain.Nodes()[global_node].source_id))
return nullptr;
}
@@ -534,7 +514,7 @@ Result<ConstraintOperators> EvaluateConstraintOperators(
std::optional<NonlinearMitc4InternalEquilibrium> fresh;
if (saved == nullptr) {
auto internal =
EvaluateInternalResidualAndTangent(shell.internal, Trial(q), &v);
shell.internal->EvaluateTrial(Trial(q), &v);
if (!internal.HasValue())
return Result<ConstraintOperators>::Failure(internal.GetStatus());
fresh = std::move(internal.Value());
@@ -572,7 +552,7 @@ Result<ConstraintOperators> EvaluateConstraintOperators(
if (external_done[global_node]) continue;
external_done[global_node] = true;
const auto first = 6U * global_node + 3U;
const auto external = shell.internal.EvaluateExternalDrillingConstraint(
const auto external = shell.internal->EvaluateExternalDrillingConstraint(
local_node, Vector3{q[6U * local_node + 3U],
q[6U * local_node + 4U],
q[6U * local_node + 5U]},
@@ -587,7 +567,7 @@ Result<ConstraintOperators> EvaluateConstraintOperators(
force_acceleration[first + 2U]});
if (!external.HasValue())
return Result<ConstraintOperators>::Failure(external.GetStatus());
const auto rate_term = shell.internal.EvaluateExternalDrillingConstraint(
const auto rate_term = shell.internal->EvaluateExternalDrillingConstraint(
local_node, Vector3{q[6U * local_node + 3U],
q[6U * local_node + 4U],
q[6U * local_node + 5U]},
@@ -596,7 +576,7 @@ Result<ConstraintOperators> EvaluateConstraintOperators(
if (!rate_term.HasValue())
return Result<ConstraintOperators>::Failure(rate_term.GetStatus());
const auto acceleration_term =
shell.internal.EvaluateExternalDrillingConstraint(
shell.internal->EvaluateExternalDrillingConstraint(
local_node, Vector3{q[6U * local_node + 3U],
q[6U * local_node + 4U],
q[6U * local_node + 5U]},
@@ -1321,21 +1301,10 @@ Result<double> CheckGraphRegularity(
if (!sparse.HasValue()) return Result<double>::Failure(sparse.GetStatus());
Status status = solver.FactorizeFresh(sparse.Value());
if (!status.IsOk()) return Result<double>::Failure(status);
Vector inverse_sums{size};
for (std::size_t column = 0U; column < size; ++column) {
Vector rhs{size};
rhs[column] = 1.0;
Vector solution{size};
status = solver.SolveCorrection(rhs, solution);
if (!status.IsOk()) return Result<double>::Failure(status);
status = hht_internal::CheckLinearSolve(sparse.Value(), rhs, solution);
if (!status.IsOk()) return Result<double>::Failure(status);
for (std::size_t row = 0U; row < size; ++row)
inverse_sums[row] += std::abs(solution[row]);
}
double inverse_norm = 0.0;
for (std::size_t row = 0U; row < size; ++row)
inverse_norm = std::max(inverse_norm, inverse_sums[row]);
status = hht_internal::AccumulateCheckedCanonicalNorm(
sparse.Value(), solver, inverse_norm);
if (!status.IsOk()) return Result<double>::Failure(status);
const double condition = 1.0 / norm / inverse_norm;
if (!std::isfinite(norm) || !std::isfinite(inverse_norm) ||
!std::isfinite(condition) || !(condition >= std::ldexp(1.0, -32))) {
@@ -1409,20 +1378,9 @@ Result<double> CheckSelectorTransversality(
if (!sparse.HasValue()) return Result<double>::Failure(sparse.GetStatus());
auto status = solver.FactorizeFresh(sparse.Value());
if (!status.IsOk()) return Result<double>::Failure(status);
Vector inverse_rows{rows.size()};
for (std::size_t column = 0U; column < rows.size(); ++column) {
Vector rhs{rows.size()};
rhs[column] = 1.0;
Vector solution{rows.size()};
status = solver.SolveCorrection(rhs, solution);
if (!status.IsOk()) return Result<double>::Failure(status);
status = hht_internal::CheckLinearSolve(sparse.Value(), rhs, solution);
if (!status.IsOk()) return Result<double>::Failure(status);
for (std::size_t row = 0U; row < rows.size(); ++row)
inverse_rows[row] += std::abs(solution[row]);
}
for (std::size_t row = 0U; row < rows.size(); ++row)
inverse_norm = (std::max)(inverse_norm, inverse_rows[row]);
status = hht_internal::AccumulateCheckedCanonicalNorm(
sparse.Value(), solver, inverse_norm);
if (!status.IsOk()) return Result<double>::Failure(status);
}
const double condition = 1.0 / norm / inverse_norm;
if (!std::isfinite(condition) || condition < std::ldexp(1.0, -32))
+520 -344
View File
@@ -1,4 +1,7 @@
#include "analysis/hht_reduced_procedure.h"
#include "analysis/hht_reduced_integrator.h"
#include "equilibrium_algorithm.h"
#include <algorithm>
#include <cmath>
@@ -296,12 +299,83 @@ Status CheckReducedUpdates(const Domain& domain,
return Status::Ok();
}
struct ReducedHistory {
Vector p{0U};
Vector velocity{0U};
Vector acceleration{0U};
Vector algorithmic_acceleration{0U};
Vector z{0U};
// Private nonvirtual primitive: keep velocity assignment before acceleration
// evaluation, including per-index position retries. No accepted state changes.
void EvaluateReducedShellRate(double predictor_velocity,
double algorithmic_acceleration,
double old_acceleration, double h,
const HhtParameters& parameters,
double* velocity, double* acceleration) {
*velocity = predictor_velocity + parameters.gamma * h * algorithmic_acceleration;
*acceleration = (algorithmic_acceleration + parameters.alpha * old_acceleration) /
(1.0 + parameters.alpha);
}
class ReducedCoordinateShellHhtIntegrator final : public ShellHhtIntegrator {
public:
ReducedShellPredictor
Predict(const ReducedHistory& history, double h,
const HhtParameters& parameters) const override {
Vector pp{history.p.Size()}, vpp{history.p.Size()};
for (std::size_t i = 0U; i < history.p.Size(); ++i) {
pp[i] =
history.p[i] + h * history.velocity[i] +
h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i];
vpp[i] = history.velocity[i] + h * (1.0 - parameters.gamma) *
history.algorithmic_acceleration[i];
}
Vector coordinate_low{history.p.Size()};
Vector increment_trace{history.p.Size()};
Vector increment_trace_compensation{history.p.Size()};
Vector increment_trace_absolute_sum{history.p.Size()};
for (std::size_t i = 0U; i < history.p.Size(); ++i) {
const double velocity_term = h * history.velocity[i];
const double old_acceleration_term =
h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i];
const double first_sum = history.p[i] + velocity_term;
coordinate_low[i] =
AdditionLowPart(history.p[i], velocity_term, first_sum) +
AdditionLowPart(first_sum, old_acceleration_term, pp[i]);
AddIncrementTraceTerm(velocity_term, &increment_trace[i],
&increment_trace_compensation[i]);
AddIncrementTraceTerm(old_acceleration_term, &increment_trace[i],
&increment_trace_compensation[i]);
increment_trace_absolute_sum[i] =
std::abs(velocity_term) + std::abs(old_acceleration_term);
}
return {std::move(pp),
std::move(vpp),
std::move(coordinate_low),
std::move(increment_trace),
std::move(increment_trace_compensation),
std::move(increment_trace_absolute_sum)};
}
void EvaluateRates(const ReducedShellRateRequest& request,
Vector& trial_v, Vector& trial_a) const override {
const auto& history = request.history;
const auto& trial_algorithmic = request.trial_algorithmic;
const auto& vpp = request.vpp;
const auto& parameters = request.parameters;
const double h = request.h;
for (std::size_t i = 0U; i < history.p.Size(); ++i) {
if (request.copy_translation_rates &&
PhysicalCoordinateChannel(request.chart, i) == 0U) {
trial_v[i] = request.retained_velocity[i];
trial_a[i] = request.retained_acceleration[i];
} else {
EvaluateReducedShellRate(
vpp[i], trial_algorithmic[i], history.acceleration[i], h, parameters,
&trial_v[i], &trial_a[i]);
}
}
}
Result<ReconstructedRates>
ReconstructPhysicalRates(const FixedChart& chart, const EmbeddingRoot& root,
const Vector& velocity, const Vector& acceleration,
TangentSolver& solver) const override {
return hht_reduced_internal::ReconstructPhysicalRates(chart, root, velocity,
acceleration, solver);
}
};
struct Trial {
@@ -320,6 +394,17 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
const Domain& domain, NewtonIterationLogWriter& log,
const ParallelFor& parallel_for, LinearSolver& positive_solver,
TangentSolver& tangent_solver) {
const ReducedCoordinateShellHhtIntegrator integrator;
NewtonStepAlgorithm algorithm;
return ExecuteReducedHht(domain, log, parallel_for, positive_solver,
tangent_solver, integrator, algorithm);
}
Result<DynamicExecutionResult> ExecuteReducedHht(
const Domain& domain, NewtonIterationLogWriter& log,
const ParallelFor& parallel_for, LinearSolver& positive_solver,
TangentSolver& tangent_solver, const ShellHhtIntegrator& integrator,
EquilibriumAlgorithm& algorithm) {
if (domain.Dynamic() == nullptr)
return Result<DynamicExecutionResult>::Failure(Status::Failure(
FailureCategory::kModel, {{Severity::kError, "invalid-dynamic-step", {},
@@ -433,7 +518,7 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
tangent_solver);
if (!root0.HasValue()) return Result<DynamicExecutionResult>::Failure(
WithProcedureContext(domain, root0.GetStatus(), "inner-position", 0U, 0.0));
auto rates0 = ReconstructPhysicalRates(chart.Value(), root0.Value(), history.velocity,
auto rates0 = integrator.ReconstructPhysicalRates(chart.Value(), root0.Value(), history.velocity,
history.acceleration, tangent_solver);
if (!rates0.HasValue()) return Result<DynamicExecutionResult>::Failure(
WithProcedureContext(domain, rates0.GetStatus(), "inner-rates", 0U, 0.0));
@@ -604,343 +689,434 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
status = log.Write(record); if (status.IsOk()) status = log.Flush();
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
for (std::uint32_t increment = 1U; increment <= domain.Dynamic()->increment_count; ++increment) {
struct ReducedStepCandidate {
DynamicAcceptedFrameState frame;
ReducedHistory history;
};
for (std::uint32_t increment = 1U;
increment <= domain.Dynamic()->increment_count; ++increment) {
const auto& old = state.CommittedState();
Vector pp{history.p.Size()}, vpp{history.p.Size()};
for (std::size_t i = 0U; i < history.p.Size(); ++i) {
pp[i] = history.p[i] + h * history.velocity[i] + h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i];
vpp[i] = history.velocity[i] + h * (1.0 - parameters.gamma) * history.algorithmic_acceleration[i];
}
DynamicAcceptedFrameState trial_frame = old;
trial_frame.is_initial = false; trial_frame.frame_index = increment;
trial_frame.increment_index = increment; trial_frame.time = static_cast<double>(increment) * h;
trial_frame.time_integration_formulation = DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
Vector trial_algorithmic{history.p.Size()};
Vector trial_p = pp;
Vector coordinate_low{history.p.Size()};
Vector chosen_start_p = pp;
Vector increment_trace{history.p.Size()};
Vector increment_trace_compensation{history.p.Size()};
Vector increment_trace_absolute_sum{history.p.Size()};
for (std::size_t i = 0U; i < history.p.Size(); ++i) {
const double velocity_term = h * history.velocity[i];
const double old_acceleration_term =
h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i];
const double first_sum = history.p[i] + velocity_term;
coordinate_low[i] = AdditionLowPart(history.p[i], velocity_term, first_sum) +
AdditionLowPart(first_sum, old_acceleration_term, pp[i]);
AddIncrementTraceTerm(velocity_term, &increment_trace[i],
&increment_trace_compensation[i]);
AddIncrementTraceTerm(old_acceleration_term, &increment_trace[i],
&increment_trace_compensation[i]);
increment_trace_absolute_sum[i] =
std::abs(velocity_term) + std::abs(old_acceleration_term);
}
Vector seed = history.z;
record = {}; record.increment_index = increment; record.time = trial_frame.time; record.correction_pass = true;
bool translation_private_algorithmic_pass = true;
bool copy_translation_rates = false;
Vector retained_velocity{history.p.Size()};
Vector retained_acceleration{history.p.Size()};
bool accepted = false;
for (std::uint32_t iteration = 0U;
iteration <= kMaximumOuterCorrections; ++iteration) {
record.iteration_index = iteration; record.stage = "evaluation"; record.outcome = "candidate";
const auto fail = [&](const Status& failure,
const char* stage = "outer") -> Result<DynamicExecutionResult> {
const Status contextual = WithProcedureContext(
domain, failure, stage, increment, trial_frame.time);
record.stage = stage; record.outcome = "failed";
record.failure_code = contextual.Diagnostics().empty()
? "dynamic-solver-failure"
: contextual.Diagnostics().front().code;
const Status written = log.Write(record);
return Result<DynamicExecutionResult>::Failure(
written.IsOk() ? contextual : written);
};
Vector trial_v{history.p.Size()}, trial_a{history.p.Size()};
for (std::size_t i = 0U; i < trial_p.Size(); ++i) {
if (copy_translation_rates && PhysicalCoordinateChannel(chart.Value(), i) == 0U) {
trial_v[i] = retained_velocity[i];
trial_a[i] = retained_acceleration[i];
} else {
trial_v[i] = vpp[i] + parameters.gamma * h * trial_algorithmic[i];
trial_a[i] = (trial_algorithmic[i] + parameters.alpha * history.acceleration[i]) / (1.0 + parameters.alpha);
}
}
const auto evaluate_trial = [&](const Vector& physical_p,
const Vector& physical_v,
const Vector& physical_a,
const Vector& root_seed) -> Result<Trial> {
auto root = SolveEmbeddingRoot(chart.Value(), physical_p, root_seed,
[&](const Vector& q) { return position_operators(trial_frame, q); },
tangent_solver);
if (!root.HasValue()) return Result<Trial>::Failure(
WithProcedureContext(domain, root.GetStatus(), "inner-position",
increment, trial_frame.time));
auto rates = ReconstructPhysicalRates(chart.Value(), root.Value(), physical_v,
physical_a, tangent_solver);
if (!rates.HasValue()) return Result<Trial>::Failure(
WithProcedureContext(domain, rates.GetStatus(), "inner-rates",
increment, trial_frame.time));
auto candidate = trial_frame;
candidate.generalized_coordinates = root.Value().q;
candidate.velocity = rates.Value().velocity;
candidate.acceleration = rates.Value().acceleration;
candidate.algorithmic_acceleration = Vector(dofs.FullDofCount());
for (std::size_t i = 0U; i < candidate.algorithmic_acceleration.Size(); ++i)
candidate.algorithmic_acceleration[i] =
(1.0 + parameters.alpha) * candidate.acceleration[i] -
parameters.alpha * old.acceleration[i];
candidate.iteration_count = iteration;
auto assembly = EvaluateAssembly(domain, dofs, shells.Value(), candidate,
loads.Value()[1U], h, parameters,
parallel_for, true);
if (!assembly.HasValue()) return Result<Trial>::Failure(
WithProcedureContext(domain, assembly.GetStatus(), "outer",
increment, trial_frame.time));
assembly.Value().frame.time_integration_formulation =
DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
return Result<Trial>::Success(
{std::move(assembly.Value()), std::move(root.Value()), physical_p,
physical_v, physical_a, trial_algorithmic,
std::move(rates.Value().z_velocity), std::move(rates.Value().z_acceleration)});
};
position_geometry_failure = false;
auto trial = evaluate_trial(trial_p, trial_v, trial_a, seed);
if (!trial.HasValue() && iteration == 0U && position_geometry_failure) {
for (std::size_t i = 0U; i < trial_p.Size(); ++i) {
trial_algorithmic[i] =
(history.p[i] - pp[i]) / (parameters.beta * h * h);
trial_p[i] = history.p[i];
chosen_start_p[i] = history.p[i];
coordinate_low[i] = 0.0;
increment_trace[i] = 0.0;
increment_trace_compensation[i] = 0.0;
increment_trace_absolute_sum[i] = 0.0;
trial_v[i] = vpp[i] + parameters.gamma * h * trial_algorithmic[i];
trial_a[i] = (trial_algorithmic[i] +
parameters.alpha * history.acceleration[i]) /
(1.0 + parameters.alpha);
}
trial = evaluate_trial(trial_p, trial_v, trial_a, seed);
}
if (!trial.HasValue()) return fail(trial.GetStatus());
if (iteration == 0U && !ExactlyEqual(trial_p, history.p)) {
Vector old_algorithmic{trial_p.Size()};
Vector old_velocity{trial_p.Size()};
Vector old_acceleration{trial_p.Size()};
for (std::size_t i = 0U; i < trial_p.Size(); ++i) {
old_algorithmic[i] = (history.p[i] - pp[i]) / (parameters.beta * h * h);
old_velocity[i] = vpp[i] + parameters.gamma * h * old_algorithmic[i];
old_acceleration[i] = (old_algorithmic[i] + parameters.alpha * history.acceleration[i]) /
(1.0 + parameters.alpha);
}
auto old_trial = evaluate_trial(history.p, old_velocity, old_acceleration, seed);
if (!old_trial.HasValue()) return fail(old_trial.GetStatus());
const auto reduced_residual = [&](const Trial& value) {
double norm = 0.0;
for (const auto full : dofs.FreeDofs()) {
const double scale = full % 6U < 3U ? length : 1.0;
norm = (std::max)(norm,
scale * std::abs(value.assembly.frame.residual[full]));
}
if (!std::isfinite(norm)) return std::numeric_limits<double>::infinity();
return norm;
};
const double old_residual = reduced_residual(old_trial.Value());
const double predictor_residual = reduced_residual(trial.Value());
if (!std::isfinite(old_residual) || !std::isfinite(predictor_residual))
return fail(Failure(domain, "dynamic-invalid-state",
"The scaled initial reduced residual is nonfinite."));
if (old_residual < predictor_residual) {
trial = std::move(old_trial);
trial_p = history.p;
chosen_start_p = history.p;
coordinate_low = Vector(history.p.Size());
increment_trace = Vector(history.p.Size());
increment_trace_compensation = Vector(history.p.Size());
increment_trace_absolute_sum = Vector(history.p.Size());
trial_v = old_velocity;
trial_a = old_acceleration;
trial_algorithmic = old_algorithmic;
}
}
auto candidate = trial.Value().assembly.frame;
auto operators = EvaluateConstraintOperators(domain, dofs, shells.Value(),
trial.Value().assembly, chart.Value(), zero, zero);
if (!operators.HasValue()) return fail(operators.GetStatus());
ResidualRecord(dofs, candidate, &record);
if (record.residual_pass && record.correction_pass) {
status = CompleteConstraintEvidence(domain, dofs, trial.Value().assembly, length, null_nodes,
positive_solver, tangent_solver, &candidate);
if (!status.IsOk()) return fail(status, "accepted");
status = CompleteEquilibrium(domain, dofs, trial.Value().assembly, &candidate);
if (!status.IsOk()) return fail(status, "accepted");
graph_regularity = CheckGraphRegularity(trial.Value().assembly,
chart.Value(), operators.Value(),
tangent_solver);
if (!graph_regularity.HasValue())
return fail(graph_regularity.GetStatus(), "accepted");
status = CheckReducedUpdates(domain, old, candidate, pp, vpp, trial_p, trial_v,
trial_a, trial.Value().root.z,
trial.Value().algebraic_velocity,
trial.Value().algebraic_acceleration,
trial_algorithmic, history.p, history.velocity,
history.acceleration,
history.algorithmic_acceleration,
increment_trace,
increment_trace_compensation,
increment_trace_absolute_sum,
chart.Value(), chosen_start_p, coordinate_low,
parameters, h);
if (!status.IsOk()) return fail(status, "accepted");
candidate.external_work = 0.0;
for (std::size_t i = 0U; i < dofs.FullDofCount(); ++i)
candidate.external_work += candidate.external_force[i] * candidate.generalized_coordinates[i];
candidate.energy_defect = candidate.kinetic_energy + candidate.physical_strain_energy +
candidate.drilling_energy - initial_energy - candidate.external_work;
status = state.CommitAcceptedFrame(candidate);
if (!status.IsOk()) return fail(status, "accepted");
history = {trial_p, trial_v, trial_a, trial_algorithmic, trial.Value().root.z};
record.stage = "accepted"; record.outcome = "accepted";
status = log.Write(record); if (status.IsOk()) status = log.Flush();
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
accepted = true; break;
}
if (!CanApplyOuterCorrection(iteration)) {
double full_hidden = 0.0;
double analytic_hidden = 0.0;
double inertia_hidden = 0.0;
double consistency = 0.0;
for (std::size_t row = 0U; row < chart.Value().null_nodes.size(); ++row) {
const auto node = chart.Value().null_nodes[row];
const auto& null = trial.Value().assembly.rotations[node].drill_null;
double full = 0.0;
double inertia = 0.0;
for (std::size_t component = 0U; component < 3U; ++component) {
const auto full_dof = 6U * node + 3U + component;
full += null[component] * candidate.residual[full_dof];
inertia += null[component] * candidate.inertia_force[full_dof];
}
const double analytic = operators.Value().rows[row].value;
full_hidden = (std::max)(full_hidden, std::abs(full));
analytic_hidden = (std::max)(analytic_hidden, std::abs(analytic));
inertia_hidden = (std::max)(inertia_hidden, std::abs(inertia));
consistency = (std::max)(consistency, std::abs(full - analytic - inertia));
}
std::ostringstream message;
const auto maximum = [](const Vector& values) {
double result = 0.0;
for (std::size_t i = 0U; i < values.Size(); ++i)
result = (std::max)(result, std::abs(values[i]));
return result;
};
Vector position_correction{trial_algorithmic.Size()};
Vector velocity_correction{trial_algorithmic.Size()};
for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) {
position_correction[i] = parameters.beta * h * h * trial_algorithmic[i];
velocity_correction[i] = parameters.gamma * h * trial_algorithmic[i];
}
message << "HHT exhausted 100 corrections without satisfying every endpoint check; "
<< "stage=outer, outer_iteration=" << iteration
<< ", graph_iteration=" << trial.Value().root.graph_iteration_count
<< ", graph_residual=" << trial.Value().root.graph_residual_infinity
<< ", graph_relative=" << trial.Value().root.graph_residual_relative
<< ", graph_correction=" << trial.Value().root.graph_correction_infinity
<< ", max_NtE=" << full_hidden
<< ", max_analytic_psi=" << analytic_hidden
<< ", max_NtInertia=" << inertia_hidden
<< ", max_NtE_minus_psi_minus_NtInertia=" << consistency
<< ", max_pp=" << maximum(pp)
<< ", max_beta_h2_trial_Ar=" << maximum(position_correction)
<< ", max_trial_p=" << maximum(trial_p)
<< ", max_vp_predictor=" << maximum(vpp)
<< ", max_gamma_h_trial_Ar=" << maximum(velocity_correction)
<< ", max_trial_v=" << maximum(trial_v)
<< ", max_physical_v=" << maximum(trial.Value().velocity) << ".";
return fail(Failure(domain, "dynamic-nonconvergence", message.str()));
}
status = log.Write(record);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
const double velocity_rate = parameters.gamma / (parameters.beta * h);
const double acceleration_rate = 1.0 / ((1.0 + parameters.alpha) * parameters.beta * h * h);
auto system = AssembleAugmentedReducedSystem(trial.Value().assembly, chart.Value(), operators.Value(), velocity_rate, acceleration_rate, length, h);
if (!system.HasValue()) return fail(system.GetStatus());
const bool angular_only = ShouldRefineAngular(
chart.Value(), candidate.free_residual_metrics,
{record.coordinate_correction[0U], record.velocity_correction[0U],
record.acceleration_correction[0U], record.algorithmic_correction[0U]},
translation_private_algorithmic_pass);
auto selected = SelectAugmentedSolve(system.Value(), chart.Value(), angular_only);
if (!selected.HasValue()) return fail(selected.GetStatus());
const auto linear_failure = [&](const Status& failure) {
return fail(failure, angular_only ? "angular-newton-linear-solve"
: "full-newton-linear-solve");
};
Vector selected_solution{selected.Value().rhs.Size()};
if (selected_solution.Size() > 0U) {
status = tangent_solver.FactorizeFresh(selected.Value().tangent);
if (!status.IsOk()) return linear_failure(status);
status = tangent_solver.SolveCorrection(selected.Value().rhs, selected_solution);
if (!status.IsOk()) return linear_failure(status);
status = CheckLinearSolve(selected.Value().tangent, selected.Value().rhs, selected_solution);
if (!status.IsOk()) return linear_failure(status);
}
auto solution = EmbedAugmentedSolution(selected.Value(), selected_solution,
system.Value().rhs.Size());
if (!solution.HasValue()) return fail(solution.GetStatus());
auto correction = MapAugmentedCorrection(chart.Value(), system.Value(), solution.Value(), velocity_rate, acceleration_rate);
if (!correction.HasValue()) return fail(correction.GetStatus());
Vector full_algorithmic{dofs.FullDofCount()};
Vector reduced_algorithmic{correction.Value().p.Size()};
Vector reduced_physical{correction.Value().p.Size()};
for (std::size_t i = 0U; i < full_algorithmic.Size(); ++i)
full_algorithmic[i] =
(1.0 + parameters.alpha) * correction.Value().acceleration[i];
for (std::size_t i = 0U; i < reduced_algorithmic.Size(); ++i) {
reduced_algorithmic[i] = correction.Value().p[i] /
(parameters.beta * h * h);
reduced_physical[i] = acceleration_rate * correction.Value().p[i];
}
CorrectionRecord(correction.Value().coordinate, correction.Value().velocity,
correction.Value().acceleration, full_algorithmic,
reduced_algorithmic, reduced_physical, chart.Value(), dofs, length, h,
parameters.alpha,
&record);
translation_private_algorithmic_pass = true;
for (std::size_t i = 0U; i < chart.Value().p_dofs.size(); ++i)
if (PhysicalCoordinateChannel(chart.Value(), i) == 0U)
translation_private_algorithmic_pass =
translation_private_algorithmic_pass &&
PrivateCorrectionEqual(chart.Value(), i, full_algorithmic,
reduced_algorithmic, reduced_physical, parameters.alpha);
copy_translation_rates = angular_only;
if (angular_only) {
retained_velocity = trial_v;
retained_acceleration = trial_a;
}
for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) {
if (angular_only && PhysicalCoordinateChannel(chart.Value(), i) == 0U) continue;
trial_algorithmic[i] += reduced_algorithmic[i];
}
for (std::size_t i = 0U; i < trial_p.Size(); ++i) {
if (angular_only && PhysicalCoordinateChannel(chart.Value(), i) == 0U) continue;
AdvanceCoordinate(correction.Value().p[i], &trial_p[i], &coordinate_low[i]);
}
for (std::size_t i = 0U; i < increment_trace.Size(); ++i) {
if (angular_only && PhysicalCoordinateChannel(chart.Value(), i) == 0U) continue;
AddIncrementTraceTerm(correction.Value().p[i], &increment_trace[i],
&increment_trace_compensation[i]);
increment_trace_absolute_sum[i] += std::abs(correction.Value().p[i]);
}
seed = trial.Value().root.z;
for (std::size_t i = 0U; i < seed.Size(); ++i)
seed[i] += correction.Value().z[i];
}
if (!accepted) return Result<DynamicExecutionResult>::Failure(
Failure(domain, "dynamic-nonconvergence", "Dynamic increment was not accepted."));
const auto report_failure = [&](const Status& failure, const char* stage) {
const Status contextual = WithProcedureContext(
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;
const Status written = log.Write(record);
return written.IsOk() ? contextual : written;
};
status = RunReducedHhtStep(
integrator, history, h, parameters,
[&](const ShellHhtIntegrator& step_integrator,
ReducedShellPredictor predictor) -> Result<ReducedStepCandidate> {
Vector pp = std::move(predictor.pp);
Vector vpp = std::move(predictor.vpp);
DynamicAcceptedFrameState trial_frame = old;
trial_frame.is_initial = false;
trial_frame.frame_index = increment;
trial_frame.increment_index = increment;
trial_frame.time = static_cast<double>(increment) * h;
trial_frame.time_integration_formulation =
DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
Vector trial_algorithmic{history.p.Size()};
Vector trial_p = pp;
Vector coordinate_low = std::move(predictor.coordinate_low);
Vector chosen_start_p = pp;
Vector increment_trace = std::move(predictor.increment_trace);
Vector increment_trace_compensation =
std::move(predictor.increment_trace_compensation);
Vector increment_trace_absolute_sum =
std::move(predictor.increment_trace_absolute_sum);
Vector seed = history.z;
record = {};
record.increment_index = increment;
record.time = trial_frame.time;
record.correction_pass = true;
bool translation_private_algorithmic_pass = true;
bool copy_translation_rates = false;
Vector retained_velocity{history.p.Size()};
Vector retained_acceleration{history.p.Size()};
using StepResult = Result<std::optional<ReducedStepCandidate>>;
const auto write_iteration =
[&](const DynamicNewtonIterationLogRecord& value) {
return log.Write(value);
};
ReducedShellHhtStep<ReducedStepCandidate> iteration_step(
[&](std::uint32_t iteration) -> StepResult {
record.iteration_index = iteration;
record.stage = "evaluation";
record.outcome = "candidate";
const auto fail = [&](const Status& failure,
const char* stage = "outer") -> StepResult {
return StepResult::Failure(report_failure(failure, stage));
};
ReducedShellTrialRates reduced_rates{
Vector{history.p.Size()}, Vector{history.p.Size()}};
step_integrator.EvaluateRates(
{chart.Value(), history, trial_algorithmic, vpp,
copy_translation_rates, retained_velocity, retained_acceleration,
h, parameters}, reduced_rates.velocity,
reduced_rates.acceleration);
Vector trial_v = std::move(reduced_rates.velocity);
Vector trial_a = std::move(reduced_rates.acceleration);
const auto evaluate_trial =
[&](const Vector& physical_p, const Vector& physical_v,
const Vector& physical_a,
const Vector& root_seed) -> Result<Trial> {
auto root = SolveEmbeddingRoot(
chart.Value(), physical_p, root_seed,
[&](const Vector& q) {
return position_operators(trial_frame, q);
},
tangent_solver);
if (!root.HasValue())
return Result<Trial>::Failure(WithProcedureContext(
domain, root.GetStatus(), "inner-position", increment,
trial_frame.time));
auto rates = step_integrator.ReconstructPhysicalRates(
chart.Value(), root.Value(), physical_v, physical_a,
tangent_solver);
if (!rates.HasValue())
return Result<Trial>::Failure(
WithProcedureContext(domain, rates.GetStatus(), "inner-rates",
increment, trial_frame.time));
auto candidate = trial_frame;
candidate.generalized_coordinates = root.Value().q;
candidate.velocity = rates.Value().velocity;
candidate.acceleration = rates.Value().acceleration;
candidate.algorithmic_acceleration = Vector(dofs.FullDofCount());
for (std::size_t i = 0U;
i < candidate.algorithmic_acceleration.Size(); ++i)
candidate.algorithmic_acceleration[i] =
(1.0 + parameters.alpha) * candidate.acceleration[i] -
parameters.alpha * old.acceleration[i];
candidate.iteration_count = iteration;
auto assembly = EvaluateAssembly(domain, dofs, shells.Value(),
candidate, loads.Value()[1U], h,
parameters, parallel_for, true);
if (!assembly.HasValue())
return Result<Trial>::Failure(
WithProcedureContext(domain, assembly.GetStatus(), "outer",
increment, trial_frame.time));
assembly.Value().frame.time_integration_formulation =
DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
return Result<Trial>::Success(
{std::move(assembly.Value()), std::move(root.Value()),
physical_p, physical_v, physical_a, trial_algorithmic,
std::move(rates.Value().z_velocity),
std::move(rates.Value().z_acceleration)});
};
position_geometry_failure = false;
auto trial = evaluate_trial(trial_p, trial_v, trial_a, seed);
if (!trial.HasValue() && iteration == 0U &&
position_geometry_failure) {
for (std::size_t i = 0U; i < trial_p.Size(); ++i) {
trial_algorithmic[i] =
(history.p[i] - pp[i]) / (parameters.beta * h * h);
trial_p[i] = history.p[i];
chosen_start_p[i] = history.p[i];
coordinate_low[i] = 0.0;
increment_trace[i] = 0.0;
increment_trace_compensation[i] = 0.0;
increment_trace_absolute_sum[i] = 0.0;
}
step_integrator.EvaluateRates(
{chart.Value(), history, trial_algorithmic, vpp, false,
retained_velocity, retained_acceleration, h, parameters},
trial_v, trial_a);
trial = evaluate_trial(trial_p, trial_v, trial_a, seed);
}
if (!trial.HasValue())
return fail(trial.GetStatus());
if (iteration == 0U && !ExactlyEqual(trial_p, history.p)) {
Vector old_algorithmic{trial_p.Size()};
Vector old_velocity{trial_p.Size()};
Vector old_acceleration{trial_p.Size()};
for (std::size_t i = 0U; i < trial_p.Size(); ++i) {
old_algorithmic[i] =
(history.p[i] - pp[i]) / (parameters.beta * h * h);
}
step_integrator.EvaluateRates(
{chart.Value(), history, old_algorithmic, vpp, false,
retained_velocity, retained_acceleration, h, parameters},
old_velocity, old_acceleration);
auto old_trial =
evaluate_trial(history.p, old_velocity, old_acceleration, seed);
if (!old_trial.HasValue())
return fail(old_trial.GetStatus());
const auto reduced_residual = [&](const Trial& value) {
double norm = 0.0;
for (const auto full : dofs.FreeDofs()) {
const double scale = full % 6U < 3U ? length : 1.0;
norm = (std::max)(norm,
scale *
std::abs(
value.assembly.frame.residual[full]));
}
if (!std::isfinite(norm))
return std::numeric_limits<double>::infinity();
return norm;
};
const double old_residual = reduced_residual(old_trial.Value());
const double predictor_residual = reduced_residual(trial.Value());
if (!std::isfinite(old_residual) ||
!std::isfinite(predictor_residual))
return fail(
Failure(domain, "dynamic-invalid-state",
"The scaled initial reduced residual is nonfinite."));
if (old_residual < predictor_residual) {
trial = std::move(old_trial);
trial_p = history.p;
chosen_start_p = history.p;
coordinate_low = Vector(history.p.Size());
increment_trace = Vector(history.p.Size());
increment_trace_compensation = Vector(history.p.Size());
increment_trace_absolute_sum = Vector(history.p.Size());
trial_v = old_velocity;
trial_a = old_acceleration;
trial_algorithmic = old_algorithmic;
}
}
auto candidate = trial.Value().assembly.frame;
auto operators = EvaluateConstraintOperators(
domain, dofs, shells.Value(), trial.Value().assembly,
chart.Value(), zero, zero);
if (!operators.HasValue())
return fail(operators.GetStatus());
ResidualRecord(dofs, candidate, &record);
if (record.residual_pass && record.correction_pass) {
status = CompleteConstraintEvidence(
domain, dofs, trial.Value().assembly, length, null_nodes,
positive_solver, tangent_solver, &candidate);
if (!status.IsOk())
return fail(status, "accepted");
status = CompleteEquilibrium(domain, dofs, trial.Value().assembly,
&candidate);
if (!status.IsOk())
return fail(status, "accepted");
graph_regularity =
CheckGraphRegularity(trial.Value().assembly, chart.Value(),
operators.Value(), tangent_solver);
if (!graph_regularity.HasValue())
return fail(graph_regularity.GetStatus(), "accepted");
status = CheckReducedUpdates(
domain, old, candidate, pp, vpp, trial_p, trial_v, trial_a,
trial.Value().root.z, trial.Value().algebraic_velocity,
trial.Value().algebraic_acceleration, trial_algorithmic,
history.p, history.velocity, history.acceleration,
history.algorithmic_acceleration, increment_trace,
increment_trace_compensation, increment_trace_absolute_sum,
chart.Value(), chosen_start_p, coordinate_low, parameters, h);
if (!status.IsOk())
return fail(status, "accepted");
candidate.external_work = 0.0;
for (std::size_t i = 0U; i < dofs.FullDofCount(); ++i)
candidate.external_work += candidate.external_force[i] *
candidate.generalized_coordinates[i];
candidate.energy_defect = candidate.kinetic_energy +
candidate.physical_strain_energy +
candidate.drilling_energy -
initial_energy - candidate.external_work;
return StepResult::Success(ReducedStepCandidate{
std::move(candidate),
{trial_p, trial_v, trial_a, trial_algorithmic,
trial.Value().root.z}});
}
if (!CanApplyOuterCorrection(iteration)) {
double full_hidden = 0.0;
double analytic_hidden = 0.0;
double inertia_hidden = 0.0;
double consistency = 0.0;
for (std::size_t row = 0U; row < chart.Value().null_nodes.size();
++row) {
const auto node = chart.Value().null_nodes[row];
const auto& null =
trial.Value().assembly.rotations[node].drill_null;
double full = 0.0;
double inertia = 0.0;
for (std::size_t component = 0U; component < 3U; ++component) {
const auto full_dof = 6U * node + 3U + component;
full += null[component] * candidate.residual[full_dof];
inertia += null[component] * candidate.inertia_force[full_dof];
}
const double analytic = operators.Value().rows[row].value;
full_hidden = (std::max)(full_hidden, std::abs(full));
analytic_hidden = (std::max)(analytic_hidden, std::abs(analytic));
inertia_hidden = (std::max)(inertia_hidden, std::abs(inertia));
consistency =
(std::max)(consistency, std::abs(full - analytic - inertia));
}
std::ostringstream message;
const auto maximum = [](const Vector& values) {
double result = 0.0;
for (std::size_t i = 0U; i < values.Size(); ++i)
result = (std::max)(result, std::abs(values[i]));
return result;
};
Vector position_correction{trial_algorithmic.Size()};
Vector velocity_correction{trial_algorithmic.Size()};
for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) {
position_correction[i] =
parameters.beta * h * h * trial_algorithmic[i];
velocity_correction[i] =
parameters.gamma * h * trial_algorithmic[i];
}
message << "HHT exhausted 100 corrections without satisfying every "
"endpoint check; "
<< "stage=outer, outer_iteration=" << iteration
<< ", graph_iteration="
<< trial.Value().root.graph_iteration_count
<< ", graph_residual="
<< trial.Value().root.graph_residual_infinity
<< ", graph_relative="
<< trial.Value().root.graph_residual_relative
<< ", graph_correction="
<< trial.Value().root.graph_correction_infinity
<< ", max_NtE=" << full_hidden
<< ", max_analytic_psi=" << analytic_hidden
<< ", max_NtInertia=" << inertia_hidden
<< ", max_NtE_minus_psi_minus_NtInertia=" << consistency
<< ", max_pp=" << maximum(pp)
<< ", max_beta_h2_trial_Ar=" << maximum(position_correction)
<< ", max_trial_p=" << maximum(trial_p)
<< ", max_vp_predictor=" << maximum(vpp)
<< ", max_gamma_h_trial_Ar=" << maximum(velocity_correction)
<< ", max_trial_v=" << maximum(trial_v)
<< ", max_physical_v=" << maximum(trial.Value().velocity)
<< ".";
return fail(
Failure(domain, "dynamic-nonconvergence", message.str()));
}
status = write_iteration(record);
if (!status.IsOk())
return StepResult::Failure(status);
const double velocity_rate = parameters.gamma / (parameters.beta * h);
const double acceleration_rate =
1.0 / ((1.0 + parameters.alpha) * parameters.beta * h * h);
auto system = AssembleAugmentedReducedSystem(
trial.Value().assembly, chart.Value(), operators.Value(),
velocity_rate, acceleration_rate, length, h);
if (!system.HasValue())
return fail(system.GetStatus());
const bool angular_only = ShouldRefineAngular(
chart.Value(), candidate.free_residual_metrics,
{record.coordinate_correction[0U], record.velocity_correction[0U],
record.acceleration_correction[0U],
record.algorithmic_correction[0U]},
translation_private_algorithmic_pass);
auto selected =
SelectAugmentedSolve(system.Value(), chart.Value(), angular_only);
if (!selected.HasValue())
return fail(selected.GetStatus());
const auto linear_failure = [&](const Status& failure) {
return fail(failure, angular_only ? "angular-newton-linear-solve"
: "full-newton-linear-solve");
};
Vector selected_solution{selected.Value().rhs.Size()};
if (selected_solution.Size() > 0U) {
status = tangent_solver.FactorizeFresh(selected.Value().tangent);
if (!status.IsOk())
return linear_failure(status);
status = tangent_solver.SolveCorrection(selected.Value().rhs,
selected_solution);
if (!status.IsOk())
return linear_failure(status);
status = CheckLinearSolve(selected.Value().tangent,
selected.Value().rhs, selected_solution);
if (!status.IsOk())
return linear_failure(status);
}
auto solution = EmbedAugmentedSolution(
selected.Value(), selected_solution, system.Value().rhs.Size());
if (!solution.HasValue())
return fail(solution.GetStatus());
auto correction = MapAugmentedCorrection(
chart.Value(), system.Value(), solution.Value(), velocity_rate,
acceleration_rate);
if (!correction.HasValue())
return fail(correction.GetStatus());
Vector full_algorithmic{dofs.FullDofCount()};
Vector reduced_algorithmic{correction.Value().p.Size()};
Vector reduced_physical{correction.Value().p.Size()};
for (std::size_t i = 0U; i < full_algorithmic.Size(); ++i)
full_algorithmic[i] =
(1.0 + parameters.alpha) * correction.Value().acceleration[i];
for (std::size_t i = 0U; i < reduced_algorithmic.Size(); ++i) {
reduced_algorithmic[i] =
correction.Value().p[i] / (parameters.beta * h * h);
reduced_physical[i] = acceleration_rate * correction.Value().p[i];
}
CorrectionRecord(correction.Value().coordinate,
correction.Value().velocity,
correction.Value().acceleration, full_algorithmic,
reduced_algorithmic, reduced_physical, chart.Value(),
dofs, length, h, parameters.alpha, &record);
translation_private_algorithmic_pass = true;
for (std::size_t i = 0U; i < chart.Value().p_dofs.size(); ++i)
if (PhysicalCoordinateChannel(chart.Value(), i) == 0U)
translation_private_algorithmic_pass =
translation_private_algorithmic_pass &&
PrivateCorrectionEqual(chart.Value(), i, full_algorithmic,
reduced_algorithmic, reduced_physical,
parameters.alpha);
copy_translation_rates = angular_only;
if (angular_only) {
retained_velocity = trial_v;
retained_acceleration = trial_a;
}
for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) {
if (angular_only &&
PhysicalCoordinateChannel(chart.Value(), i) == 0U)
continue;
trial_algorithmic[i] += reduced_algorithmic[i];
}
for (std::size_t i = 0U; i < trial_p.Size(); ++i) {
if (angular_only &&
PhysicalCoordinateChannel(chart.Value(), i) == 0U)
continue;
AdvanceCoordinate(correction.Value().p[i], &trial_p[i],
&coordinate_low[i]);
}
for (std::size_t i = 0U; i < increment_trace.Size(); ++i) {
if (angular_only &&
PhysicalCoordinateChannel(chart.Value(), i) == 0U)
continue;
AddIncrementTraceTerm(correction.Value().p[i], &increment_trace[i],
&increment_trace_compensation[i]);
increment_trace_absolute_sum[i] +=
std::abs(correction.Value().p[i]);
}
seed = trial.Value().root.z;
for (std::size_t i = 0U; i < seed.Size(); ++i)
seed[i] += correction.Value().z[i];
return StepResult::Success(std::nullopt);
},
kMaximumOuterCorrections);
const Status solved = iteration_step.Solve(algorithm);
if (!solved.IsOk())
return Result<ReducedStepCandidate>::Failure(solved);
if (!iteration_step.HasCandidate()) return Result<ReducedStepCandidate>::Failure(
Failure(domain, "dynamic-nonconvergence", "Dynamic increment was not accepted."));
return Result<ReducedStepCandidate>::Success(
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);
record.stage = "accepted";
record.outcome = "accepted";
status = log.Write(record);
if (status.IsOk())
status = log.Flush();
return status;
});
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
}
return Result<DynamicExecutionResult>::Success({std::move(state), domain.Warnings()});
return Result<DynamicExecutionResult>::Success(
{std::move(state), domain.Warnings()});
}
} // namespace fesa
} // namespace fesa
+13 -1
View File
@@ -3,16 +3,20 @@
#include <cstdint>
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/analysis/dynamic_execution_result.h"
#include "fesa/core/status.h"
namespace fesa {
namespace hht_reduced_internal {
class ShellHhtIntegrator;
constexpr std::uint32_t kMaximumOuterCorrections = 100U;
constexpr bool CanApplyOuterCorrection(const std::uint32_t completed) {
return completed < kMaximumOuterCorrections;
}
} // namespace hht_reduced_internal
class Domain;
class EquilibriumAlgorithm;
class LinearSolver;
class NewtonIterationLogWriter;
class ParallelFor;
@@ -22,6 +26,14 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
const Domain& domain, NewtonIterationLogWriter& log,
const ParallelFor& parallel_for, LinearSolver& positive_solver,
TangentSolver& tangent_solver);
/// @brief Borrows caller iteration policy for the complete production core.
Result<DynamicExecutionResult> ExecuteReducedHht(
const Domain& domain, NewtonIterationLogWriter& log,
const ParallelFor& parallel_for, LinearSolver& positive_solver,
TangentSolver& tangent_solver,
const hht_reduced_internal::ShellHhtIntegrator& integrator,
EquilibriumAlgorithm& algorithm);
} // namespace fesa
#endif // FESA_ANALYSIS_HHT_REDUCED_PROCEDURE_H_
@@ -20,6 +20,7 @@
#include "fesa/elements/mitc4_shell.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/sparse_matrix.h"
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
namespace fesa {
namespace {
@@ -84,7 +85,8 @@ double MaximumEdgeLength(const Domain& domain) {
return maximum;
}
Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain) {
Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain,
const EigenSolver& solver) {
const FrequencyStepDefinition* frequency = domain.Frequency();
if (frequency == nullptr || domain.Steps().Size() != 1U) {
return Result<EigenAnalysisState>::Failure(
@@ -101,6 +103,7 @@ Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain) {
return Result<EigenAnalysisState>::Failure(model.GetStatus());
const ElementFactory factory;
std::vector<std::unique_ptr<Element>> owned;
std::vector<std::unique_ptr<ShellReferenceElement>> references;
ElementView view;
owned.reserve(model.Value().ActiveElements().size());
view.reserve(model.Value().ActiveElements().size());
@@ -108,13 +111,15 @@ Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain) {
auto element = factory.Create(domain.Elements()[index], domain);
if (!element.HasValue())
return Result<EigenAnalysisState>::Failure(element.GetStatus());
if (dynamic_cast<const Mitc4Shell*>(element.Value().get()) == nullptr) {
auto reference = factory.BindShellReference(*element.Value());
if (reference == nullptr) {
return Result<EigenAnalysisState>::Failure(Failure(
"unsupported-frequency-element", std::to_string(index),
"The frequency procedure supports MITC4 shell elements only."));
}
view.emplace_back(*element.Value());
owned.push_back(std::move(element.Value()));
references.push_back(std::move(reference));
}
DofManager dofs;
const Status dof_status = dofs.Build(model.Value(), view);
@@ -158,7 +163,7 @@ Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain) {
std::to_string(definition.MaterialIndex()),
"Every active shell material requires a positive density."));
}
const auto* element = dynamic_cast<const Mitc4Shell*>(owned[shell].get());
const auto& element = references[shell];
auto mass = element->Mass(density[definition.MaterialIndex()]);
auto shell_stiffness = element->Stiffness();
if (!mass.HasValue())
@@ -207,7 +212,7 @@ Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain) {
(material->YoungsModulus() / density[material_id]) /
length / length);
}
auto backend = MklFeastEigenSolver{}.Solve(
auto backend = solver.Solve(
reduction.Value().whitened_operator, request, zero_scale);
if (!backend.HasValue())
return Result<EigenAnalysisState>::Failure(backend.GetStatus());
@@ -403,7 +408,8 @@ Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain) {
Result<EigenAnalysisState> LinearFrequencyProcedureExecutor::Execute(
const Domain& domain) const {
try {
auto result = ExecuteCandidate(domain);
const MklFeastEigenSolver default_solver;
auto result = ExecuteCandidate(domain, solver_ ? *solver_ : default_solver);
if (!result.HasValue()) {
auto diagnostics = result.GetStatus().Diagnostics();
for (auto& diagnostic : diagnostics) {
@@ -142,7 +142,7 @@ Result<LinearStaticExecutionResult> LinearStaticProcedureExecutor::Execute(
}
state.ExternalForce() = std::move(nodal_external.Value());
Result<Vector> rhs = LoadAssembler::EffectiveFreeRhs(
Result<Vector> rhs = EssentialConstraintPolicy{}.EffectiveFreeRhs(
state.ExternalForce(), partitions.Value().k_fc, dofs.PrescribedValues(),
dofs);
if (!rhs.HasValue()) {
+10 -57
View File
@@ -1,6 +1,5 @@
#include "fesa/analysis/nonlinear_analysis_state.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
@@ -9,11 +8,18 @@
#include <utility>
#include <vector>
#include "fesa/core/source_identity.h"
#include "fesa/elements/element_definition.h"
#include "math/finite_values.h"
#include "results/shell_result_recovery.h"
#include "analysis/shell_state_inventory.h"
namespace fesa {
namespace {
using analysis_internal::ShellElementOrder;
using math_internal::IsFinite;
constexpr std::size_t kDofsPerNode = 6U;
constexpr std::size_t kShellLocationsPerElement = 4U;
constexpr std::uint32_t kMaximumNewtonIterations = 100U;
@@ -27,59 +33,6 @@ Status StateFailure(const FailureCategory category, const std::string& code,
message}});
}
/// @brief Compares every preserved source-identity component.
bool SourceIdsMatch(const SourceEntityId& left, const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
template <std::size_t Size>
/// @brief Tests one fixed-size numerical array for finite values.
bool IsFinite(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](const double value) { return std::isfinite(value); });
}
/// @brief Tests each component of a stored trial vector for finiteness.
bool IsFinite(const Vector& values) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return false;
}
}
return true;
}
/// @brief Tests all physical quantities attached to one shell recovery row.
bool IsFinite(const ShellResultRow& row) {
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
!IsFinite(row.section_resultant)) {
return false;
}
for (const auto& axis : row.local_frame) {
if (!IsFinite(axis)) {
return false;
}
}
return std::all_of(row.stress.begin(), row.stress.end(),
[](const ShellSectionStressRow& stress) {
return std::isfinite(stress.zeta) &&
IsFinite(stress.components);
});
}
/// @brief Returns shell Domain indices in deterministic unified element order.
std::vector<EntityIndex> ShellElementOrder(const Domain& domain) {
std::vector<EntityIndex> order;
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) {
order.push_back(static_cast<EntityIndex>(index));
}
}
return order;
}
/// @brief Validates one accepted frame before any committed-state mutation.
Status ValidateAcceptedFrame(const AcceptedFrameState& frame,
const NonlinearTrialState& committed,
@@ -117,7 +70,7 @@ Status ValidateAcceptedFrame(const AcceptedFrameState& frame,
for (std::size_t node = 0U; node < node_count; ++node) {
if (frame.nodal_internal_order[node] !=
committed.nodal_internal_order[node] ||
!SourceIdsMatch(frame.nodal_source_ids[node],
!SourceIdentityEquals(frame.nodal_source_ids[node],
committed.nodal_source_ids[node]) ||
!IsFinite(frame.nodal_triad[node][0U]) ||
!IsFinite(frame.nodal_triad[node][1U]) ||
@@ -179,7 +132,7 @@ Status ValidateAcceptedFrame(const AcceptedFrameState& frame,
if (row.element != shell_order[element] ||
row.location != expected_locations[point] ||
row.natural_coordinates != expected_coordinates[point] ||
!IsFinite(row)) {
!results_internal::IsFiniteShellRow(row)) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Shell result rows do not preserve stable identity.");
}
@@ -241,7 +194,7 @@ Result<NonlinearAnalysisState> NonlinearAnalysisState::Create(
for (std::size_t node = 0U; node < node_count; ++node) {
const auto& entry = baseline.node_baselines[node];
if (entry.node_index != static_cast<EntityIndex>(node) ||
!SourceIdsMatch(entry.source_id, domain.Nodes()[node].source_id) ||
!SourceIdentityEquals(entry.source_id, domain.Nodes()[node].source_id) ||
!IsFinite(entry.triad[0U]) || !IsFinite(entry.triad[1U]) ||
!IsFinite(entry.triad[2U]) || !IsFinite(entry.director)) {
return Result<NonlinearAnalysisState>::Failure(StateFailure(

Some files were not shown because too many files have changed in this diff Show More