Compare commits
28 Commits
6ac474f19b
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 5855604318 | |||
| 6bc7cc3ada | |||
| 8c448ffe45 | |||
| ca0268ea5b | |||
| 675379779f | |||
| b68f6ee143 | |||
| 02e2994c5b | |||
| 4f96719663 | |||
| 2df90b95f8 | |||
| 98c13c2ec2 | |||
| 51939fba5b | |||
| f23deb0ade | |||
| fb67007527 | |||
| 4d04f3dbbe | |||
| 9d37465d92 | |||
| e0a6a6fa70 | |||
| 218cfa9d50 | |||
| 5fe57cb145 | |||
| 5618636f0d | |||
| 7d52247d6c | |||
| ac8d24a2cf | |||
| 6ad9f3cd47 | |||
| 6ee51a8502 | |||
| 127286cf2b | |||
| dc6baed8ed | |||
| 6c2e1f3ab1 | |||
| b9bb439766 | |||
| 40a7e6c3af |
@@ -60,7 +60,7 @@
|
|||||||
- 전단강성이 생략되면 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 Phase 1 기본값으로
|
- 전단강성이 생략되면 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 Phase 1 기본값으로
|
||||||
적용한다.
|
적용한다.
|
||||||
- reference 비교는 metadata 없이 요청된 물리량과 CSV 경로를 명시한다. 현재
|
- reference 비교는 metadata 없이 요청된 물리량과 CSV 경로를 명시한다. 현재
|
||||||
캔틸레버 샘플은 변위와 반력을 비교하며 요소 내력과 도심 응력 비교 루틴은
|
캔틸레버 샘플은 변위, 반력 및 요소 단면력을 비교하며 도심 응력 비교 루틴은
|
||||||
synthetic CSV로 검증한다.
|
synthetic CSV로 검증한다.
|
||||||
- 새 MSVC 빌드 경고를 추가하지 않는다.
|
- 새 MSVC 빌드 경고를 추가하지 않는다.
|
||||||
- 변경은 요청 범위에 한정하고 Conventional Commits 형식의 메시지를 사용한다.
|
- 변경은 요청 범위에 한정하고 Conventional Commits 형식의 메시지를 사용한다.
|
||||||
|
|||||||
+32
-1
@@ -28,6 +28,8 @@ include(cmake/FesaDependencies.cmake)
|
|||||||
add_library(fesa_core STATIC
|
add_library(fesa_core STATIC
|
||||||
src/fesa/analysis/linear_static_analysis.cpp
|
src/fesa/analysis/linear_static_analysis.cpp
|
||||||
src/fesa/analysis/run_solver.cpp
|
src/fesa/analysis/run_solver.cpp
|
||||||
|
src/fesa/assembly/contribution.cpp
|
||||||
|
src/fesa/assembly/parallel_assembler.cpp
|
||||||
src/fesa/assembly/serial_assembler.cpp
|
src/fesa/assembly/serial_assembler.cpp
|
||||||
src/fesa/constraints/essential_bc.cpp
|
src/fesa/constraints/essential_bc.cpp
|
||||||
src/fesa/core/version.cpp
|
src/fesa/core/version.cpp
|
||||||
@@ -45,6 +47,8 @@ add_library(fesa_core STATIC
|
|||||||
src/fesa/model/domain_builder.cpp
|
src/fesa/model/domain_builder.cpp
|
||||||
src/fesa/results/result_database.cpp
|
src/fesa/results/result_database.cpp
|
||||||
src/fesa/solvers/linear/pardiso_linear_solver.cpp
|
src/fesa/solvers/linear/pardiso_linear_solver.cpp
|
||||||
|
src/fesa/validation/comparison.cpp
|
||||||
|
src/fesa/validation/reference_csv.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(fesa_core
|
target_include_directories(fesa_core
|
||||||
@@ -54,7 +58,7 @@ target_include_directories(fesa_core
|
|||||||
|
|
||||||
target_compile_features(fesa_core PUBLIC cxx_std_20)
|
target_compile_features(fesa_core PUBLIC cxx_std_20)
|
||||||
target_compile_options(fesa_core PRIVATE /W4 /permissive- /EHsc)
|
target_compile_options(fesa_core PRIVATE /W4 /permissive- /EHsc)
|
||||||
target_link_libraries(fesa_core PRIVATE MKL::MKL HDF5::HDF5)
|
target_link_libraries(fesa_core PRIVATE MKL::MKL TBB::tbb HDF5::HDF5)
|
||||||
|
|
||||||
add_executable(fesa
|
add_executable(fesa
|
||||||
src/fesa/cli/main.cpp
|
src/fesa/cli/main.cpp
|
||||||
@@ -64,6 +68,33 @@ target_link_libraries(fesa PRIVATE fesa_core)
|
|||||||
target_compile_features(fesa PRIVATE cxx_std_20)
|
target_compile_features(fesa PRIVATE cxx_std_20)
|
||||||
target_compile_options(fesa PRIVATE /W4 /permissive- /EHsc)
|
target_compile_options(fesa PRIVATE /W4 /permissive- /EHsc)
|
||||||
|
|
||||||
|
add_executable(fesa-reference-compare
|
||||||
|
src/fesa/validation/reference_compare_main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(fesa-reference-compare PRIVATE fesa_core)
|
||||||
|
target_compile_features(fesa-reference-compare PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(
|
||||||
|
fesa-reference-compare PRIVATE /W4 /permissive- /EHsc
|
||||||
|
)
|
||||||
|
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
|
add_executable(fesa_assembly_benchmark
|
||||||
|
tests/performance/assembly_benchmark.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(fesa_assembly_benchmark PRIVATE fesa_core)
|
||||||
|
target_compile_features(fesa_assembly_benchmark PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(
|
||||||
|
fesa_assembly_benchmark PRIVATE /W4 /permissive- /EHsc
|
||||||
|
)
|
||||||
|
add_custom_command(
|
||||||
|
TARGET fesa_assembly_benchmark
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"$<TARGET_FILE:TBB::tbb>"
|
||||||
|
"$<TARGET_FILE_DIR:fesa_assembly_benchmark>"
|
||||||
|
)
|
||||||
|
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
+43
-2
@@ -164,7 +164,7 @@ HDF5 파일에 저장하고 root에 schema version을 기록한다.
|
|||||||
|
|
||||||
## ADR-010: 검증 계층별 허용오차
|
## ADR-010: 검증 계층별 허용오차
|
||||||
|
|
||||||
**상태:** Accepted
|
**상태:** Accepted for threshold tests; cross-formulation correlation superseded by ADR-017
|
||||||
|
|
||||||
**상황:** 모든 물리량에 하나의 상대오차를 적용하면 영에 가까운 값이나 서로 다른
|
**상황:** 모든 물리량에 하나의 상대오차를 적용하면 영에 가까운 값이나 서로 다른
|
||||||
규모의 결과를 올바르게 판정할 수 없다.
|
규모의 결과를 올바르게 판정할 수 없다.
|
||||||
@@ -177,6 +177,8 @@ HDF5 파일에 저장하고 root에 schema version을 기록한다.
|
|||||||
- 영에 가까운 값과 큰 값 모두 의미 있게 비교할 수 있다.
|
- 영에 가까운 값과 큰 값 모두 의미 있게 비교할 수 있다.
|
||||||
- 모델별 예외 tolerance에는 문서화된 수치 근거가 필요하다.
|
- 모델별 예외 tolerance에는 문서화된 수치 근거가 필요하다.
|
||||||
- 단일 tolerance보다 comparison request와 helper가 복잡해진다.
|
- 단일 tolerance보다 comparison request와 helper가 복잡해진다.
|
||||||
|
- Abaqus와 FESA의 정식화가 다른 상관성 비교에는 ADR-017의 component별 RMSE와
|
||||||
|
Relative L2 계약을 적용한다.
|
||||||
|
|
||||||
## ADR-011: 일관 단위계와 결과 좌표계
|
## ADR-011: 일관 단위계와 결과 좌표계
|
||||||
|
|
||||||
@@ -257,7 +259,7 @@ flat `Domain`으로 정규화한다. 외부 entity는 `(instance name, part-loca
|
|||||||
|
|
||||||
## ADR-015: 명시적 물리량 선택 기반 CSV 검증
|
## ADR-015: 명시적 물리량 선택 기반 CSV 검증
|
||||||
|
|
||||||
**상태:** Accepted
|
**상태:** Superseded by ADR-017
|
||||||
|
|
||||||
**상황:** 현재 캔틸레버 reference에는 변위와 반력만 있고 per-model metadata는
|
**상황:** 현재 캔틸레버 reference에는 변위와 반력만 있고 per-model metadata는
|
||||||
요구하지 않는다. 요소 내력과 응력 비교 기능은 해당 CSV가 추가되기 전에 구현해야
|
요구하지 않는다. 요소 내력과 응력 비교 기능은 해당 CSV가 추가되기 전에 구현해야
|
||||||
@@ -297,3 +299,42 @@ toolset을 명시한다. CMake, CMake Presets, CTest 및 GoogleTest/GoogleMock
|
|||||||
보장 대상이 아니다.
|
보장 대상이 아니다.
|
||||||
- 컴파일러 갱신에 따른 경고와 표준 라이브러리 동작은 전체 Debug/Release 검증에서
|
- 컴파일러 갱신에 따른 경고와 표준 라이브러리 동작은 전체 Debug/Release 검증에서
|
||||||
다시 확인해야 한다.
|
다시 확인해야 한다.
|
||||||
|
|
||||||
|
## ADR-017: FESA 정식화 적합성과 Abaqus 결과 상관성의 이중 gate
|
||||||
|
|
||||||
|
**상태:** Accepted
|
||||||
|
|
||||||
|
**상황:** FESA의 2절점 Timoshenko Beam은 선택적 감차적분과 `SCF=0`을 사용하고
|
||||||
|
Abaqus B31의 slenderness compensation을 구현하지 않는다. 따라서 `SCF=0.25`인
|
||||||
|
Abaqus 결과에 기본 상대오차 \(10^{-5}\)를 적용하면 FESA 정식화 결함과 의도된
|
||||||
|
정식화 차이를 구분할 수 없다. 현재 캔틸레버 reference에는 변위, 반력 및 요소
|
||||||
|
단면력 CSV가 있다.
|
||||||
|
|
||||||
|
**결정:**
|
||||||
|
|
||||||
|
- FESA 정식화 적합성 gate는 해석해, 에너지, 강체 mode, 평형 및 엄격한 tolerance로
|
||||||
|
FESA 자체 정식화를 검증한다.
|
||||||
|
- Abaqus 결과 상관성 gate는 원본 Abaqus 입력과 CSV를 변경하지 않는다. FESA는
|
||||||
|
동일한 기하·재료·하중과 명시적 전단강성을 사용하되 `SCF=0`인 별도 입력을
|
||||||
|
production parser와 solver로 해석한다.
|
||||||
|
- 상관성 gate는 요청된 모든 entity와 component가 유일하게 매칭되고 유한한
|
||||||
|
component별 RMSE 및 Relative L2가 생성되면 evaluable이다. 서로 다른 정식화에
|
||||||
|
기본 상대오차 \(10^{-5}\) pass/fail을 적용하지 않는다.
|
||||||
|
- Relative L2의 reference norm이 영에 가까우면 해당 component의 characteristic
|
||||||
|
absolute scale norm을 분모 하한으로 사용한다.
|
||||||
|
- 병진과 회전, 힘과 모멘트처럼 단위가 다른 component를 하나의 RMSE 또는 norm에
|
||||||
|
혼합하지 않는다.
|
||||||
|
- Abaqus의 \((\mathbf t,\mathbf n_1,\mathbf n_2)\)와 FESA의
|
||||||
|
\((\mathbf e_x,\mathbf e_y,\mathbf e_z)\)를 각각 일치시킨 모델에서 Abaqus
|
||||||
|
`SF1,SF2,SF3,SM1,SM2,SM3`은 FESA \(N,V_y,V_z,T,M_y,M_z\) 순서로
|
||||||
|
`SF1,SF3,SF2,SM3,SM1,SM2`를 사용한다.
|
||||||
|
- 현재 캔틸레버 상관성 요청은 변위, 반력 및 요소 단면력을 명시한다. 요청하지 않은
|
||||||
|
응력은 통과로 보고하지 않는다.
|
||||||
|
|
||||||
|
**결과와 트레이드오프:**
|
||||||
|
|
||||||
|
- FESA 구현 회귀와 상용 solver와의 모델 상관성을 서로 오인하지 않는다.
|
||||||
|
- Abaqus 원본과 FESA 투영 입력을 함께 관리해야 하며 formulation 차이를
|
||||||
|
`docs/VALIDATION.md`에 기록해야 한다.
|
||||||
|
- 첫 상관성 보고서는 metric을 제시하지만 관측값에 맞춘 acceptance envelope를
|
||||||
|
만들지 않는다. 후속 envelope에는 해석적 또는 mesh study 근거가 필요하다.
|
||||||
|
|||||||
@@ -429,16 +429,17 @@ kernel을 추가한다.
|
|||||||
- `tests/reference`: CSV 골든 결과와 FESA HDF5 결과 비교
|
- `tests/reference`: CSV 골든 결과와 FESA HDF5 결과 비교
|
||||||
- `reference/<model-id>`: Abaqus 입력과 현재 사용할 수 있는 결과 CSV
|
- `reference/<model-id>`: Abaqus 입력과 현재 사용할 수 있는 결과 CSV
|
||||||
|
|
||||||
reference comparison request가 비교할 물리량과 CSV 경로, 상대 tolerance 및
|
reference comparison request가 비교할 물리량과 CSV 경로 및 물리량별 절대 scale을
|
||||||
물리량별 절대 scale을 명시한다. 요청한 CSV가 없으면 실패하며 요청하지 않은 결과를
|
명시한다. 요청한 CSV가 없으면 실패하며 요청하지 않은 결과를 통과로 표시하지
|
||||||
통과로 표시하지 않는다. 현재 캔틸레버는 변위와 반력만 요청하고, 요소 내력과
|
않는다. 현재 캔틸레버는 변위, 반력 및 요소 단면력을 요청하고, 단면 도심 응력
|
||||||
단면 도심 응력 adapter는 synthetic CSV로 검증한다.
|
adapter는 synthetic CSV로 검증한다.
|
||||||
|
|
||||||
요소 내력 CSV의 `(Instance, Element Label, Node Label)` 위치에서
|
요소 내력 CSV의 `(Instance, Element Label, Node Label)` 위치에서
|
||||||
`SF1,SF2,SF3,SM1,SM2,SM3`을 \(N,V_y,V_z,T,M_y,M_z\)로 매핑한다. 응력 CSV의
|
`SF1,SF3,SF2,SM3,SM1,SM2`를 \(N,V_y,V_z,T,M_y,M_z\)로 매핑한다. 응력 CSV의
|
||||||
같은 위치에 있는 `Sxx`는 단면 도심값 \(N/A\)와 비교한다. 단일 Instance에서는
|
같은 위치에 있는 `Sxx`는 단면 도심값 \(N/A\)와 비교한다. 단일 Instance에서는
|
||||||
Instance 열 생략을 허용하되 comparison request가 제공한 Instance 이름으로
|
Instance 열 생략을 허용하되 comparison request가 제공한 Instance 이름으로
|
||||||
보완한다.
|
보완한다. Abaqus와 FESA의 정식화가 다른 상관성 비교는 component별 RMSE와
|
||||||
|
Relative L2를 보고하며 관측값으로 만든 pass/fail tolerance를 적용하지 않는다.
|
||||||
|
|
||||||
reference helper는 반드시 public parser와 analysis 경로로 FESA 결과를 생성한다.
|
reference helper는 반드시 public parser와 analysis 경로로 FESA 결과를 생성한다.
|
||||||
테스트 전용 경로로 Domain이나 matrix를 직접 주입해 전체 파이프라인 결함을 숨기지
|
테스트 전용 경로로 Domain이나 matrix를 직접 주입해 전체 파이프라인 결함을 숨기지
|
||||||
|
|||||||
+296
-431
@@ -1,459 +1,324 @@
|
|||||||
# FESA Session Handoff
|
# FESA Session Handoff
|
||||||
|
|
||||||
## 1. 문서 목적
|
## 1. 목적과 기준 문서
|
||||||
|
|
||||||
이 문서는 results-and-pipeline과 abaqus-subset-completion 완료 후 새 세션에서
|
이 문서는 `beam-reference-qualification` 완료 후 새 세션에서 마지막 Phase 1 단계인
|
||||||
deterministic-parallel-assembly Phase를 바로 시작하기 위한 인수인계 기록이다.
|
`internal-release`를 시작하기 위한 인수인계 기록이다. 과거 phase의 구현 역사를
|
||||||
요구사항과 설계의 기준은 이 문서가 아니라 다음 파일이다.
|
반복하기보다 현재 기준선, 검증 결과, 남은 작업과 실행 순서를 제공한다.
|
||||||
|
|
||||||
- /AGENTS.md
|
다음 문서를 우선 기준으로 사용한다.
|
||||||
- /docs/PRD.md
|
|
||||||
- /docs/ARCHITECTURE.md
|
- `AGENTS.md`
|
||||||
- /docs/ADR.md
|
- `docs/PRD.md`
|
||||||
- /docs/HARNESS.md
|
- `docs/ARCHITECTURE.md`
|
||||||
- /docs/ABAQUS_INPUT_SUBSET.md
|
- `docs/ADR.md`
|
||||||
- /docs/HDF5_SCHEMA.md
|
- `docs/HARNESS.md`
|
||||||
- /phases/deterministic-parallel-assembly/index.json
|
- `docs/HDF5_SCHEMA.md`
|
||||||
- /phases/deterministic-parallel-assembly/step0.md부터 step2.md
|
- `docs/ABAQUS_INPUT_SUBSET.md`
|
||||||
|
- `docs/formulation/timoshenko-beam-3d.md`
|
||||||
내용이 충돌하면 AGENTS.md, 제품·아키텍처 문서와 phases/의 현재 상태를 우선한다.
|
- `docs/VALIDATION.md`
|
||||||
이 문서는 현재 구현, 검증 baseline과 실행환경에서 특히 놓치기 쉬운 계약을
|
- `phases/internal-release/index.json`
|
||||||
보충한다.
|
- `phases/internal-release/step0.md`부터 `step4.md`
|
||||||
|
|
||||||
## 2. 현재 저장소 상태
|
내용이 충돌하면 `AGENTS.md`, PRD/ADR/아키텍처, 완료된 phase metadata와 실제
|
||||||
|
테스트를 우선한다. 특히 `internal-release`의 기존 Step 0과 Step 4에 남아 있는
|
||||||
2026-08-01 확인 기준:
|
reference 범위 설명은 4절의 최신 계약으로 바로잡은 뒤 release evidence에 사용한다.
|
||||||
|
|
||||||
- 기준 브랜치: dev
|
## 2. Git과 Phase 상태
|
||||||
- 이 HANDOFF 작성 직전 구현 HEAD: cbb621bb282a301b6708d95ff697b40644188e46
|
|
||||||
- cbb621b는 abaqus-subset-completion의 최종 보완 commit이다.
|
2026-08-03 기준 구현 상태는 다음과 같다.
|
||||||
- 이 문서는 cbb621b 다음 commit으로 dev에 기록하고 origin/dev에 함께 push한다.
|
|
||||||
새 세션에서는 아래 명령으로 실제 동기화 상태를 다시 확인한다.
|
- 기준 브랜치: `dev`
|
||||||
- 완료 Phase:
|
- 이 문서 갱신 직전 구현 HEAD: `6bc7cc3adadded6f1a2a0ed45449b2264c0f7a4e`
|
||||||
- solver-bootstrap
|
- `feat-beam-reference-qualification`은 `dev`에 fast-forward 병합된 뒤 로컬에서
|
||||||
- domain-and-input-skeleton
|
삭제됐다.
|
||||||
- fem-and-beam-kernel
|
- `internal-release`를 제외한 `phases/index.json`의 모든 phase가 `completed`다.
|
||||||
- equation-and-linear-solve
|
- 다음 Phase: `internal-release`
|
||||||
- results-and-pipeline
|
- 다음 Step: Step 0 `release-checklist`
|
||||||
- abaqus-subset-completion
|
- Step 0부터 Step 4까지 모두 `pending`이다.
|
||||||
- 다음 Phase: deterministic-parallel-assembly
|
- 이 문서의 갱신은 구현 기준선 다음의 docs-only commit이다.
|
||||||
- 다음 Step: 0 - canonical-contribution-order
|
|
||||||
- deterministic-parallel-assembly의 Step 0~2는 모두 pending이다.
|
새 세션에서는 기록된 hash를 강제로 맞추지 말고 로컬과 원격의 실제 상태를 먼저
|
||||||
- 후속 Phase인 result-contract-completion, beam-reference-qualification,
|
확인한다. 이 HANDOFF commit이 push된 뒤에는 `dev`와 `origin/dev`가 같은 commit을
|
||||||
internal-release도 아직 pending이다.
|
가리켜야 한다.
|
||||||
- feat-abaqus-subset-completion은 dev에 fast-forward 병합된 뒤 삭제되었다.
|
|
||||||
- 이 문서 갱신 전 작업 트리는 clean이었다.
|
```powershell
|
||||||
|
git switch dev
|
||||||
새 세션에서 원격에 맞추기 위한 reset, rebase, force push를 수행하지 않는다.
|
git status --short --branch
|
||||||
먼저 다음 상태를 확인하고 dev와 origin/dev가 다르면 원인을 조사한다.
|
git rev-parse HEAD
|
||||||
|
git rev-parse origin/dev
|
||||||
git switch dev
|
git rev-list --left-right --count origin/dev...dev
|
||||||
git status --short --branch
|
```
|
||||||
git rev-parse HEAD
|
|
||||||
git rev-parse origin/dev
|
reset, rebase 또는 force push로 차이를 숨기지 않는다. 예상하지 못한 변경이 있으면
|
||||||
git rev-list --left-right --count origin/dev...dev
|
소유자를 확인하고 보존한다.
|
||||||
|
|
||||||
## 3. 완료된 results-and-pipeline
|
## 3. 완료된 Phase 1 기준선
|
||||||
|
|
||||||
Phase metadata는 /phases/results-and-pipeline/index.json에 기록되어 있으며 Step
|
현재 production 경로는 다음 기능을 제공한다.
|
||||||
0~3이 모두 completed다.
|
|
||||||
|
- Abaqus `.inp` 제한 부분집합의 flat/orphan mesh 또는 좌표변환이 없는 단일
|
||||||
주요 결과:
|
Part/Assembly/Instance 모델 파싱과 semantic validation
|
||||||
|
- 2절점 3D Isoparametric Timoshenko Beam, 등방성 선형 탄성, 일반 단면,
|
||||||
- HDF5 API에 의존하지 않는 ResultDatabase, ResultStep, ResultFrame, NodalFrame
|
`*BOUNDARY`, `*CLOAD`, 단일 선형 정적 Step
|
||||||
semantic model과 results-stage validation
|
- MSVC v145, Intel oneAPI MKL PARDISO와 TBB를 사용하는 Windows x64 해석 경로
|
||||||
- /docs/HDF5_SCHEMA.md의 schema 1.0.0 계약
|
- canonical contribution ordering에 기반한 결정론적 병렬 요소 평가와 조립
|
||||||
- move-only HDF5 RAII writer와 public reader round trip
|
- 원래 full equation에서 변위, 반력과 평형을 복구하는 선형 정적 해석
|
||||||
- serial assembly, essential BC, PARDISO, full reconstruction과 reaction recovery를
|
- 두 요소 끝의 section strain, section force, centroid/recovery-point `Sxx` 회복
|
||||||
조율하는 LinearStaticAnalysis
|
- node/element provenance, component와 좌표계 metadata를 포함한 완전한
|
||||||
- free equation이 0인 all-constrained 해석의 PARDISO 우회
|
`ResultDatabase`
|
||||||
- parser부터 HDF5 writer까지 연결하는 run_solver와 solve CLI
|
- 입력 원본 없이 model, analysis와 결과를 재구성할 수 있는 HDF5 schema `2.0.0`
|
||||||
- fesa solve <model.inp> --output <results.h5> 수직 파이프라인
|
- 변위, 반력, 요소 단면력용 Abaqus CSV adapter와 component별 상관성 보고 CLI
|
||||||
|
|
||||||
관련 파일:
|
결과 계약과 Beam reference phase의 세부 완료 기록은 다음 파일에 있다.
|
||||||
|
|
||||||
- /include/fesa/results/
|
- `phases/result-contract-completion/index.json`
|
||||||
- /src/fesa/results/
|
- `phases/beam-reference-qualification/index.json`
|
||||||
- /include/fesa/io/hdf5/
|
- `docs/HDF5_SCHEMA.md`
|
||||||
- /src/fesa/io/hdf5/
|
- `docs/VALIDATION.md`
|
||||||
- /include/fesa/analysis/
|
|
||||||
- /src/fesa/analysis/
|
`internal-release`에서는 위 solver 계약을 다시 설계하지 않는다. 설치, clean consumer,
|
||||||
- /include/fesa/analysis/run_solver.hpp
|
scale measurement와 release evidence에 필요한 최소 변경만 수행한다.
|
||||||
- /src/fesa/analysis/run_solver.cpp
|
|
||||||
- /docs/HDF5_SCHEMA.md
|
## 4. 검증 상태와 남은 제한
|
||||||
- /tests/integration/pipeline/minimal_cantilever_test.cpp
|
|
||||||
|
### 4.1 이중 검증 gate
|
||||||
핵심 계약:
|
|
||||||
|
`docs/VALIDATION.md`가 검증 결과의 단일 상세 보고서다.
|
||||||
- results semantic model은 HDF5 타입이나 handle을 노출하지 않는다.
|
|
||||||
- 모든 HDF5 resource는 adapter 내부의 move-only RAII wrapper가 소유한다.
|
- Gate A — FESA 정식화 적합성: **PASS**
|
||||||
- nodal result의 node ID와 6성분 displacement/reaction 배열 순서는 DofManager의
|
- Gate B — Abaqus 결과 상관성: **EVALUABLE**
|
||||||
full-vector 순서와 일치한다.
|
|
||||||
- 반력은 reduced system이 아니라 원래 full system의 r=Ku-f에서 계산한다.
|
Gate A는 해석해, strain energy, 강체 mode, 회전 불변성, 평형과 결정성을 엄격한
|
||||||
- all-constrained system은 유효한 analysis case이며 PARDISO order 0 입력으로
|
tolerance로 검증한다. Gate B는 Abaqus B31과 FESA 정식화의 차이를 인정하고
|
||||||
전달하지 않는다.
|
component별 RMSE와 Relative L2를 보고한다. Gate B의 `EVALUABLE`은 모든 요청
|
||||||
- CLI와 run_solver는 parser, semantic mapper, analysis와 writer를 조율하는
|
entity/component가 유일하게 매칭되고 metric이 유한하다는 뜻이며, 임의의 상관성
|
||||||
application orchestration 경계다. LinearStaticAnalysis 자체는 parser나 HDF5를
|
pass/fail threshold를 통과했다는 뜻은 아니다.
|
||||||
호출하지 않는다.
|
|
||||||
- CTest의 CLI test에는 설치된 oneAPI/HDF5 runtime PATH가 test property로
|
현재 cantilever reference의 주요 Relative L2는 다음과 같다.
|
||||||
전달된다. 기본 셸 PATH에 해당 디렉터리가 없어도 CTest가 성공해야 한다.
|
|
||||||
|
| 결과 | Component | Relative L2 |
|
||||||
이 vertical slice 완료는 요소 결과 계약, Abaqus reference 자격 또는 내부 배포
|
|---|---|---:|
|
||||||
완료를 의미하지 않는다.
|
| displacement | `Uz` | `0.0022349969291714038` |
|
||||||
|
| displacement | `Ry` | `0.0000010416581667076092` |
|
||||||
## 4. 완료된 abaqus-subset-completion
|
| reaction | `RFz` | `4.4393520322089023e-13` |
|
||||||
|
| reaction | `RMy` | `2.591919334788851e-13` |
|
||||||
Phase metadata는 /phases/abaqus-subset-completion/index.json에 기록되어 있으며
|
| internal force | `Vz` | `2.7107472798172426e-13` |
|
||||||
Step 0~4가 모두 completed다.
|
| internal force | `My` | `0.082541022764834646` |
|
||||||
|
|
||||||
주요 결과:
|
18개 전체 component의 count, RMSE와 Relative L2는 `docs/VALIDATION.md`를 사용한다.
|
||||||
|
|
||||||
- /docs/ABAQUS_INPUT_SUBSET.md에 Phase 1 입력 계약을 명문화
|
### 4.2 Reference 모델과 축 매핑
|
||||||
- public parser/mapper fixture matrix를 74 cases로 확대
|
|
||||||
- strict keyword scope, parameter form, data ownership과 정확한 source diagnostic
|
- Abaqus provenance는 `reference/cantilever beam/cantilever beam.inp`와 세 CSV다.
|
||||||
- Part/Assembly의 명시적, GENERATE, nested, forward set resolution
|
- Abaqus 원본 모델의 `SCF=0.25`는 보존한다.
|
||||||
- flat mesh 또는 좌표변환 없는 단일 Part/Assembly/Instance 선택
|
- FESA projection은 `reference/cantilever beam/cantilever beam fesa.inp`이며 다른
|
||||||
- 전역 material, Part-local Beam section과 ELSET assignment
|
의미 입력을 유지하고 `SCF=0`을 사용한다.
|
||||||
- 명시적 transverse shear와 Phase 1 기본값 Asy=Asz=5A/6, SCF=0
|
- FESA에 Abaqus SCF 보정이나 결과 맞춤 계수를 추가하지 않는다.
|
||||||
- 단일 Step/Static, Boundary, Cload와 명시적 no-op directive
|
|
||||||
- active Part뿐 아니라 inactive Part의 NODE, ELEMENT, section record와 reference
|
|
||||||
유효성 검증
|
|
||||||
|
|
||||||
관련 파일:
|
|
||||||
|
|
||||||
- /docs/ABAQUS_INPUT_SUBSET.md
|
|
||||||
- /include/fesa/io/abaqus/active_input.hpp
|
|
||||||
- /include/fesa/io/abaqus/set_resolver.hpp
|
|
||||||
- /src/fesa/io/abaqus/parser.cpp
|
|
||||||
- /src/fesa/io/abaqus/active_input.cpp
|
|
||||||
- /src/fesa/io/abaqus/set_resolver.cpp
|
|
||||||
- /src/fesa/io/abaqus/semantic_mapper.cpp
|
|
||||||
- /tests/fixtures/abaqus/contract.tsv
|
|
||||||
- /tests/unit/io/abaqus/
|
|
||||||
- /tests/integration/io/minimal_deck_to_domain_test.cpp
|
|
||||||
|
|
||||||
특히 유지할 계약:
|
|
||||||
|
|
||||||
- 사용되지 않는 Part는 파싱·record/reference validation하지만 Domain에는 넣지 않는다.
|
|
||||||
- NODE와 ELEMENT data row의 field 수는 정확해야 한다.
|
|
||||||
- enum 값 B31과 GENERAL은 대소문자를 구분하지 않는다.
|
|
||||||
- 계층형 Step의 Boundary/Cload target은 Assembly node set으로 해석한다.
|
|
||||||
- 모든 active B31 element만 정확히 하나의 section assignment를 가져야 한다.
|
|
||||||
inactive Part의 완전한 해석 가능성을 요구하도록 범위를 넓히지 않는다.
|
|
||||||
- missing_section diagnostic은 element keyword가 아니라 해당 element data row를
|
|
||||||
source로 사용한다.
|
|
||||||
- unsupported keyword/parameter를 일반 ignore 경로로 숨기지 않는다.
|
|
||||||
- Heading, Preprint, Restart, Output만 문서화된 조건에서 no-op으로 허용한다.
|
|
||||||
|
|
||||||
최종 독립 review에서 Critical, Important, Minor finding은 모두 0건이었다.
|
|
||||||
|
|
||||||
## 5. 현재 serial assembly oracle
|
|
||||||
|
|
||||||
다음 Phase가 변경할 핵심 코드는 현재 /src/fesa/assembly/serial_assembler.cpp 한
|
|
||||||
파일에 private helper로 모여 있다.
|
|
||||||
|
|
||||||
현재 흐름:
|
|
||||||
|
|
||||||
1. 모든 full DOF의 diagonal을 포함하는 upper-triangle CSR sparsity pattern 생성
|
|
||||||
2. 각 Beam 요소의 compute_beam3d2 호출
|
|
||||||
3. local upper triangle 78개를 NumericContribution으로 수집
|
|
||||||
4. row, column, EntityOrigin, local_order 순으로 정렬
|
|
||||||
5. 같은 row/column을 고정된 순서로 합산
|
|
||||||
6. nodal load를 full force vector로 조립
|
|
||||||
|
|
||||||
관련 public 계약:
|
|
||||||
|
|
||||||
- /include/fesa/assembly/symmetric_csr.hpp
|
|
||||||
- /include/fesa/assembly/equation_system.hpp
|
|
||||||
- /include/fesa/assembly/serial_assembler.hpp
|
|
||||||
- /src/fesa/assembly/serial_assembler.cpp
|
|
||||||
- /tests/unit/assembly/serial_assembler_test.cpp
|
|
||||||
|
|
||||||
현재 테스트가 고정하는 oracle:
|
|
||||||
|
|
||||||
- SymmetricCsr는 0-based upper triangle만 저장한다.
|
|
||||||
- row_offsets와 column_indices는 유효하며 각 row의 column이 strictly increasing이다.
|
|
||||||
- 연결되지 않은 node DOF도 값 0의 diagonal entry를 갖는다.
|
|
||||||
- element storage order와 external label 순열이 결과를 바꾸지 않는다.
|
|
||||||
- 1 + 1 + 1e16 규모의 contribution은 EntityOrigin 순으로 합산한 bit pattern을
|
|
||||||
고정한다.
|
|
||||||
- Beam kernel failure를 0 contribution으로 바꾸지 않고 assembly failure로
|
|
||||||
전달한다.
|
|
||||||
- full force vector의 load accumulation도 기존 결과와 같아야 한다.
|
|
||||||
|
|
||||||
parallel 구현을 이유로 이 serial oracle을 먼저 변경하거나 tolerance 비교로
|
|
||||||
약화하지 않는다.
|
|
||||||
|
|
||||||
## 6. 검증된 baseline과 개발환경
|
|
||||||
|
|
||||||
2026-08-01 현재 확인한 도구:
|
|
||||||
|
|
||||||
- CMake 4.4.0
|
|
||||||
- MSBuild 18.8.2.30814
|
|
||||||
- Visual Studio 2026 MSVC v145, Windows x64
|
|
||||||
- codex-cli 0.146.0
|
|
||||||
- Intel oneAPI MKL/TBB 2026.1
|
|
||||||
- HDF5 2.1.1
|
|
||||||
- GoogleTest 1.17.0, v145 x64 CRT build
|
|
||||||
|
|
||||||
새 PowerShell 세션에서 configure 또는 Harness 실행 전에 다음 환경 변수를 설정한다.
|
|
||||||
절대경로를 tracked CMake 파일이나 Preset에 넣지 않는다.
|
|
||||||
|
|
||||||
$env:MKL_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\mkl"
|
|
||||||
$env:TBB_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\tbb"
|
|
||||||
$env:HDF5_DIR = "C:\Program Files\HDF_Group\HDF5\2.1.1\cmake"
|
|
||||||
$env:GTest_DIR = "C:\Users\baram\AppData\Local\FESA\dependencies\googletest-1.17.0-v145-x64-crt\lib\cmake\GTest"
|
|
||||||
|
|
||||||
Test-Path "$env:MKL_DIR\MKLConfig.cmake"
|
|
||||||
Test-Path "$env:TBB_DIR\TBBConfig.cmake"
|
|
||||||
Test-Path "$env:HDF5_DIR\hdf5-config.cmake"
|
|
||||||
Test-Path "$env:GTest_DIR\GTestConfig.cmake"
|
|
||||||
|
|
||||||
네 package config와 다음 h5ls 경로가 존재함을 확인했다.
|
|
||||||
|
|
||||||
C:\Program Files\HDF_Group\HDF5\2.1.1\bin\h5ls.exe
|
|
||||||
|
|
||||||
검증 명령:
|
|
||||||
|
|
||||||
cmake --build --preset windows-debug
|
|
||||||
ctest --preset windows-debug --output-on-failure
|
|
||||||
uv run --with pytest python -m pytest -v -rs
|
|
||||||
|
|
||||||
현재 baseline:
|
|
||||||
|
|
||||||
- MSVC Debug build 성공, 새 warning 없음
|
|
||||||
- CTest 49개 중 49개 성공
|
|
||||||
- Harness pytest 20개 중 20개 성공
|
|
||||||
- pytest가 실제 20개를 수집했으므로 0-test 성공이 아님
|
|
||||||
- 최소 cantilever CLI solve와 HDF5 public schema inspection 성공
|
|
||||||
|
|
||||||
CMake cache가 없거나 package 경로가 바뀐 경우에만 같은 환경 변수 세션에서 먼저
|
|
||||||
다음을 실행한다.
|
|
||||||
|
|
||||||
cmake --fresh --preset windows-debug
|
|
||||||
|
|
||||||
h5ls를 직접 실행할 때는 HDF5 DLL 외에 Intel libmmd.dll이 필요하다. HDF5 bin만
|
|
||||||
PATH에 추가하면 Windows exit 0xC0000135가 발생할 수 있으므로 두 runtime
|
|
||||||
디렉터리를 현재 세션 PATH에 추가한다. 시스템 PATH는 영구 변경하지 않는다.
|
|
||||||
|
|
||||||
$env:PATH = @(
|
|
||||||
"C:\Program Files\HDF_Group\HDF5\2.1.1\bin",
|
|
||||||
"C:\Program Files (x86)\Intel\oneAPI\2026.1\bin",
|
|
||||||
$env:PATH
|
|
||||||
) -join ";"
|
|
||||||
h5ls --version
|
|
||||||
|
|
||||||
## 7. 다음 Phase 목표와 Step 순서
|
요소력 CSV는 다음 순서로 매핑한다.
|
||||||
|
|
||||||
deterministic-parallel-assembly의 목표는 oneTBB로 요소 계산을 병렬화하면서
|
| FESA | Abaqus CSV |
|
||||||
serial oracle과 thread count 사이의 CSR, RHS와 최종 해석 결과를 bit-for-bit
|
|---|---|
|
||||||
동일하게 유지하는 것이다.
|
| `N` | `SF1` |
|
||||||
|
| `Vy` | `SF3` |
|
||||||
### Step 0 - canonical-contribution-order
|
| `Vz` | `SF2` |
|
||||||
|
| `T` | `SM3` |
|
||||||
- serial assembly를 변경하지 않은 상태에서 병렬 경로가 공유할
|
| `My` | `SM1` |
|
||||||
MatrixContribution과 canonical merge contract를 분리한다.
|
| `Mz` | `SM2` |
|
||||||
- 입력 순열, 같은 row/column의 여러 element, cancellation과 signed zero를 포함한
|
|
||||||
실패 테스트를 먼저 작성한다.
|
### 4.3 아직 완료되지 않은 검증
|
||||||
- 정렬 key는 row, column, stable element identity, local_order다.
|
|
||||||
- canonicalize_contributions와 merge_contributions는 하나의 고정 total order와
|
- 실제 Abaqus 결과로 검증된 물리량은 변위, 반력과 요소 단면력이다.
|
||||||
하나의 merge 구현만 가져야 한다.
|
- Abaqus 도심 응력 CSV는 제공되지 않았다. `StressCsv`는 synthetic fixture로 schema와
|
||||||
- 아직 TBB code를 추가하지 않는다.
|
entity mapping만 검증했으며 응력은 **not yet Abaqus-qualified**다.
|
||||||
|
- synthetic internal-force fixture도 adapter 단위 검증에 계속 사용하지만, 요소
|
||||||
Focused acceptance:
|
단면력 자체는 별도의 실제 Abaqus golden CSV와 상관성 비교가 완료됐다.
|
||||||
|
- 내부 배포용 Release build, install tree, clean consumer smoke test와 약 100,000 DOF
|
||||||
cmake --build --preset windows-debug
|
측정은 아직 실행되지 않았다. 증거 없이 완료 표시하지 않는다.
|
||||||
ctest --preset windows-debug -R "CanonicalContribution|DeterministicMerge" --output-on-failure
|
|
||||||
ctest --preset windows-debug --output-on-failure
|
### 4.4 `internal-release` Step 문서의 계약 불일치
|
||||||
|
|
||||||
### Step 1 - tbb-element-evaluation
|
`phases/internal-release/step0.md`와 `step4.md`에는 beam reference phase 이전의 문구가
|
||||||
|
남아 있다.
|
||||||
- oneTBB는 독립적인 Beam element evaluation에만 사용한다.
|
|
||||||
- worker는 thread-local contribution을 만들며 공유 CSR values에 쓰지 않는다.
|
- “현재 Abaqus displacement/reaction comparison”은 변위·반력·요소 단면력
|
||||||
- merge는 Step 0의 canonical serial 순서를 그대로 사용한다.
|
correlation으로 갱신해야 한다.
|
||||||
- AssemblyOptions의 max_threads와 grain_size로 실행을 제한한다.
|
- “내력·응력은 synthetic coverage”라는 묶음 표현은 요소 단면력과 응력을 분리해야
|
||||||
- max_threads=1과 2 이상에서 serial/parallel CSR와 force를 bit-for-bit 비교한다.
|
한다. 요소 단면력은 real golden correlation과 synthetic adapter coverage가 모두
|
||||||
- PARDISO를 TBB task 안에서 호출하지 않는다.
|
있고, 응력만 synthetic adapter coverage다.
|
||||||
|
|
||||||
Focused acceptance:
|
새 세션은 Harness 실행 전에 이 두 Step 문서와 관련 checklist 문구를 현재
|
||||||
|
`docs/PRD.md` 8절 및 `docs/VALIDATION.md`와 일치시켜야 한다. 이 정렬은 검증 범위의
|
||||||
cmake --build --preset windows-debug
|
확장이 아니라 이미 완료된 증거를 정확히 기술하는 작업이다.
|
||||||
ctest --preset windows-debug -R "ParallelAssembly|TbbElementEvaluation" --output-on-failure
|
|
||||||
ctest --preset windows-debug --output-on-failure
|
## 5. 다음 Phase: `internal-release`
|
||||||
|
|
||||||
### Step 2 - thread-count-determinism
|
Phase metadata는 `phases/internal-release/index.json`에 있다. Step은 순서대로 실행한다.
|
||||||
|
|
||||||
- thread count 1, 2, available concurrency에서 CSR, RHS, displacement와 reaction을
|
### Step 0 — `release-checklist`
|
||||||
bit-for-bit 비교한다.
|
|
||||||
- 최소 10회 반복해 scheduling 변화 회귀를 검사한다.
|
- `docs/BUILDING.md`, `docs/INPUT_FORMAT.md`, `docs/RELEASE_CHECKLIST.md`를 작성한다.
|
||||||
- 측정용 fesa_assembly_benchmark를 추가해 serial/parallel 시간과 element count를
|
- PRD 8절의 각 내부 배포 기준에 고유 checklist ID를 부여한다.
|
||||||
출력한다.
|
- 실제 target, preset, example과 evidence command를 CMake에서 재확인한다.
|
||||||
- benchmark speedup은 환경 의존적이므로 pass 조건으로 만들지 않는다.
|
- 미실행 Release/install/benchmark 항목을 완료 표시하지 않는다.
|
||||||
- assembly test 동안 MKL thread 수를 중첩해 키우지 않는다.
|
- 4.4절의 stale reference 문구를 먼저 바로잡는다.
|
||||||
|
|
||||||
Focused acceptance:
|
### Step 1 — `cmake-install-package`
|
||||||
|
|
||||||
cmake --build --preset windows-debug
|
- `cmake --install`로 내부 배포용 install tree를 만든다.
|
||||||
ctest --preset windows-debug -R ThreadCountDeterminism --output-on-failure
|
- CLI, `fesa_core`, public headers, CMake package config, example, schema/input/validation
|
||||||
.\out\build\windows-debug\Debug\fesa_assembly_benchmark.exe
|
문서와 runtime DLL inventory를 포함한다.
|
||||||
ctest --preset windows-debug --output-on-failure
|
- install config에 absolute build path가 남는 실패 검사를 먼저 작성한다.
|
||||||
|
- installer, registry write, 외부 dependency 다운로드와 public ABI 약속은 범위 밖이다.
|
||||||
## 8. 구현 전에 정렬할 설계점
|
|
||||||
|
### Step 2 — `install-tree-smoke-test`
|
||||||
아래 항목은 범위를 늘리라는 의미가 아니다. Step 계약과 현재 serial oracle 사이에서
|
|
||||||
구현 전에 결정하고 테스트로 고정할 최소 질문이다.
|
- source/build tree를 참조하지 않는 consumer configure/link test를 만든다.
|
||||||
|
- 설치된 CLI의 `--version`, example solve와 생성 HDF5 open을 검증한다.
|
||||||
1. Stable element identity
|
- 개발 PATH의 `fesa.exe`나 source include fallback으로 결함을 숨기지 않는다.
|
||||||
- Step 0 초안의 MatrixContribution은 ElementId를 제시한다.
|
|
||||||
- 현재 oracle은 EntityOrigin의 instance_name, local_label, part_name 순으로
|
### Step 3 — `phase1-scale-benchmark`
|
||||||
정렬한다.
|
|
||||||
- ElementId만으로 기존 bit pattern과 storage-order 독립성을 보존할 수 있는지
|
- 약 100,000 DOF Beam chain에서 generation/parsing, assembly, PARDISO solve,
|
||||||
먼저 확인한다. 확인 없이 oracle의 정렬 key를 바꾸지 않는다.
|
recovery와 HDF5 write 시간을 분리해 측정한다.
|
||||||
|
- 재현 가능한 Windows memory metric, thread/solver 설정과 실제 DOF 수를 기록한다.
|
||||||
2. Canonical API와 serial 경로
|
- finite result, 평형과 정상 종료는 검사하되 임의 시간·speedup 기준은 만들지 않는다.
|
||||||
- Step 0은 serial assembly를 변경하지 말라고 요구하면서 공통 merge contract를
|
|
||||||
분리한다.
|
### Step 4 — `release-evidence-gate`
|
||||||
- 먼저 public/internal 경계를 최소화하고, 기존 assemble_serial의 관찰 가능한
|
|
||||||
결과가 bit-for-bit 유지되는 테스트를 둔다.
|
- Debug와 Release configure/build/test를 새로 실행한다.
|
||||||
- 실제 두 번째 사용처가 생기기 전에 범용 registry나 backend hierarchy를 만들지
|
- test count가 0이 아닌지 확인한다.
|
||||||
않는다.
|
- Harness pytest, reference, determinism, HDF5 inspection, install-tree smoke test와 scale
|
||||||
|
benchmark 증거를 checklist ID에 연결한다.
|
||||||
3. Parallel failure ordering
|
- 모든 수용 조건에 현재 실행 증거가 있을 때만 Step과 Phase를 `completed`로 바꾼다.
|
||||||
- worker에서 여러 Beam kernel failure가 발생해도 scheduling 순서로 어느 오류를
|
- 실패나 미실행 항목이 있으면 정확한 blocker를 남기고 release 완료를 선언하지 않는다.
|
||||||
반환할지 결정하면 재현성이 깨진다.
|
|
||||||
- shared CSR write나 first-writer-wins exception 상태를 만들지 말고 canonical
|
## 6. 유지해야 할 경계
|
||||||
element identity 기준으로 deterministic하게 처리한다.
|
|
||||||
|
- C++20, Visual Studio 2026 MSVC v145와 Windows x64 기준을 유지한다.
|
||||||
4. AssemblyOptions validation
|
- `core`, `model`, `fem`, `elements`에 Abaqus, MKL, TBB 또는 HDF5 API를 노출하지
|
||||||
- max_threads와 grain_size의 0 의미를 묵시적으로 정하지 않는다.
|
|
||||||
- automatic 또는 invalid 중 가장 단순한 계약을 선택하고 focused test로 고정한다.
|
|
||||||
|
|
||||||
5. Force assembly
|
|
||||||
- 현재 RHS는 nodal load를 serial full-vector 순서로 누적한다.
|
|
||||||
- 이번 Phase는 element evaluation 병렬화가 목적이다. force accumulation을 별도
|
|
||||||
병렬 기능으로 확장하지 말고 serial oracle과 bitwise equality를 유지한다.
|
|
||||||
|
|
||||||
6. Benchmark 격리
|
|
||||||
- benchmark는 제품 correctness test를 우회하는 별도 assembly를 사용하지 않는다.
|
|
||||||
- fixed Domain과 production serial/parallel API를 호출한다.
|
|
||||||
- speedup이나 release 성능 목표를 임의 assertion으로 추가하지 않는다.
|
|
||||||
|
|
||||||
## 9. 아키텍처와 범위 경계
|
|
||||||
|
|
||||||
- oneTBB 의존성은 assembly adapter/implementation 경계에 가둔다.
|
|
||||||
- core, model, fem, elements의 public contract에 TBB type을 노출하지 않는다.
|
|
||||||
- Beam kernel은 element-local contribution만 계산하고 전역 CSR을 알지 않는다.
|
|
||||||
- DofManager가 DOF와 equation mapping을 계속 단독 소유한다.
|
|
||||||
- worker는 공유 CSR values에 atomic add하지 않는다.
|
|
||||||
- contribution의 부동소수점 합산 순서를 thread scheduling과 분리한다.
|
|
||||||
- TBB element work가 모두 끝난 후에만 PARDISO를 호출한다.
|
|
||||||
- 기존 assemble_serial은 oracle로 유지한다.
|
|
||||||
- tolerance 비교를 bitwise 재현성의 대체물로 사용하지 않는다.
|
|
||||||
- result-contract-completion의 element result, HDF5 확장, CSV adapter를 선행하지
|
|
||||||
않는다.
|
않는다.
|
||||||
- beam-reference-qualification의 Abaqus 골든 비교와 tolerance 작업을 선행하지
|
- solver semantic model과 result contract에 installer 또는 serialization 전용 타입을
|
||||||
않는다.
|
추가하지 않는다.
|
||||||
- internal-release의 installer, Release package와 validation report를 선행하지
|
- dependency는 개발 환경에 사전 설치된 버전을 사용하며 package 중 다운로드하지
|
||||||
않는다.
|
않는다.
|
||||||
|
- install tree는 source/build tree의 절대경로에 의존하지 않아야 한다.
|
||||||
|
- Debug와 Release artifact/runtime을 혼합하지 않는다.
|
||||||
|
- FESA는 단위 변환을 수행하지 않는다.
|
||||||
|
- performance 수치를 correctness gate로 바꾸지 않는다.
|
||||||
|
- stress를 Abaqus-qualified로 표현하지 않는다.
|
||||||
|
- 테스트를 disable하거나 제외해 release evidence를 만들지 않는다.
|
||||||
|
- 사용자가 명시적으로 요청하지 않은 phase 실행에서는 `--push`를 사용하지 않는다.
|
||||||
|
|
||||||
## 10. Harness child 환경
|
## 7. 검증 기준선과 개발환경
|
||||||
|
|
||||||
이전 Harness 실행에서 WindowsApps PowerShell을 child process로 시작할 때 access
|
### 7.1 마지막 확인 결과
|
||||||
denied가 발생했다. 확인된 구성:
|
|
||||||
|
|
||||||
- standalone Codex release:
|
2026-08-03의 beam reference qualification 완료 및 `dev` 병합 후 다음을 확인했다.
|
||||||
C:\Users\baram\.codex\packages\standalone\releases\0.146.0-x86_64-pc-windows-msvc
|
|
||||||
- WindowsApps가 제거된 PATH
|
|
||||||
- Windows PowerShell 5.1
|
|
||||||
- Harness 실행 동안만 [windows] sandbox = "unelevated"
|
|
||||||
|
|
||||||
현재 C:\Users\baram\.codex\config.toml은 원래 값인
|
- `cmake --build --preset windows-debug`: 성공, 새 MSVC warning 없음
|
||||||
[windows] sandbox = "elevated"로 복원되어 있음을 2026-08-01에 확인했다.
|
- `ctest --preset windows-debug --output-on-failure`: 68/68 통과
|
||||||
|
- `uv run --with pytest python -m pytest -v -rs`: 20/20 통과
|
||||||
|
- thread `{1,2,16}`에서 10회 반복한 조립/해석 결과: bitwise 동일
|
||||||
|
|
||||||
같은 문제가 재현될 때만 다음 순서를 사용한다.
|
이 수치는 새 세션이 유지해야 할 Debug baseline이다. Release와 install-tree 결과로
|
||||||
|
확대 해석하지 않는다.
|
||||||
|
|
||||||
1. 다른 Codex 작업에 미칠 영향을 확인하고 config 원래 값을 기록한다.
|
### 7.2 Package 설정
|
||||||
2. Harness 실행 동안만 sandbox를 unelevated로 바꾼다.
|
|
||||||
3. 현재 PowerShell 세션 PATH 앞에 standalone bin과 Windows PowerShell 5.1을
|
|
||||||
두고 모든 WindowsApps entry를 제거한다.
|
|
||||||
4. package 환경 변수와 baseline을 확인한 뒤 같은 세션에서 Harness를 실행한다.
|
|
||||||
5. 성공·실패와 무관하게 finally에 해당하는 정리 단계에서 global config를 즉시
|
|
||||||
elevated로 복원하고 실제 값을 다시 읽는다.
|
|
||||||
|
|
||||||
$codexReleaseBin = "C:\Users\baram\.codex\packages\standalone\releases\0.146.0-x86_64-pc-windows-msvc\bin"
|
새 PowerShell 세션에서 configure 전에 현재 설치 위치를 확인한다. tracked preset이나
|
||||||
$windowsPowerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0"
|
CMake 파일에 사용자별 절대경로를 넣지 않는다.
|
||||||
$filteredPath = $env:PATH -split ";" | Where-Object {
|
|
||||||
$_ -and
|
|
||||||
$_ -ne $codexReleaseBin -and
|
|
||||||
$_ -ne $windowsPowerShell -and
|
|
||||||
$_ -notmatch "WindowsApps" -and
|
|
||||||
$_ -notmatch "\\OpenAI\\Codex\\bin$"
|
|
||||||
}
|
|
||||||
$env:PATH = (@($codexReleaseBin, $windowsPowerShell) + $filteredPath) -join ";"
|
|
||||||
|
|
||||||
(Get-Command codex).Source
|
```powershell
|
||||||
(Get-Command powershell).Source
|
$env:MKL_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\mkl"
|
||||||
|
$env:TBB_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\tbb"
|
||||||
|
$env:HDF5_DIR = "C:\Program Files\HDF_Group\HDF5\2.1.1\cmake"
|
||||||
|
$env:GTest_DIR = "C:\Users\baram\AppData\Local\FESA\dependencies\googletest-1.17.0-v145-x64-crt\lib\cmake\GTest"
|
||||||
|
|
||||||
사용자 profile 전체나 drive root를 --codex-add-dir로 허용하지 않는다. 설치 버전이나
|
Test-Path "$env:MKL_DIR\MKLConfig.cmake"
|
||||||
경로가 달라졌다면 위 절대경로를 그대로 사용하지 말고 실제 standalone release와
|
Test-Path "$env:TBB_DIR\TBBConfig.cmake"
|
||||||
codex-resources 존재를 먼저 확인한다.
|
Test-Path "$env:HDF5_DIR\hdf5-config.cmake"
|
||||||
|
Test-Path "$env:GTest_DIR\GTestConfig.cmake"
|
||||||
|
```
|
||||||
|
|
||||||
Harness가 모든 Step과 phase commit을 완료한 뒤 stderr reader의 CP949/UTF-8 decode
|
네 경로가 모두 유효한 같은 셸에서 preset을 실행한다. cache가 없거나 package 위치가
|
||||||
예외를 출력할 수 있다. exit code만 보지 말고 phase metadata, output JSON, Git
|
변경됐을 때만 `--fresh` configure를 사용한다.
|
||||||
commit과 전체 검증 결과를 함께 확인한다.
|
|
||||||
|
|
||||||
## 11. 새 세션 시작 절차
|
```powershell
|
||||||
|
cmake --fresh --preset windows-debug
|
||||||
|
cmake --build --preset windows-debug
|
||||||
|
ctest --preset windows-debug --output-on-failure
|
||||||
|
uv run --with pytest python -m pytest -v -rs
|
||||||
|
```
|
||||||
|
|
||||||
먼저 이 문서와 다음 파일을 모두 읽는다.
|
Release evidence는 `internal-release` Step에서 새로 생성한다.
|
||||||
|
|
||||||
/phases/deterministic-parallel-assembly/index.json
|
```powershell
|
||||||
/phases/deterministic-parallel-assembly/step0.md
|
cmake --preset windows-release
|
||||||
/phases/deterministic-parallel-assembly/step1.md
|
cmake --build --preset windows-release
|
||||||
/phases/deterministic-parallel-assembly/step2.md
|
ctest --preset windows-release --output-on-failure
|
||||||
/src/fesa/assembly/serial_assembler.cpp
|
```
|
||||||
/tests/unit/assembly/serial_assembler_test.cpp
|
|
||||||
|
|
||||||
그 다음 Git과 package 상태를 재검증하고 6절의 baseline 명령을 실행한다. 필요하면
|
## 8. Harness 주의사항
|
||||||
10절의 child sandbox 조건을 적용한 뒤 다음을 실행한다.
|
|
||||||
|
|
||||||
python scripts/execute.py deterministic-parallel-assembly
|
표준 실행 명령은 다음과 같다.
|
||||||
|
|
||||||
executor는 feat-deterministic-parallel-assembly 브랜치를 생성하거나 checkout하고
|
```powershell
|
||||||
Step 상태와 output metadata를 기록한다. 사용자가 명시적으로 요청하지 않은 한
|
python scripts/execute.py internal-release
|
||||||
--push를 사용하지 않는다.
|
```
|
||||||
|
|
||||||
각 Step은 다음 순서를 지킨다.
|
이전 beam reference phase에서 Harness child가 WindowsApps PowerShell을 시작하지 못해
|
||||||
|
`CreateProcessAsUserW` 오류가 반복됐다. 동일 저장소와 preset에서 직접 실행한 수용
|
||||||
|
명령은 성공했고, `stepN-output.json`에는 child 환경 blocker와 직접 실행 증거를
|
||||||
|
구분해 기록했다.
|
||||||
|
|
||||||
1. Step 파일의 필수 문서와 선행 구현을 모두 읽는다.
|
새 세션에서는 다음 원칙을 지킨다.
|
||||||
2. 성공 기준과 canonical ordering/threading invariant를 명시한다.
|
|
||||||
3. 8절의 모호한 계약을 구현 전에 정렬한다.
|
|
||||||
4. 실패 테스트를 먼저 작성하고 예상한 이유로 실패함을 확인한다.
|
|
||||||
5. 테스트를 통과시키는 최소 production code만 구현한다.
|
|
||||||
6. focused test, 전체 CTest와 Harness pytest를 실행한다.
|
|
||||||
7. Step summary와 output metadata가 실제 결과와 일치하는지 확인한다.
|
|
||||||
8. Phase 종료 전 전체 diff를 determinism, race avoidance, TBB 경계와 기존 serial
|
|
||||||
oracle 기준으로 review한다.
|
|
||||||
|
|
||||||
## 12. 다음 Phase 완료 조건
|
1. 먼저 표준 Harness 실행을 시도한다.
|
||||||
|
2. 같은 child shell 오류가 발생하면 중복 executor를 시작하지 말고 남은 process를
|
||||||
|
확인한다.
|
||||||
|
3. 직접 실행한 명령과 Harness 실행 결과를 혼동하지 않고 metadata에 사실대로 적는다.
|
||||||
|
4. 사용자 profile이나 drive root를 `--codex-add-dir`로 허용하지 않는다.
|
||||||
|
5. global Codex 설정을 임시 변경했다면 원래 값을 기록하고 종료 즉시 복원·재확인한다.
|
||||||
|
6. 사용자가 push를 명시하지 않은 phase 실행에는 `--push`를 추가하지 않는다.
|
||||||
|
|
||||||
- /phases/deterministic-parallel-assembly/index.json의 Step 0~2가 모두 completed
|
## 9. 새 세션 시작 순서
|
||||||
- /phases/index.json에서 deterministic-parallel-assembly가 completed
|
|
||||||
- shuffled/cancellation/signed-zero contribution의 canonical 결과가 bit-for-bit 동일
|
|
||||||
- serial과 parallel의 CSR row offsets, column indices, values와 force가 동일
|
|
||||||
- thread count 1, 2, available concurrency와 최소 10회 반복에서 결과가 동일
|
|
||||||
- 최종 linear static displacement와 reaction이 thread count에 무관하게 동일
|
|
||||||
- worker가 thread-local contribution만 생성하고 공유 CSR에 atomic add하지 않음
|
|
||||||
- TBB task와 PARDISO 실행이 중첩되지 않음
|
|
||||||
- deterministic Beam kernel failure propagation 검증
|
|
||||||
- benchmark가 production API를 사용하고 speedup을 pass 조건으로 만들지 않음
|
|
||||||
- focused test와 전체 CTest 통과
|
|
||||||
- Harness pytest가 0개가 아닌 상태로 전체 통과
|
|
||||||
- 새 MSVC warning 없음
|
|
||||||
- 독립 review의 Critical/Important finding 해결
|
|
||||||
- 사용자 선택 전 원격 push나 dev 병합을 수행하지 않음
|
|
||||||
|
|
||||||
새 세션의 권장 첫 요청:
|
1. `AGENTS.md`와 이 문서를 읽는다.
|
||||||
|
2. `docs/PRD.md` 8절, `docs/VALIDATION.md`, `phases/internal-release/index.json`과
|
||||||
|
Step 0~4를 읽는다.
|
||||||
|
3. Git 상태와 `dev == origin/dev`를 확인한다.
|
||||||
|
4. 4.4절의 Step 0/4 reference 범위 문구를 최신 계약에 맞춘다.
|
||||||
|
5. Debug baseline을 새로 실행한다.
|
||||||
|
6. Step 0의 release 문서와 checklist 요구조건을 먼저 테스트 가능한 형태로 고정한다.
|
||||||
|
7. 다음 명령으로 Phase를 실행한다.
|
||||||
|
|
||||||
> docs/HANDOFF.md와 deterministic-parallel-assembly의 index/step0~2를 읽고 현재
|
```powershell
|
||||||
> dev baseline, oneTBB 환경과 Harness child 실행 조건을 확인한 뒤
|
python scripts/execute.py internal-release
|
||||||
> deterministic-parallel-assembly Phase를 시작해주세요.
|
```
|
||||||
|
|
||||||
|
각 Step에서는 실패하는 검사 또는 미충족 evidence를 먼저 확인하고, 최소 변경으로
|
||||||
|
수용 조건을 만족시킨 뒤 focused test와 전체 test를 실행한다. Step output과 phase
|
||||||
|
metadata는 실제 명령 결과를 그대로 반영한다.
|
||||||
|
|
||||||
|
## 10. `internal-release` 완료 조건
|
||||||
|
|
||||||
|
- `phases/internal-release/index.json`의 Step 0~4가 모두 `completed`
|
||||||
|
- `phases/index.json`의 `internal-release`가 `completed`
|
||||||
|
- PRD 8절의 모든 기준이 고유 checklist ID와 실제 증거에 연결됨
|
||||||
|
- Debug/Release build에 새 MSVC warning이 없음
|
||||||
|
- Debug/Release CTest와 Harness pytest가 0개가 아닌 상태로 모두 통과
|
||||||
|
- `cmake --install` 결과에 요구 binary/library/header/document/example/runtime
|
||||||
|
inventory가 포함됨
|
||||||
|
- source/build tree를 숨긴 install consumer와 CLI/HDF5 smoke test 통과
|
||||||
|
- 약 100,000 DOF benchmark의 correctness, stage time, memory와 환경 기록 완료
|
||||||
|
- Gate A PASS와 Gate B EVALUABLE의 의미 및 Abaqus stress 제한을 release 문서가
|
||||||
|
정확히 유지함
|
||||||
|
- 실패하거나 미실행인 증거가 없는 경우에만 내부 배포 완료 선언
|
||||||
|
|
||||||
|
새 세션의 권장 첫 요청은 다음과 같다.
|
||||||
|
|
||||||
|
> `docs/HANDOFF.md`와 `internal-release`의 index/step0~4를 읽고 현재 `dev`
|
||||||
|
> baseline과 beam reference 검증 범위를 확인해주세요. Step 0과 Step 4의 stale
|
||||||
|
> reference 문구를 PRD/VALIDATION에 맞춘 뒤 `internal-release` Phase를 시작해주세요.
|
||||||
|
|||||||
+154
-76
@@ -1,90 +1,168 @@
|
|||||||
# FESA HDF5 Schema 1.0.0
|
# FESA HDF5 Schema 2.0.0
|
||||||
|
|
||||||
## 1. 범위
|
## 1. Scope and version compatibility
|
||||||
|
|
||||||
Schema `1.0.0`은 `results-and-pipeline` Phase의 최소 수직 슬라이스를 정의한다.
|
Schema `2.0.0` is the self-contained Phase 1 result contract. One file contains
|
||||||
파일은 활성 `Domain`의 절점, Beam 연결성, 적용된 전단면적과 그 출처, 그리고 전역
|
the active normalized model, its single linear-static analysis definition and
|
||||||
좌표계 절점 변위·회전 및 반력·반력모멘트를 저장한다. 단위 변환은 수행하지 않는다.
|
solver settings, and every nodal and Beam result needed without the source
|
||||||
|
`.inp` file. FESA performs no unit conversion.
|
||||||
|
|
||||||
이 버전에는 재료 전체 속성, 집합, 하중·경계조건, solver 설정, 요소 결과, history,
|
Schema `1.0.0` was the earlier minimal vertical slice. Version `2.0.0` changes
|
||||||
reference CSV 및 진단 dataset을 저장하지 않는다. 이후 같은 major version에서
|
the required model and result objects, so it is a new major version rather than
|
||||||
dataset을 추가할 수 있지만 아래 required object의 의미, 형상 또는 datatype을
|
an in-place change to `1.0.0`. The current writer and reader accept exactly
|
||||||
변경해서는 안 된다.
|
`2.0.0`; every other version fails with `hdf5.unsupported_schema`. A future
|
||||||
|
reader may explicitly add support for compatible minor versions, but must not
|
||||||
|
infer compatibility from a version prefix.
|
||||||
|
|
||||||
## 2. 공통 규칙
|
## 2. Common rules
|
||||||
|
|
||||||
- root attribute `schema_version`은 UTF-8 문자열 `1.0.0`이다.
|
- Root attributes are variable-length UTF-8 strings:
|
||||||
- 정수 dataset은 명시한 little-endian 고정폭 타입을 사용한다.
|
`schema_version="2.0.0"`, `fesa_version`, and
|
||||||
- 실수 dataset은 IEEE 754 little-endian 64-bit 타입을 사용한다.
|
`unit_policy="consistent_input_units_no_conversion"`, `input_source`, and
|
||||||
- 문자열 dataset과 attribute는 UTF-8 variable-length string을 사용한다.
|
`input_fingerprint`. `input_source` is the UTF-8 path supplied to the solve
|
||||||
- `dense_index`는 해당 dataset 행의 0-based index이며 연속적이다.
|
request. `input_fingerprint` is `fnv1a64:` followed by the 16 lowercase
|
||||||
- `internal_id`와 결과의 `node_ids`는 FESA semantic model의 nonnegative ID다.
|
hexadecimal digits of FNV-1a 64 over the original input bytes; it is a
|
||||||
- flat/orphan mesh의 `part_name`과 `instance_name`은 빈 문자열이다.
|
reproducibility identifier, not a cryptographic integrity guarantee.
|
||||||
- 결과의 6개 component 순서는
|
- Integer datasets use the stated little-endian fixed-width type. Floating
|
||||||
`(Ux, Uy, Uz, Rx, Ry, Rz)` 및 `(RFx, RFy, RFz, RMx, RMy, RMz)`다.
|
datasets use IEEE 754 little-endian `float64`. Strings are variable-length
|
||||||
- Step과 frame group 이름은 각각 0부터 연속된 decimal index다. 원래 Step 이름은
|
UTF-8.
|
||||||
Step group의 `name` attribute에 저장한다.
|
- `dense_index` is a contiguous 0-based row index. Semantic `internal_id`
|
||||||
|
values are nonnegative and are not assumed to be dense or ordered.
|
||||||
|
- Flat/orphan mesh `part_name` and `instance_name` values are empty strings.
|
||||||
|
- Ragged arrays use an offset dataset of length `row_count + 1`. Offsets start
|
||||||
|
at zero, are nondecreasing, and the final offset equals the flattened row
|
||||||
|
count. Input order is preserved.
|
||||||
|
- Numeric field datasets carry UTF-8 `coordinate_system` and `components`
|
||||||
|
attributes where listed. `components` is a comma-separated ordered list.
|
||||||
|
- Step and frame group names are contiguous decimal indices beginning at zero.
|
||||||
|
Phase 1 requires exactly one analysis step and one result step with the same
|
||||||
|
name, and exactly one result frame in that step.
|
||||||
|
|
||||||
## 3. Required objects
|
## 3. Required objects
|
||||||
|
|
||||||
```text
|
### 3.1 Model
|
||||||
/
|
|
||||||
├── @schema_version UTF-8 = "1.0.0"
|
|
||||||
├── model
|
|
||||||
│ ├── nodes
|
|
||||||
│ │ ├── dense_index uint64 [node_count]
|
|
||||||
│ │ ├── internal_id int64 [node_count]
|
|
||||||
│ │ ├── part_name UTF-8 [node_count]
|
|
||||||
│ │ ├── instance_name UTF-8 [node_count]
|
|
||||||
│ │ ├── local_label int64 [node_count]
|
|
||||||
│ │ └── coordinates float64[node_count, 3]
|
|
||||||
│ ├── elements
|
|
||||||
│ │ ├── dense_index uint64 [element_count]
|
|
||||||
│ │ ├── internal_id int64 [element_count]
|
|
||||||
│ │ ├── connectivity uint64 [element_count, 2]
|
|
||||||
│ │ └── section_id int64 [element_count]
|
|
||||||
│ └── sections
|
|
||||||
│ ├── internal_id int64 [section_count]
|
|
||||||
│ ├── shear_area_y float64[section_count]
|
|
||||||
│ ├── shear_area_z float64[section_count]
|
|
||||||
│ └── shear_source uint8 [section_count]
|
|
||||||
└── results
|
|
||||||
└── steps
|
|
||||||
└── <step_index>
|
|
||||||
├── @name UTF-8
|
|
||||||
└── frames
|
|
||||||
└── <frame_index>
|
|
||||||
├── @step_time float64
|
|
||||||
└── nodal
|
|
||||||
├── node_ids int64 [result_node_count]
|
|
||||||
├── displacement float64[result_node_count, 6]
|
|
||||||
└── reaction float64[result_node_count, 6]
|
|
||||||
```
|
|
||||||
|
|
||||||
`model/elements/connectivity`는 `model/nodes/dense_index`를 참조한다. 따라서
|
Let `N`, `E`, `M`, `S`, `NS`, and `ES` be the node, Beam element, material,
|
||||||
`internal_id`가 연속적이거나 Domain 저장 순서와 같다고 가정하지 않는다.
|
section, node-set, and element-set counts. Let `P` be the total number of
|
||||||
`section_id`는 `model/sections/internal_id`를 참조한다.
|
section recovery points, `NM` the total node-set membership count, and `EM` the
|
||||||
|
total element-set membership count.
|
||||||
|
|
||||||
`shear_source` 값은 다음과 같다.
|
| Path | Type | Rank and shape | Attributes / meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/model/nodes/dense_index` | `uint64` | 1, `[N]` | contiguous row index |
|
||||||
|
| `/model/nodes/internal_id` | `int64` | 1, `[N]` | `NodeId` |
|
||||||
|
| `/model/nodes/part_name` | UTF-8 | 1, `[N]` | entity provenance |
|
||||||
|
| `/model/nodes/instance_name` | UTF-8 | 1, `[N]` | entity provenance |
|
||||||
|
| `/model/nodes/local_label` | `int64` | 1, `[N]` | external local label |
|
||||||
|
| `/model/nodes/coordinates` | `float64` | 2, `[N,3]` | `coordinate_system="global"`, `components="X,Y,Z"` |
|
||||||
|
| `/model/elements/dense_index` | `uint64` | 1, `[E]` | contiguous row index |
|
||||||
|
| `/model/elements/internal_id` | `int64` | 1, `[E]` | `ElementId` |
|
||||||
|
| `/model/elements/part_name` | UTF-8 | 1, `[E]` | entity provenance |
|
||||||
|
| `/model/elements/instance_name` | UTF-8 | 1, `[E]` | entity provenance |
|
||||||
|
| `/model/elements/local_label` | `int64` | 1, `[E]` | external local label |
|
||||||
|
| `/model/elements/connectivity` | `uint64` | 2, `[E,2]` | node `dense_index`, ordered end `-1,+1` |
|
||||||
|
| `/model/elements/material_id` | `int64` | 1, `[E]` | references material `internal_id` |
|
||||||
|
| `/model/elements/section_id` | `int64` | 1, `[E]` | references section `internal_id` |
|
||||||
|
| `/model/materials/internal_id` | `int64` | 1, `[M]` | `MaterialId` |
|
||||||
|
| `/model/materials/name` | UTF-8 | 1, `[M]` | material name |
|
||||||
|
| `/model/materials/young_modulus` | `float64` | 1, `[M]` | finite, positive |
|
||||||
|
| `/model/materials/poisson_ratio` | `float64` | 1, `[M]` | finite, `-1 < nu < 0.5` |
|
||||||
|
| `/model/sections/internal_id` | `int64` | 1, `[S]` | `SectionId` |
|
||||||
|
| `/model/sections/name` | UTF-8 | 1, `[S]` | section name |
|
||||||
|
| `/model/sections/area` | `float64` | 1, `[S]` | `A` |
|
||||||
|
| `/model/sections/moment_y` | `float64` | 1, `[S]` | `Iy` |
|
||||||
|
| `/model/sections/moment_z` | `float64` | 1, `[S]` | `Iz` |
|
||||||
|
| `/model/sections/torsion_constant` | `float64` | 1, `[S]` | `J` |
|
||||||
|
| `/model/sections/shear_area_y` | `float64` | 1, `[S]` | applied `Asy` |
|
||||||
|
| `/model/sections/shear_area_z` | `float64` | 1, `[S]` | applied `Asz` |
|
||||||
|
| `/model/sections/shear_source` | `uint8` | 1, `[S]` | `0=input`, `1=phase1_default` |
|
||||||
|
| `/model/sections/orientation` | `float64` | 2, `[S,3]` | `coordinate_system="global"`, `components="X,Y,Z"` |
|
||||||
|
| `/model/sections/recovery_point_offsets` | `uint64` | 1, `[S+1]` | offsets into `recovery_points` |
|
||||||
|
| `/model/sections/recovery_points` | `float64` | 2, `[P,2]` | `coordinate_system="element_local"`, `components="y,z"` |
|
||||||
|
| `/model/sets/node/names` | UTF-8 | 1, `[NS]` | exact node-set names |
|
||||||
|
| `/model/sets/node/member_offsets` | `uint64` | 1, `[NS+1]` | offsets into `members` |
|
||||||
|
| `/model/sets/node/members` | `int64` | 1, `[NM]` | `NodeId`, input set/member order |
|
||||||
|
| `/model/sets/element/names` | UTF-8 | 1, `[ES]` | exact element-set names |
|
||||||
|
| `/model/sets/element/member_offsets` | `uint64` | 1, `[ES+1]` | offsets into `members` |
|
||||||
|
| `/model/sets/element/members` | `int64` | 1, `[EM]` | `ElementId`, input set/member order |
|
||||||
|
|
||||||
| 값 | 의미 |
|
### 3.2 Analysis
|
||||||
|
|
||||||
|
`/analysis/steps/0` has UTF-8 attribute `name`. Let `B` be the prescribed-DOF
|
||||||
|
count and `L` the nodal-load count.
|
||||||
|
|
||||||
|
| Path | Type | Rank and shape | Attributes / meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/analysis/steps/0/boundary_conditions/node_ids` | `int64` | 1, `[B]` | target `NodeId` |
|
||||||
|
| `/analysis/steps/0/boundary_conditions/dofs` | `uint8` | 1, `[B]` | Abaqus/FESA DOF number 1 through 6 |
|
||||||
|
| `/analysis/steps/0/boundary_conditions/values` | `float64` | 1, `[B]` | prescribed value |
|
||||||
|
| `/analysis/steps/0/nodal_loads/node_ids` | `int64` | 1, `[L]` | target `NodeId` |
|
||||||
|
| `/analysis/steps/0/nodal_loads/values` | `float64` | 2, `[L,6]` | `coordinate_system="global"`, `components="Fx,Fy,Fz,Mx,My,Mz"` |
|
||||||
|
|
||||||
|
`/analysis/solver_settings` has the exact UTF-8 attributes used by the Phase 1
|
||||||
|
pipeline: `backend="mkl_pardiso"`,
|
||||||
|
`matrix_storage="symmetric_upper_csr"`,
|
||||||
|
`matrix_type="symmetric_positive_definite"`,
|
||||||
|
`constraint_method="essential_dof_elimination"`, and
|
||||||
|
`assembly="deterministic_serial"`. No unused future settings are stored.
|
||||||
|
|
||||||
|
### 3.3 Results
|
||||||
|
|
||||||
|
`/results/steps/0` has UTF-8 attribute `name`; frame group `0` has scalar
|
||||||
|
`float64` attribute `step_time`. Let `RN`, `RE`, `RP`, and `D` be the nodal
|
||||||
|
result, Beam result, flattened Beam recovery-point, and diagnostic counts.
|
||||||
|
|
||||||
|
| Path below `/results/steps/0/frames/0` | Type | Rank and shape | Attributes / meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `nodal/node_ids` | `int64` | 1, `[RN]` | `NodeId`; provenance is joined from `/model/nodes` |
|
||||||
|
| `nodal/displacement` | `float64` | 2, `[RN,6]` | `coordinate_system="global"`, `components="Ux,Uy,Uz,Rx,Ry,Rz"` |
|
||||||
|
| `nodal/reaction` | `float64` | 2, `[RN,6]` | `coordinate_system="global"`, `components="RFx,RFy,RFz,RMx,RMy,RMz"` |
|
||||||
|
| `element/beam/element_ids` | `int64` | 1, `[RE]` | `ElementId` |
|
||||||
|
| `element/beam/part_name` | UTF-8 | 1, `[RE]` | result provenance |
|
||||||
|
| `element/beam/instance_name` | UTF-8 | 1, `[RE]` | result provenance |
|
||||||
|
| `element/beam/local_label` | `int64` | 1, `[RE]` | result provenance |
|
||||||
|
| `element/beam/local_frame` | `float64` | 3, `[RE,3,3]` | `coordinate_system="global"`, `components="ex,ey,ez"`; last dimension is `X,Y,Z` |
|
||||||
|
| `element/beam/end_node_ids` | `int64` | 2, `[RE,2]` | ordered ends `-1,+1` |
|
||||||
|
| `element/beam/xi` | `float64` | 2, `[RE,2]` | `components="end_minus,end_plus"` |
|
||||||
|
| `element/beam/section_strain` | `float64` | 3, `[RE,2,6]` | `coordinate_system="element_local"`, `components="epsilon,gamma_y,gamma_z,kappa_x,kappa_y,kappa_z"` |
|
||||||
|
| `element/beam/section_force` | `float64` | 3, `[RE,2,6]` | `coordinate_system="element_local"`, `components="N,Vy,Vz,T,My,Mz"` |
|
||||||
|
| `element/beam/centroid_sigma_xx` | `float64` | 2, `[RE,2]` | `coordinate_system="element_local"`, `components="end_minus,end_plus"`, `quantity="sigma_xx"` |
|
||||||
|
| `element/beam/recovery_point_offsets` | `uint64` | 1, `[RE+1]` | offsets into recovery-point rows |
|
||||||
|
| `element/beam/recovery_point_sigma_xx` | `float64` | 2, `[RP,2]` | `coordinate_system="element_local"`, `components="end_minus,end_plus"`, `quantity="sigma_xx"`; point order comes from the referenced section |
|
||||||
|
| `diagnostics/stage` | `uint8` | 1, `[D]` | enum table below |
|
||||||
|
| `diagnostics/severity` | `uint8` | 1, `[D]` | `0=warning`, `1=error` |
|
||||||
|
| `diagnostics/code` | UTF-8 | 1, `[D]` | exact diagnostic code |
|
||||||
|
| `diagnostics/message` | UTF-8 | 1, `[D]` | exact diagnostic message |
|
||||||
|
| `diagnostics/has_source` | `uint8` | 1, `[D]` | `0=no source`, `1=source present` |
|
||||||
|
| `diagnostics/source_file` | UTF-8 | 1, `[D]` | empty when source is absent |
|
||||||
|
| `diagnostics/source_line` | `uint64` | 1, `[D]` | zero when source is absent |
|
||||||
|
| `diagnostics/source_column` | `uint64` | 1, `[D]` | zero when source is absent |
|
||||||
|
|
||||||
|
Diagnostic stage encoding follows the declaration order:
|
||||||
|
|
||||||
|
| Value | Stage |
|
||||||
|---:|---|
|
|---:|---|
|
||||||
| `0` | 입력에서 명시된 전단강성으로부터 구성한 값 (`input`) |
|
| 0 | `io` |
|
||||||
| `1` | Phase 1 기본값 `Asy=Asz=5A/6`, `SCF=0` (`phase1_default`) |
|
| 1 | `syntax` |
|
||||||
|
| 2 | `semantic` |
|
||||||
|
| 3 | `model` |
|
||||||
|
| 4 | `equation` |
|
||||||
|
| 5 | `solver` |
|
||||||
|
| 6 | `results` |
|
||||||
|
| 7 | `validation` |
|
||||||
|
|
||||||
## 4. Writer와 reader 계약
|
## 4. Writer and reader contract
|
||||||
|
|
||||||
- writer는 쓰기 전에 `ResultDatabase` 유효성과 schema version을 검사한다.
|
- The writer validates `ResultDatabase`, exact schema version, model/result ID
|
||||||
- schema `1.0.0`이 표현하지 않는 non-empty frame diagnostics는 파일을 만들기 전에
|
and provenance joins, Beam connectivity, recovery-point counts, and the
|
||||||
`hdf5.unsupported_result_diagnostics`로 거부한다.
|
single-step name before creating the file.
|
||||||
- required object 생성·쓰기·flush·close 중 HDF5 오류가 발생하면 성공으로 반환하지
|
- Required-object creation, write, flush, and close failures become
|
||||||
않고 `DiagnosticStage::results` 오류로 변환한다.
|
`DiagnosticStage::results` errors. A failed write is never reported as
|
||||||
- reader는 schema version, required object, datatype, rank와 shape를 검사한다.
|
success.
|
||||||
- reader는 model internal ID의 uniqueness, finite coordinates 및 finite positive
|
- The reader validates the exact version, required datatypes, ranks, shapes,
|
||||||
shear area를 검사한다.
|
offsets, finite values, uniqueness, references, field metadata, and result
|
||||||
- writer와 reader는 모든 nodal result ID가 `model/nodes/internal_id`에 존재하는지
|
contracts. It does not return a partial database or partial snapshots.
|
||||||
검사하며, 없는 ID를 성공 결과로 반환하지 않는다.
|
- The public reader returns adapter-owned, read-only metadata, model, and
|
||||||
- reader는 nodal result를 `ResultDatabase`로, model dataset을 HDF5 adapter 전용
|
analysis snapshots plus the semantic `ResultDatabase`. No HDF5 object or
|
||||||
read-only inspection model로 반환한다. `Domain`과 `ResultDatabase`에는 HDF5
|
handle escapes the adapter, and the snapshots contain enough information to
|
||||||
저장 계약을 추가하지 않는다.
|
reconstruct the Phase 1 model and run definition without the source deck.
|
||||||
- malformed 또는 지원하지 않는 파일은 부분 database를 반환하지 않는다.
|
|
||||||
|
|||||||
+40
-13
@@ -163,8 +163,8 @@ HDF5 결과는 다음 정보를 함께 갖는 자기완결형 파일이어야
|
|||||||
- 여러 재료·단면과 중첩 집합
|
- 여러 재료·단면과 중첩 집합
|
||||||
4. Reference 테스트
|
4. Reference 테스트
|
||||||
- Abaqus/Standard 2024 B31 결과
|
- Abaqus/Standard 2024 B31 결과
|
||||||
- 현재 캔틸레버의 변위와 반력
|
- 현재 캔틸레버의 변위, 반력 및 요소 단면력
|
||||||
- 요소 내력 및 요소 절점 단면 도심 응력 비교 계약의 synthetic CSV 검증
|
- 요소 절점 단면 도심 응력 비교 계약의 synthetic CSV 검증
|
||||||
|
|
||||||
### 5.2 골든 데이터
|
### 5.2 골든 데이터
|
||||||
|
|
||||||
@@ -174,8 +174,15 @@ Abaqus는 CI나 Harness에서 자동 실행하지 않는다. 별도 Abaqus 2024
|
|||||||
|
|
||||||
비교 실행은 물리량과 해당 CSV 경로를 명시한다. 요청한 파일이 없으면 실패하고,
|
비교 실행은 물리량과 해당 CSV 경로를 명시한다. 요청한 파일이 없으면 실패하고,
|
||||||
요청하지 않은 물리량은 통과로 보고하지 않는다. 현재 `reference/cantilever beam`
|
요청하지 않은 물리량은 통과로 보고하지 않는다. 현재 `reference/cantilever beam`
|
||||||
샘플은 변위와 반력만 비교한다. 요소 내력과 응력 CSV가 추가되기 전까지 해당
|
샘플은 변위, 반력 및 요소 단면력을 비교한다. 요소 응력 CSV가 추가되기 전까지
|
||||||
reader와 비교 kernel은 synthetic CSV로 검증한다.
|
해당 reader와 비교 kernel은 synthetic CSV로 검증한다.
|
||||||
|
|
||||||
|
FESA 정식화 적합성과 Abaqus 결과 상관성은 별도 gate로 운영한다. FESA 적합성
|
||||||
|
gate는 `SCF=0`, 선택적 감차적분 및 문서화된 FESA 정식화를 해석해와 physics
|
||||||
|
invariant로 엄격히 검증한다. Abaqus 상관성 gate는 `SCF=0.25`를 포함할 수 있는
|
||||||
|
원본 Abaqus 모델과 CSV를 보존하고, 동일한 기하·재료·하중에 `SCF=0`을 적용한
|
||||||
|
별도 FESA 입력을 production pipeline으로 해석해 결과 차이를 정량화한다. 상관성
|
||||||
|
gate는 서로 다른 정식화의 수치 일치를 주장하지 않는다.
|
||||||
|
|
||||||
CSV 식별 및 값 열:
|
CSV 식별 및 값 열:
|
||||||
|
|
||||||
@@ -185,17 +192,35 @@ CSV 식별 및 값 열:
|
|||||||
`SF-SF1..SF-SF3`, `SM-SM1..SM-SM3`
|
`SF-SF1..SF-SF3`, `SM-SM1..SM-SM3`
|
||||||
- 요소 응력: `Part Instance Name`, `Element Label`, `Node Label`, `Sxx`
|
- 요소 응력: `Part Instance Name`, `Element Label`, `Node Label`, `Sxx`
|
||||||
|
|
||||||
단일 Instance에서는 `Part Instance Name` 열을 생략할 수 있다. 내력은
|
단일 Instance에서는 `Part Instance Name` 열을 생략할 수 있다. Abaqus Beam의
|
||||||
`SF1,SF2,SF3,SM1,SM2,SM3`을 각각 \(N,V_y,V_z,T,M_y,M_z\)로 비교한다.
|
단면축 \((\mathbf n_1,\mathbf n_2)\)를 FESA의 \((\mathbf e_y,\mathbf e_z)\)와
|
||||||
응력은 요소 절점의 단면 도심값 \(\sigma_{xx}=N/A\)를 비교한다.
|
일치시킨 입력에서 요소 내력은 Abaqus CSV 순서를
|
||||||
|
`SF1,SF3,SF2,SM3,SM1,SM2`로 재배열해 FESA의
|
||||||
|
\(N,V_y,V_z,T,M_y,M_z\)와 비교한다. 응력은 요소 절점의 단면 도심값
|
||||||
|
\(\sigma_{xx}=N/A\)를 비교한다.
|
||||||
|
|
||||||
### 5.3 허용오차
|
### 5.3 허용오차
|
||||||
|
|
||||||
- 단위·정식화 테스트는 정규화된 엄격한 tolerance를 사용한다.
|
- FESA 단위·정식화 적합성 gate는 정규화된 엄격한 tolerance를 사용한다.
|
||||||
- Abaqus 비교 기본 상대오차는 \(10^{-5}\)로 한다.
|
- 정식화가 일치하는 reference 비교의 기본 상대오차는 \(10^{-5}\)로 한다.
|
||||||
- 영에 가까운 결과는 특성 길이, 하중 및 응력에 기반한 절대오차를 함께 사용한다.
|
- Abaqus B31과 FESA Beam의 정식화가 다른 상관성 gate는 component별 RMSE와
|
||||||
- formulation 또는 output 위치 차이로 별도 tolerance가 필요하면 comparison
|
Relative L2를 보고한다. 물리량과 component가 다른 값을 하나의 norm으로
|
||||||
test 설정과 `docs/VALIDATION.md`에 근거를 기록한다.
|
혼합하지 않는다.
|
||||||
|
- component \(c\)의 값 쌍을 \((F_{ic},A_{ic})\), characteristic absolute scale을
|
||||||
|
\(s_c\)라 하면
|
||||||
|
|
||||||
|
\[
|
||||||
|
\operatorname{RMSE}_c=
|
||||||
|
\sqrt{\frac{1}{n}\sum_i(F_{ic}-A_{ic})^2},\qquad
|
||||||
|
\operatorname{RelativeL2}_c=
|
||||||
|
\frac{\sqrt{\sum_i(F_{ic}-A_{ic})^2}}
|
||||||
|
{\max\left(\sqrt{\sum_iA_{ic}^2},\sqrt{n}s_c\right)}.
|
||||||
|
\]
|
||||||
|
|
||||||
|
- Abaqus 상관성 gate의 성공은 요청된 모든 entity/component가 매칭되고 유한한
|
||||||
|
metric이 생성됨을 뜻한다. 관측된 단일 샘플에 맞춘 임의 pass/fail tolerance는
|
||||||
|
두지 않는다. 이후 acceptance envelope를 추가하려면 해석적 또는 mesh study
|
||||||
|
근거와 함께 `docs/VALIDATION.md`에 사전 기록한다.
|
||||||
|
|
||||||
## 6. 개발 워크플로우
|
## 6. 개발 워크플로우
|
||||||
|
|
||||||
@@ -235,6 +260,8 @@ CSV 식별 및 값 열:
|
|||||||
- 테스트 0개 수집이 아님을 확인
|
- 테스트 0개 수집이 아님을 확인
|
||||||
- 전체 입력-해석-출력 통합 테스트 통과
|
- 전체 입력-해석-출력 통합 테스트 통과
|
||||||
- physics sanity와 평형 잔차 기준 통과
|
- physics sanity와 평형 잔차 기준 통과
|
||||||
- 현재 Abaqus 2024 변위·반력 골든 결과의 tolerance 통과
|
- FESA Beam 정식화 적합성 gate의 엄격한 tolerance 통과
|
||||||
|
- 현재 Abaqus 2024 변위·반력·요소 단면력과의 component별 RMSE 및 Relative L2
|
||||||
|
상관성 보고서 생성
|
||||||
- 요소 내력·도심 응력 CSV adapter와 비교 kernel의 synthetic 검증 통과
|
- 요소 내력·도심 응력 CSV adapter와 비교 kernel의 synthetic 검증 통과
|
||||||
- HDF5 schema, 입력 부분집합, 정식화 및 검증 보고서 제공
|
- HDF5 schema, 입력 부분집합, 정식화 및 검증 보고서 제공
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# FESA Phase 1 검증 보고서
|
||||||
|
|
||||||
|
## 1. 검증 기준과 실행 증거
|
||||||
|
|
||||||
|
이 보고서는 2026-08-03에 `feat-beam-reference-qualification`에서 새 Debug 빌드와
|
||||||
|
테스트를 실행해 수집한 결과다. FESA Beam 검증은 다음 두 gate를 독립적으로
|
||||||
|
운영한다.
|
||||||
|
|
||||||
|
- Gate A — FESA 정식화 적합성: FESA가 채택한 선택적 감차적분 Timoshenko
|
||||||
|
정식화를 해석해, 에너지, 강체 mode와 평형으로 검증한다.
|
||||||
|
- Gate B — Abaqus 결과 상관성: Abaqus B31과 FESA의 정식화 차이를 인정하고
|
||||||
|
요청된 결과의 component별 RMSE와 Relative L2를 보고한다.
|
||||||
|
|
||||||
|
실행 결과는 다음과 같다.
|
||||||
|
|
||||||
|
| 명령 | 결과 |
|
||||||
|
|------|------|
|
||||||
|
| `cmake --build --preset windows-debug` | MSVC v145 Debug x64 빌드 성공, 새 경고 없음 |
|
||||||
|
| `ctest --preset windows-debug --output-on-failure` | 68/68 통과 |
|
||||||
|
| `uv run --with pytest python -m pytest -v -rs` | 20/20 통과 |
|
||||||
|
| `fesa solve` 후 `fesa-reference-compare` | 42개 요청 위치와 18개 component metric 생성, exit code 0 |
|
||||||
|
|
||||||
|
Harness child의 관리형 WindowsApps PowerShell은 `CreateProcessAsUserW` 오류로 명령을
|
||||||
|
시작하지 못했다. 위 명령은 동일 저장소와 preset에서 현재 세션이 직접 실행했으며,
|
||||||
|
executor 환경 문제를 테스트 성공으로 간주하지 않았다.
|
||||||
|
|
||||||
|
## 2. Gate A — FESA 정식화 적합성
|
||||||
|
|
||||||
|
### 2.1 요소 정식화와 회복
|
||||||
|
|
||||||
|
`tests/unit/elements/beam3d2_test.cpp`의 다음 검증이 모두 통과했다.
|
||||||
|
|
||||||
|
| 검증 | 증거 | 판정 기준 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| 행렬 유한성·대칭성 | `Beam3D2.ProducesFiniteSymmetricLocalAndGlobalStiffness` | local/global 12×12 전 항 유한, 대칭 항 bitwise equality |
|
||||||
|
| 강체 mode | `RigidBody.SixIndependentModesHaveZeroStrainEnergy` | 3개 병진과 3개 회전 mode의 energy가 roundoff bound 이내 |
|
||||||
|
| 축·비틀림 | `Timoshenko.ReproducesAnalyticalAxialSubmatrix`, `ReproducesAnalyticalTorsionalSubmatrix` | 해석 stiffness 대비 `512*epsilon` 상대 규모 |
|
||||||
|
| 굽힘 y/z | `Timoshenko.ReproducesConstantCurvatureEnergyAboutLocalY/LocalZ` | 해석 strain energy 대비 `512*epsilon` 상대 규모 |
|
||||||
|
| 전단 y/z | `Timoshenko.ReproducesConstantShearEnergyInLocalY/LocalZ` | 해석 strain energy 대비 `512*epsilon` 상대 규모 |
|
||||||
|
| 세장비 | `Timoshenko.AvoidsShearLockingAcrossSlendernessSweep` | `L/h={2,10,100,1000}`, `4096*epsilon*(L/h)^2` 상대 규모 |
|
||||||
|
| 회전 불변성 | `Beam3D2.PreservesGlobalEnergyUnderRigidCoordinateRotation` | 회전 전후 global energy가 `4096*epsilon` 상대 규모 이내 |
|
||||||
|
| 단면력 회복 | `BeamRecovery.*`, `SectionForce.*` | 축력, 비틀림, 전단, My, Mz와 biaxial 부호를 양 끝에서 검증 |
|
||||||
|
|
||||||
|
### 2.2 Physics sanity와 결정성
|
||||||
|
|
||||||
|
| 검증 | 증거 | 결과 |
|
||||||
|
|------|------|------|
|
||||||
|
| full-system 평형 | `StaticEquilibrium.ReturnedFieldsSatisfyOriginalFullEquation` | `Ku-f-r` 각 DOF가 `1e-12` 이내 |
|
||||||
|
| 비영 지정 DOF | `ConstraintElimination.ShiftsNonzeroPrescribedValueAndPreservesOriginalSystem` 및 `LinearStaticAnalysis.SolvesAllConstrainedSystemWithoutPardiso` | reduced RHS 이동, full displacement와 reaction 검증 |
|
||||||
|
| 반력 | `Reaction.UsesOriginalFullEquilibriumEquation` | 원래 full equation에서 회복 |
|
||||||
|
| production pipeline | `MinimalCantileverPipeline.WritesReadableFiniteEquilibratedResults` | parser→solver→HDF5 결과 유한, 전역 반력 평형 `1e-12` 이내 |
|
||||||
|
| reference 평형 | `CantileverReference.CorrelatesAllAvailableAbaqusResults` | 절대 `1e-12`와 적용 하중 대비 상대 `2e-13` 중 큰 허용치 이내 |
|
||||||
|
| 병렬 결정성 | `ThreadCountDeterminism.AssemblyAndLinearStateAreBitwiseStable` | thread `{1,2,16}`에서 10회 반복, matrix/displacement/reaction bitwise 동일 |
|
||||||
|
|
||||||
|
Gate A disposition은 **PASS**다.
|
||||||
|
|
||||||
|
## 3. Gate B — Abaqus 2024 결과 상관성
|
||||||
|
|
||||||
|
### 3.1 모델과 비교 계약
|
||||||
|
|
||||||
|
- Abaqus provenance: `reference/cantilever beam/cantilever beam.inp`와 변위, 반력,
|
||||||
|
요소 단면력 CSV. 원본 모델의 `SCF=0.25`는 유지한다.
|
||||||
|
- FESA projection: `reference/cantilever beam/cantilever beam fesa.inp`. 기하, 재료,
|
||||||
|
단면, 명시적 전단강성, 경계조건과 하중은 같고 `SCF=0`만 적용한다.
|
||||||
|
- entity join: nodal 결과 11개씩은 `(Instance, Node Label)`, 요소 단면력 20개는
|
||||||
|
`(Instance, Element Label, End Node Label)`로 매칭한다.
|
||||||
|
- absolute scale: displacement `1e-10`, reaction `1e-8`, internal force `1e-8`.
|
||||||
|
- 서로 단위가 다른 component는 하나의 norm으로 합치지 않는다.
|
||||||
|
|
||||||
|
요소력 축 순서는 다음과 같다.
|
||||||
|
|
||||||
|
| FESA | Abaqus CSV |
|
||||||
|
|------|------------|
|
||||||
|
| `N` | `SF1` |
|
||||||
|
| `Vy` | `SF3` |
|
||||||
|
| `Vz` | `SF2` |
|
||||||
|
| `T` | `SM3` |
|
||||||
|
| `My` | `SM1` |
|
||||||
|
| `Mz` | `SM2` |
|
||||||
|
|
||||||
|
component \(c\)의 표본 수를 \(n\), 오차를 \(e_i=a_i-r_i\), absolute scale을
|
||||||
|
\(s_i\)라 하면 다음을 사용한다.
|
||||||
|
|
||||||
|
\[
|
||||||
|
\operatorname{RMSE}_c=\sqrt{\frac{1}{n}\sum_i e_i^2}
|
||||||
|
\]
|
||||||
|
|
||||||
|
\[
|
||||||
|
\operatorname{RelativeL2}_c=
|
||||||
|
\frac{\sqrt{\sum_i e_i^2}}
|
||||||
|
{\max\left(\sqrt{\sum_i r_i^2},\sqrt{\sum_i s_i^2}\right)}
|
||||||
|
\]
|
||||||
|
|
||||||
|
### 3.2 수집된 correlation metric
|
||||||
|
|
||||||
|
| 물리량 | component | count | RMSE | Relative L2 |
|
||||||
|
|--------|-----------|------:|-----:|------------:|
|
||||||
|
| displacement | Ux | 11 | 0 | 0 |
|
||||||
|
| displacement | Uy | 11 | 0 | 0 |
|
||||||
|
| displacement | Uz | 11 | 2.1970445023044601e-05 | 2.2349969291714038e-03 |
|
||||||
|
| displacement | Rx | 11 | 0 | 0 |
|
||||||
|
| displacement | Ry | 11 | 2.1672945177750166e-09 | 1.0416581667076092e-06 |
|
||||||
|
| displacement | Rz | 11 | 0 | 0 |
|
||||||
|
| reaction | RFx | 11 | 0 | 0 |
|
||||||
|
| reaction | RFy | 11 | 0 | 0 |
|
||||||
|
| reaction | RFz | 11 | 1.3385150002853335e-07 | 4.4393520322089023e-13 |
|
||||||
|
| reaction | RMx | 11 | 0 | 0 |
|
||||||
|
| reaction | RMy | 11 | 7.8149308366928907e-07 | 2.591919334788851e-13 |
|
||||||
|
| reaction | RMz | 11 | 0 | 0 |
|
||||||
|
| internal force | N | 20 | 0 | 0 |
|
||||||
|
| internal force | Vy | 20 | 0 | 0 |
|
||||||
|
| internal force | Vz | 20 | 2.7107472798172421e-07 | 2.7107472798172426e-13 |
|
||||||
|
| internal force | T | 20 | 0 | 0 |
|
||||||
|
| internal force | My | 20 | 474341.64902538335 | 0.082541022764834646 |
|
||||||
|
| internal force | Mz | 20 | 0 | 0 |
|
||||||
|
|
||||||
|
모든 요청 entity/component가 유일하게 매칭됐고 metric이 유한하므로 Gate B
|
||||||
|
disposition은 **EVALUABLE**이다. 이는 Abaqus B31과 FESA의 정식화가 동일하거나
|
||||||
|
임의 정확도 threshold를 통과했다는 뜻이 아니다. 현재 가장 큰 Relative L2는
|
||||||
|
`My=0.082541022764834646`, 다음은 `Uz=0.0022349969291714038`이다. pass/fail
|
||||||
|
envelope는 mesh 또는 해석적 연구 근거 없이 이 관측값에 맞춰 설정하지 않는다.
|
||||||
|
|
||||||
|
## 4. 검증 범위와 제한
|
||||||
|
|
||||||
|
- Abaqus 단면 도심 응력 CSV는 제공되지 않았다. `StressCsv`는 synthetic CSV schema와
|
||||||
|
`(Instance, Element, End Node)` 매핑만 검증하며, 응력은 **not yet
|
||||||
|
Abaqus-qualified**다.
|
||||||
|
- Gate B는 solver 간 correlation이며 Gate A의 해석적 정확도 검증을 대체하지 않는다.
|
||||||
|
- Abaqus golden 갱신에는 Abaqus 2024 환경과 수동 provenance 검토가 필요하다.
|
||||||
|
- FESA는 단위 변환을 수행하지 않으므로 입력과 CSV가 일관 단위계를 사용해야 한다.
|
||||||
|
|
||||||
|
현재 이중 gate 결론은 **Gate A PASS / Gate B EVALUABLE**이다.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <fesa/assembly/equation_system.hpp>
|
||||||
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct AssemblyOptions final {
|
||||||
|
std::size_t max_threads;
|
||||||
|
std::size_t grain_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] EquationSystem assemble_parallel(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs,
|
||||||
|
AssemblyOptions options);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/assembly/symmetric_csr.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct MatrixContribution final {
|
||||||
|
std::size_t row;
|
||||||
|
std::size_t column;
|
||||||
|
ElementId element;
|
||||||
|
std::uint16_t local_order;
|
||||||
|
double value;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<MatrixContribution> canonicalize_contributions(
|
||||||
|
std::span<const MatrixContribution> contributions);
|
||||||
|
|
||||||
|
[[nodiscard]] SymmetricCsr merge_contributions(
|
||||||
|
std::size_t order,
|
||||||
|
std::span<const MatrixContribution> canonical);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -2,22 +2,34 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
#include <fesa/core/vec3.hpp>
|
#include <fesa/core/vec3.hpp>
|
||||||
#include <fesa/fem/beam_frame.hpp>
|
#include <fesa/fem/beam_frame.hpp>
|
||||||
#include <fesa/model/beam_section.hpp>
|
#include <fesa/model/beam_section.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
#include <fesa/model/material.hpp>
|
#include <fesa/model/material.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
struct Beam3D2Input final {
|
struct Beam3D2Input final {
|
||||||
std::array<Vec3, 2> coordinates;
|
std::array<Vec3, 2> coordinates;
|
||||||
|
std::array<NodeId, 2> node_ids;
|
||||||
IsotropicElastic material;
|
IsotropicElastic material;
|
||||||
BeamSection section;
|
BeamSection section;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct BeamSectionResult final {
|
||||||
|
double xi;
|
||||||
|
NodeId end_node;
|
||||||
|
std::array<double, 6> section_strain;
|
||||||
|
std::array<double, 6> section_force;
|
||||||
|
double centroid_sigma_xx;
|
||||||
|
std::vector<double> sigma_xx;
|
||||||
|
};
|
||||||
|
|
||||||
struct Beam3D2Contribution final {
|
struct Beam3D2Contribution final {
|
||||||
Matrix12 local_stiffness;
|
Matrix12 local_stiffness;
|
||||||
Matrix12 global_stiffness;
|
Matrix12 global_stiffness;
|
||||||
@@ -32,4 +44,9 @@ struct BeamKernelResult final {
|
|||||||
[[nodiscard]] BeamKernelResult compute_beam3d2(
|
[[nodiscard]] BeamKernelResult compute_beam3d2(
|
||||||
const Beam3D2Input& input);
|
const Beam3D2Input& input);
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<BeamSectionResult> recover_beam3d2(
|
||||||
|
const Beam3D2Input& input,
|
||||||
|
std::span<const double, 12> element_displacement,
|
||||||
|
std::span<const std::array<double, 2>> recovery_points);
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
@@ -12,6 +13,7 @@ namespace fesa {
|
|||||||
struct ParseDeckResult final {
|
struct ParseDeckResult final {
|
||||||
std::optional<ParsedDeck> deck;
|
std::optional<ParsedDeck> deck;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
std::string input_fingerprint;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] ParseDeckResult parse_deck(
|
[[nodiscard]] ParseDeckResult parse_deck(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
@@ -26,33 +27,74 @@ struct Hdf5NodeSnapshot final {
|
|||||||
struct Hdf5ElementSnapshot final {
|
struct Hdf5ElementSnapshot final {
|
||||||
std::uint64_t dense_index;
|
std::uint64_t dense_index;
|
||||||
ElementId id;
|
ElementId id;
|
||||||
|
EntityOrigin origin;
|
||||||
std::array<std::uint64_t, 2> connectivity;
|
std::array<std::uint64_t, 2> connectivity;
|
||||||
|
MaterialId material;
|
||||||
SectionId section;
|
SectionId section;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5SectionSnapshot final {
|
struct Hdf5SectionSnapshot final {
|
||||||
SectionId id;
|
SectionId id;
|
||||||
|
std::string name;
|
||||||
|
double area;
|
||||||
|
double iy;
|
||||||
|
double iz;
|
||||||
|
double torsion_j;
|
||||||
double shear_area_y;
|
double shear_area_y;
|
||||||
double shear_area_z;
|
double shear_area_z;
|
||||||
ShearPropertySource shear_source;
|
ShearPropertySource shear_source;
|
||||||
|
Vec3 orientation;
|
||||||
|
std::vector<std::array<double, 2>> recovery_points;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5ModelSnapshot final {
|
struct Hdf5ModelSnapshot final {
|
||||||
std::vector<Hdf5NodeSnapshot> nodes;
|
std::vector<Hdf5NodeSnapshot> nodes;
|
||||||
std::vector<Hdf5ElementSnapshot> elements;
|
std::vector<Hdf5ElementSnapshot> elements;
|
||||||
|
std::vector<IsotropicElastic> materials;
|
||||||
std::vector<Hdf5SectionSnapshot> sections;
|
std::vector<Hdf5SectionSnapshot> sections;
|
||||||
|
std::vector<NodeSet> node_sets;
|
||||||
|
std::vector<ElementSet> element_sets;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5MetadataSnapshot final {
|
||||||
|
std::string schema_version;
|
||||||
|
std::string fesa_version;
|
||||||
|
std::string unit_policy;
|
||||||
|
std::string input_source;
|
||||||
|
std::string input_fingerprint;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5InputIdentity final {
|
||||||
|
std::string source;
|
||||||
|
std::string fingerprint;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5SolverSettingsSnapshot final {
|
||||||
|
std::string backend;
|
||||||
|
std::string matrix_storage;
|
||||||
|
std::string matrix_type;
|
||||||
|
std::string constraint_method;
|
||||||
|
std::string assembly;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5AnalysisSnapshot final {
|
||||||
|
StepDefinition step;
|
||||||
|
Hdf5SolverSettingsSnapshot solver;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5ReadResult final {
|
struct Hdf5ReadResult final {
|
||||||
std::optional<ResultDatabase> database;
|
std::optional<ResultDatabase> database;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
std::optional<Hdf5ModelSnapshot> model;
|
std::optional<Hdf5ModelSnapshot> model;
|
||||||
|
std::optional<Hdf5MetadataSnapshot> metadata;
|
||||||
|
std::optional<Hdf5AnalysisSnapshot> analysis;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
const ResultDatabase& database);
|
const ResultDatabase& database,
|
||||||
|
const Hdf5InputIdentity& input_identity);
|
||||||
|
|
||||||
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
||||||
const std::filesystem::path& path);
|
const std::filesystem::path& path);
|
||||||
|
|||||||
@@ -2,23 +2,62 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
#include <fesa/core/status.hpp>
|
#include <fesa/core/status.hpp>
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
|
#include <fesa/model/entity_origin.hpp>
|
||||||
#include <fesa/model/ids.hpp>
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
|
enum class FieldCoordinateSystem { global, element_local };
|
||||||
|
|
||||||
struct NodalFrame final {
|
struct NodalFrame final {
|
||||||
|
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||||
|
FieldCoordinateSystem::global;
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
displacement_components{
|
||||||
|
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"};
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
reaction_components{
|
||||||
|
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"};
|
||||||
|
|
||||||
std::vector<NodeId> node_ids;
|
std::vector<NodeId> node_ids;
|
||||||
|
std::vector<EntityOrigin> origins;
|
||||||
std::vector<std::array<double, 6>> displacement;
|
std::vector<std::array<double, 6>> displacement;
|
||||||
std::vector<std::array<double, 6>> reaction;
|
std::vector<std::array<double, 6>> reaction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct BeamElementFrame final {
|
||||||
|
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||||
|
FieldCoordinateSystem::element_local;
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
section_strain_components{
|
||||||
|
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||||
|
"kappa_z"};
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
section_force_components{
|
||||||
|
"N", "Vy", "Vz", "T", "My", "Mz"};
|
||||||
|
inline static constexpr std::string_view axial_stress_component =
|
||||||
|
"sigma_xx";
|
||||||
|
|
||||||
|
ElementId element;
|
||||||
|
EntityOrigin origin;
|
||||||
|
BeamFrame local_frame;
|
||||||
|
std::array<BeamSectionResult, 2> end_results;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ElementFrame final {
|
||||||
|
std::vector<BeamElementFrame> beams;
|
||||||
|
};
|
||||||
|
|
||||||
struct ResultFrame final {
|
struct ResultFrame final {
|
||||||
double step_time;
|
double step_time;
|
||||||
NodalFrame nodal;
|
NodalFrame nodal;
|
||||||
|
ElementFrame element;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/results/result_database.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
enum class ReferenceQuantity {
|
||||||
|
displacement,
|
||||||
|
reaction,
|
||||||
|
internal_force,
|
||||||
|
centroid_stress
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Tolerance final {
|
||||||
|
double relative;
|
||||||
|
double absolute_scale;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResultPosition final {
|
||||||
|
std::string instance_name;
|
||||||
|
std::int64_t entity_label;
|
||||||
|
std::optional<std::int64_t> end_node_label;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ComparisonSample final {
|
||||||
|
ReferenceQuantity quantity;
|
||||||
|
ResultPosition position;
|
||||||
|
std::vector<double> reference;
|
||||||
|
std::vector<double> actual;
|
||||||
|
Tolerance tolerance;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ComparisonReport final {
|
||||||
|
bool passed;
|
||||||
|
double maximum_normalized_error;
|
||||||
|
std::vector<Diagnostic> failures;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ComponentCorrelationMetric final {
|
||||||
|
ReferenceQuantity quantity;
|
||||||
|
std::size_t component_index;
|
||||||
|
std::size_t value_count;
|
||||||
|
double root_mean_square_error;
|
||||||
|
double relative_l2_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CorrelationReport final {
|
||||||
|
bool evaluable;
|
||||||
|
std::vector<ComponentCorrelationMetric> metrics;
|
||||||
|
std::vector<Diagnostic> failures;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ComparisonSampleMatch final {
|
||||||
|
std::optional<ComparisonSample> sample;
|
||||||
|
std::vector<Diagnostic> failures;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ComparisonSampleMatch make_comparison_sample(
|
||||||
|
const ResultFrame& frame,
|
||||||
|
ReferenceQuantity quantity,
|
||||||
|
const ResultPosition& position,
|
||||||
|
std::span<const double> reference,
|
||||||
|
Tolerance tolerance);
|
||||||
|
|
||||||
|
[[nodiscard]] ComparisonReport compare_samples(
|
||||||
|
std::span<const ComparisonSample> samples);
|
||||||
|
|
||||||
|
[[nodiscard]] CorrelationReport correlate_samples(
|
||||||
|
std::span<const ComparisonSample> samples);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/validation/comparison.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct ReferenceRow final {
|
||||||
|
ReferenceQuantity quantity;
|
||||||
|
ResultPosition position;
|
||||||
|
std::vector<double> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReferenceCsvReadResult final {
|
||||||
|
std::vector<ReferenceRow> rows;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ReferenceCsvReadResult read_reference_csv(
|
||||||
|
ReferenceQuantity quantity,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
std::string_view single_instance_name);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -5,22 +5,35 @@
|
|||||||
{
|
{
|
||||||
"step": 0,
|
"step": 0,
|
||||||
"name": "comparison-metric-and-entity-matching",
|
"name": "comparison-metric-and-entity-matching",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added CSV-independent normalized comparison metrics and diagnostics with ResultFrame origin and Beam end-node matching.",
|
||||||
|
"started_at": "2026-08-02T02:42:01+0900",
|
||||||
|
"completed_at": "2026-08-02T03:22:08+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 1,
|
"step": 1,
|
||||||
"name": "reference-csv-adapters",
|
"name": "reference-csv-adapters",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added strict four-schema Abaqus reference CSV adapters with canonical row mapping, synthetic internal-force/stress fixtures, and validation diagnostics.",
|
||||||
|
"started_at": "2026-08-02T03:22:08+0900",
|
||||||
|
"completed_at": "2026-08-02T03:33:06+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 2,
|
"step": 2,
|
||||||
"name": "cantilever-reference-comparison",
|
"name": "cantilever-reference-comparison",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added the SCF=0 FESA cantilever projection, production HDF5/CSV correlation CLI, component-wise RMSE and Relative L2, Abaqus-to-FESA beam-force axis mapping, and displacement/reaction/internal-force reference coverage.",
|
||||||
|
"started_at": "2026-08-02T03:33:07+0900",
|
||||||
|
"completed_at": "2026-08-03T01:45:42+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 3,
|
"step": 3,
|
||||||
"name": "qualification-report",
|
"name": "qualification-report",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Recorded docs/VALIDATION.md with Gate A PASS, Gate B EVALUABLE, 68/68 CTest and 20/20 Harness pytest evidence, all 18 correlation metrics, determinism coverage, and the remaining Abaqus stress qualification gap.",
|
||||||
|
"started_at": "2026-08-03T01:45:43+0900",
|
||||||
|
"completed_at": "2026-08-03T01:48:16+0900"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"created_at": "2026-08-02T02:42:01+0900"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"step": 2,
|
||||||
|
"name": "cantilever-reference-comparison",
|
||||||
|
"exitCode": 0,
|
||||||
|
"stdout": "Harness child verification was blocked by the managed WindowsApps PowerShell sandbox. The same acceptance contract was completed directly: CMake Debug build passed; CTest passed 68/68; Harness pytest passed 20/20; the fresh solver and reference-comparison CLI produced finite component metrics for 11 displacement rows, 11 reaction rows, and 20 internal-force rows.",
|
||||||
|
"stderr": ""
|
||||||
|
}
|
||||||
@@ -6,45 +6,73 @@
|
|||||||
- `/docs/PRD.md`
|
- `/docs/PRD.md`
|
||||||
- `/docs/ARCHITECTURE.md`
|
- `/docs/ARCHITECTURE.md`
|
||||||
- `/docs/ADR.md`
|
- `/docs/ADR.md`
|
||||||
|
- `/docs/formulation/timoshenko-beam-3d.md`
|
||||||
- `/reference/cantilever beam/cantilever beam.inp`
|
- `/reference/cantilever beam/cantilever beam.inp`
|
||||||
- `/reference/cantilever beam/cantilever beam displacements.csv`
|
- `/reference/cantilever beam/cantilever beam displacements.csv`
|
||||||
- `/reference/cantilever beam/cantilever beam reactions.csv`
|
- `/reference/cantilever beam/cantilever beam reactions.csv`
|
||||||
|
- `/reference/cantilever beam/cantilever beam elemental forces.csv`
|
||||||
- `/include/fesa/analysis/run_solver.hpp`
|
- `/include/fesa/analysis/run_solver.hpp`
|
||||||
- `/include/fesa/io/hdf5/reader.hpp`
|
- `/include/fesa/io/hdf5/writer.hpp`
|
||||||
|
- `/include/fesa/validation/comparison.hpp`
|
||||||
- `/include/fesa/validation/reference_csv.hpp`
|
- `/include/fesa/validation/reference_csv.hpp`
|
||||||
|
|
||||||
## 작업
|
## 작업
|
||||||
|
|
||||||
제공된 계층형 캔틸레버를 production pipeline으로 해석하고 현재 존재하는 변위와
|
FESA 정식화 적합성과 Abaqus 결과 상관성을 별도 gate로 검증한다.
|
||||||
반력만 Abaqus 2024 결과와 비교한다.
|
|
||||||
|
|
||||||
- `tests/reference/cantilever_reference_test.cpp`와 reference compare CLI를 먼저
|
- Gate A는 기존 해석해, energy, rigid mode 및 equilibrium 테스트를 그대로 엄격히
|
||||||
작성한다.
|
통과시킨다. `SCF=0.25`를 kernel에 추가하거나 production parser가 무시하게 하지
|
||||||
- comparison request는 Instance `Part-1-1`, relative tolerance `1e-5`,
|
않는다.
|
||||||
displacement absolute scale `1e-10`, reaction absolute scale `1e-8`을 명시한다.
|
- 원본 `cantilever beam.inp`와 세 CSV는 Abaqus provenance로 보존한다. 동일한
|
||||||
- HDF5 결과와 CSV를 public adapter로 읽어 `(Instance,Node Label)`로 join한다.
|
기하·재료·하중과 전단강성을 사용하되 `SCF=0`인
|
||||||
- 요청하지 않은 internal force/stress 파일을 검색하거나 pass로 보고하지 않는다.
|
`reference/cantilever beam/cantilever beam fesa.inp`를 추가해 production
|
||||||
- equilibrium과 finite result도 함께 assertion한다.
|
pipeline으로 해석한다.
|
||||||
|
- `tests/unit/validation/comparison_test.cpp`에 component별 RMSE와 Relative L2의
|
||||||
|
실패 테스트를 먼저 추가한다. `include/fesa/validation/comparison.hpp`에는
|
||||||
|
`ComponentCorrelationMetric`과 `CorrelationReport`,
|
||||||
|
`correlate_samples(std::span<const ComparisonSample>)`를 공개한다.
|
||||||
|
- Relative L2는 reference L2 norm과 component별 absolute-scale norm 중 큰 값을
|
||||||
|
분모로 사용한다. 서로 다른 component를 하나의 norm으로 합치지 않는다.
|
||||||
|
- `tests/unit/validation/reference_csv_test.cpp`의 internal-force fixture 기대값을
|
||||||
|
Abaqus `SF1,SF3,SF2,SM3,SM1,SM2`에서 FESA
|
||||||
|
`N,Vy,Vz,T,My,Mz` 순서로 재배열하도록 먼저 변경하고 RED를 확인한다.
|
||||||
|
- `tests/reference/cantilever_reference_test.cpp`와 reference compare CLI는 Instance
|
||||||
|
`PART-1_1-1`, 변위, 반력 및 요소 단면력 CSV 경로와 각 물리량의 absolute scale을
|
||||||
|
명시한다.
|
||||||
|
- HDF5 결과와 CSV를 public adapter로 읽어 nodal 결과는
|
||||||
|
`(Instance,Node Label)`, 요소 단면력은
|
||||||
|
`(Instance,Element Label,End Node Label)`로 join한다.
|
||||||
|
- correlation CLI는 요청된 결과가 모두 매칭되고 metric이 유한할 때 성공하며
|
||||||
|
component별 `count`, `rmse`, `relative_l2`를 출력한다. 관측값을 이용한 임의
|
||||||
|
pass/fail tolerance를 적용하지 않는다.
|
||||||
|
- equilibrium과 finite result도 함께 assertion한다. 요청하지 않은 stress 파일을
|
||||||
|
검색하거나 pass로 보고하지 않는다.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake --build --preset windows-debug
|
cmake --build --preset windows-debug
|
||||||
|
ctest --preset windows-debug -R "CorrelationMetric|ReferenceCsv|InternalForceCsv" --output-on-failure
|
||||||
ctest --preset windows-debug -R CantileverReference --output-on-failure
|
ctest --preset windows-debug -R CantileverReference --output-on-failure
|
||||||
.\out\build\windows-debug\Debug\fesa.exe solve "reference\cantilever beam\cantilever beam.inp" --output out\cantilever-beam.h5
|
.\out\build\windows-debug\Debug\fesa.exe solve "reference\cantilever beam\cantilever beam fesa.inp" --output out\cantilever-beam.h5
|
||||||
.\out\build\windows-debug\Debug\fesa-reference-compare.exe --results out\cantilever-beam.h5 --instance Part-1-1 --displacements "reference\cantilever beam\cantilever beam displacements.csv" --reactions "reference\cantilever beam\cantilever beam reactions.csv" --relative-tolerance 1e-5 --displacement-absolute-scale 1e-10 --reaction-absolute-scale 1e-8
|
.\out\build\windows-debug\Debug\fesa-reference-compare.exe --results out\cantilever-beam.h5 --instance PART-1_1-1 --displacements "reference\cantilever beam\cantilever beam displacements.csv" --reactions "reference\cantilever beam\cantilever beam reactions.csv" --internal-forces "reference\cantilever beam\cantilever beam elemental forces.csv" --displacement-absolute-scale 1e-10 --reaction-absolute-scale 1e-8 --internal-force-absolute-scale 1e-8
|
||||||
ctest --preset windows-debug --output-on-failure
|
ctest --preset windows-debug --output-on-failure
|
||||||
```
|
```
|
||||||
|
|
||||||
## 검증 절차
|
## 검증 절차
|
||||||
|
|
||||||
1. reference test가 실제 오차를 보고하며 실패하는 것을 확인한다.
|
1. RMSE, Relative L2 및 요소력 축 재배열 테스트가 각각 의도한 이유로 실패하는
|
||||||
2. discrepancy마다 가장 작은 analytical test를 추가한 뒤 근거 있는 kernel만 수정한다.
|
RED를 확인한다.
|
||||||
3. tolerance를 넓혀 결함을 숨기지 않는다.
|
2. 최소 comparison metric과 CSV adapter 변경으로 GREEN을 만든다.
|
||||||
4. 전체 테스트와 최대 정규화 오차를 index summary에 기록한다.
|
3. reference test가 production solve/HDF5/CSV/correlation 경로를 실행하고 세
|
||||||
|
물리량의 component metric을 출력하는지 확인한다.
|
||||||
|
4. 전체 테스트와 component별 metric 요약을 index summary에 기록한다.
|
||||||
|
|
||||||
## 금지사항
|
## 금지사항
|
||||||
|
|
||||||
- reference `.inp` 또는 CSV를 수정하지 마라. 이유: 원본 golden을 보존해야 한다.
|
- 원본 Abaqus `.inp` 또는 CSV를 수정하지 마라. 이유: 원본 golden과 formulation
|
||||||
- 미제공 내력/응력 Abaqus 검증을 통과했다고 주장하지 마라. 이유: 증거가 없다.
|
provenance를 보존해야 한다.
|
||||||
|
- Abaqus `SCF=0.25`를 FESA가 구현하거나 무시하지 마라. 이유: 승인된 FESA
|
||||||
|
정식화와 입력 계약을 바꾼다.
|
||||||
|
- 미제공 응력 Abaqus 검증을 통과했다고 주장하지 마라. 이유: 증거가 없다.
|
||||||
- test-only parser/solver 경로를 만들지 마라. 이유: production pipeline 검증이다.
|
- test-only parser/solver 경로를 만들지 마라. 이유: production pipeline 검증이다.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"name": "qualification-report",
|
||||||
|
"exitCode": 0,
|
||||||
|
"stdout": "Created docs/VALIDATION.md from fresh evidence. CMake Debug build passed, CTest passed 68/68, Harness pytest passed 20/20, and the focused Beam3D2/ThreadCountDeterminism/Reference tests passed 4/4. The report records Gate A PASS, Gate B EVALUABLE, all 18 finite correlation metrics, and Abaqus stress as not yet qualified.",
|
||||||
|
"stderr": ""
|
||||||
|
}
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
rotated frame, slenderness sweep
|
rotated frame, slenderness sweep
|
||||||
- physics: equilibrium, symmetry, reaction, nonzero prescribed DOF
|
- physics: equilibrium, symmetry, reaction, nonzero prescribed DOF
|
||||||
- determinism: tested thread counts와 repeated runs
|
- determinism: tested thread counts와 repeated runs
|
||||||
- Abaqus: 현재 cantilever displacement/reaction의 tolerance와 maximum error
|
- Abaqus correlation: 현재 cantilever displacement/reaction/internal-force의
|
||||||
- contract-only: synthetic internal-force/stress CSV schema와 component mapping
|
component별 RMSE와 Relative L2
|
||||||
- 미제공 Abaqus internal-force/stress는 `not yet Abaqus-qualified`라고 명시한다.
|
- contract-only: synthetic stress CSV schema와 component mapping
|
||||||
|
- 미제공 Abaqus stress는 `not yet Abaqus-qualified`라고 명시한다.
|
||||||
|
|
||||||
보고서 수치를 새 test output에서 수집하며 수동 추정값을 쓰지 않는다.
|
보고서 수치를 새 test output에서 수집하며 수동 추정값을 쓰지 않는다.
|
||||||
|
|
||||||
@@ -34,18 +35,18 @@ ctest --preset windows-debug --output-on-failure
|
|||||||
ctest --preset windows-debug -R "Reference|Beam3D2|Determinism" --output-on-failure
|
ctest --preset windows-debug -R "Reference|Beam3D2|Determinism" --output-on-failure
|
||||||
```
|
```
|
||||||
|
|
||||||
모든 실행이 통과하고 보고서의 test 이름, tolerance, 최대오차와 disposition이 실제
|
모든 실행이 통과하고 보고서의 test 이름, component별 RMSE·Relative L2와
|
||||||
출력과 일치해야 한다.
|
disposition이 실제 출력과 일치해야 한다.
|
||||||
|
|
||||||
## 검증 절차
|
## 검증 절차
|
||||||
|
|
||||||
1. 전체 suite를 새로 실행한다.
|
1. 전체 suite를 새로 실행한다.
|
||||||
2. 결과를 benchmark/quantity별 표에 기록한다.
|
2. 결과를 benchmark/quantity별 표에 기록한다.
|
||||||
3. synthetic coverage와 Abaqus-backed qualification을 명확히 분리한다.
|
3. FESA 정식화 gate, Abaqus correlation 및 synthetic coverage를 명확히 분리한다.
|
||||||
4. index summary에 보고서 경로와 test counts를 기록한다.
|
4. index summary에 보고서 경로와 test counts를 기록한다.
|
||||||
|
|
||||||
## 금지사항
|
## 금지사항
|
||||||
|
|
||||||
- 실행하지 않은 결과를 보고서에 쓰지 마라. 이유: 검증 증거가 아니다.
|
- 실행하지 않은 결과를 보고서에 쓰지 마라. 이유: 검증 증거가 아니다.
|
||||||
- Abaqus 내력/응력 qualification을 추론하지 마라. 이유: CSV가 아직 없다.
|
- Abaqus 응력 qualification을 추론하지 마라. 이유: CSV가 아직 없다.
|
||||||
- 실패 테스트를 제외하거나 disable하지 마라. 이유: release gate를 약화한다.
|
- 실패 테스트를 제외하거나 disable하지 마라. 이유: release gate를 약화한다.
|
||||||
|
|||||||
@@ -5,17 +5,28 @@
|
|||||||
{
|
{
|
||||||
"step": 0,
|
"step": 0,
|
||||||
"name": "canonical-contribution-order",
|
"name": "canonical-contribution-order",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added canonical MatrixContribution ordering and deterministic upper-triangle CSR merge with bitwise permutation, cancellation, signed-zero, and serial-oracle coverage.",
|
||||||
|
"started_at": "2026-08-01T22:17:55+0900",
|
||||||
|
"completed_at": "2026-08-01T23:06:21+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 1,
|
"step": 1,
|
||||||
"name": "tbb-element-evaluation",
|
"name": "tbb-element-evaluation",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added AssemblyOptions and oneTBB Beam element evaluation with element-local contributions, canonical serial merge, explicit execution limits, and bitwise serial parity tests.",
|
||||||
|
"started_at": "2026-08-01T23:06:22+0900",
|
||||||
|
"completed_at": "2026-08-01T23:21:27+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 2,
|
"step": 2,
|
||||||
"name": "thread-count-determinism",
|
"name": "thread-count-determinism",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added 10-run bitwise CSR, RHS, displacement, and reaction checks at 1, 2, and 16 threads plus a production serial/parallel assembly benchmark.",
|
||||||
|
"started_at": "2026-08-01T23:21:27+0900",
|
||||||
|
"completed_at": "2026-08-01T23:36:17+0900"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
"created_at": "2026-08-01T22:17:55+0900",
|
||||||
|
"completed_at": "2026-08-01T23:36:18+0900"
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
-4
@@ -32,19 +32,22 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "deterministic-parallel-assembly",
|
"dir": "deterministic-parallel-assembly",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"completed_at": "2026-08-01T23:36:18+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "result-contract-completion",
|
"dir": "result-contract-completion",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"completed_at": "2026-08-02T01:51:25+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "beam-reference-qualification",
|
"dir": "beam-reference-qualification",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"completed_at": "2026-08-03T01:48:16+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "internal-release",
|
"dir": "internal-release",
|
||||||
"status": "pending"
|
"status": "pending"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,28 @@
|
|||||||
{
|
{
|
||||||
"step": 0,
|
"step": 0,
|
||||||
"name": "beam-element-end-recovery",
|
"name": "beam-element-end-recovery",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added actual Beam node IDs and signed two-end strain, resultant, centroid/recovery-point stress recovery using the stiffness frame and reduced-shear convention, with hand-calculated tests.",
|
||||||
|
"started_at": "2026-08-02T00:27:47+0900",
|
||||||
|
"completed_at": "2026-08-02T01:14:40+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 1,
|
"step": 1,
|
||||||
"name": "complete-result-contract",
|
"name": "complete-result-contract",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added nodal and Beam element provenance, explicit field metadata and validation, and production element-end recovery orchestration in LinearStaticAnalysis.",
|
||||||
|
"started_at": "2026-08-02T01:14:43+0900",
|
||||||
|
"completed_at": "2026-08-02T01:28:37+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 2,
|
"step": 2,
|
||||||
"name": "self-contained-hdf5",
|
"name": "self-contained-hdf5",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Defined schema 2.0.0 and completed self-contained HDF5 model, analysis, nodal/Beam result, diagnostic writer/reader round trips with strict metadata and version validation.",
|
||||||
|
"started_at": "2026-08-02T01:28:37+0900",
|
||||||
|
"completed_at": "2026-08-02T01:51:24+0900"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
"created_at": "2026-08-02T00:27:47+0900",
|
||||||
|
"completed_at": "2026-08-02T01:51:25+0900"
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,12 +1,12 @@
|
|||||||
Node Label, U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3
|
Part Instance Name, Node Label, U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3
|
||||||
1,0,0,-1.00E-32,0,1.00E-31,0
|
PART-1_1-1,1,0.000000E+00,0.000000E+00,-1.000000E-30,0.000000E+00,1.000000E-29,0.000000E+00
|
||||||
2,0,0,-2.87E-06,0,5.43E-06,0
|
PART-1_1-1,2,0.000000E+00,0.000000E+00,-2.900000E-04,0.000000E+00,5.428570E-04,0.000000E+00
|
||||||
3,0,0,-1.09E-05,0,1.03E-05,0
|
PART-1_1-1,3,0.000000E+00,0.000000E+00,-1.094290E-03,0.000000E+00,1.028570E-03,0.000000E+00
|
||||||
4,0,0,-2.35E-05,0,1.46E-05,0
|
PART-1_1-1,4,0.000000E+00,0.000000E+00,-2.355720E-03,0.000000E+00,1.457140E-03,0.000000E+00
|
||||||
5,0,0,-4.00E-05,0,1.83E-05,0
|
PART-1_1-1,5,0.000000E+00,0.000000E+00,-4.017140E-03,0.000000E+00,1.828570E-03,0.000000E+00
|
||||||
6,0,0,-6.01E-05,0,2.14E-05,0
|
PART-1_1-1,6,0.000000E+00,0.000000E+00,-6.021430E-03,0.000000E+00,2.142860E-03,0.000000E+00
|
||||||
7,0,0,-8.29E-05,0,2.40E-05,0
|
PART-1_1-1,7,0.000000E+00,0.000000E+00,-8.311430E-03,0.000000E+00,2.400000E-03,0.000000E+00
|
||||||
8,0,0,-1.08E-04,0,2.60E-05,0
|
PART-1_1-1,8,0.000000E+00,0.000000E+00,-1.083000E-02,0.000000E+00,2.600000E-03,0.000000E+00
|
||||||
9,0,0,-1.35E-04,0,2.74E-05,0
|
PART-1_1-1,9,0.000000E+00,0.000000E+00,-1.352000E-02,0.000000E+00,2.742860E-03,0.000000E+00
|
||||||
10,0,0,-1.63E-04,0,2.83E-05,0
|
PART-1_1-1,10,0.000000E+00,0.000000E+00,-1.632430E-02,0.000000E+00,2.828570E-03,0.000000E+00
|
||||||
11,0,0,-1.92E-04,0,2.86E-05,0
|
PART-1_1-1,11,0.000000E+00,0.000000E+00,-1.918570E-02,0.000000E+00,2.857140E-03,0.000000E+00
|
||||||
|
|||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
Part Instance Name, Element Label, Node Label, SF-SF1, SF-SF2, SF-SF3, SM-SM1, SM-SM2, SM-SM3,,,,,
|
||||||
|
PART-1_1-1,1,1,0.000000E+00,-1.000000E+06,0.000000E+00,9.500000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,1,2,0.000000E+00,-1.000000E+06,0.000000E+00,9.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,2,2,0.000000E+00,-1.000000E+06,0.000000E+00,9.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,2,3,0.000000E+00,-1.000000E+06,0.000000E+00,8.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,3,3,0.000000E+00,-1.000000E+06,0.000000E+00,8.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,3,4,0.000000E+00,-1.000000E+06,0.000000E+00,7.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,4,4,0.000000E+00,-1.000000E+06,0.000000E+00,7.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,4,5,0.000000E+00,-1.000000E+06,0.000000E+00,6.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,5,5,0.000000E+00,-1.000000E+06,0.000000E+00,6.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,5,6,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,6,6,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,6,7,0.000000E+00,-1.000000E+06,0.000000E+00,4.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,7,7,0.000000E+00,-1.000000E+06,0.000000E+00,4.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,7,8,0.000000E+00,-1.000000E+06,0.000000E+00,3.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,8,8,0.000000E+00,-1.000000E+06,0.000000E+00,3.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,8,9,0.000000E+00,-1.000000E+06,0.000000E+00,2.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,9,9,0.000000E+00,-1.000000E+06,0.000000E+00,2.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,9,10,0.000000E+00,-1.000000E+06,0.000000E+00,1.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,10,10,0.000000E+00,-1.000000E+06,0.000000E+00,1.000000E+06,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
PART-1_1-1,10,11,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+05,0.000000E+00,0.000000E+00,,,,,
|
||||||
|
+5
-46
@@ -1,10 +1,8 @@
|
|||||||
*Heading
|
*Heading
|
||||||
** Job name: Job-1 Model name: Model-1
|
** FESA projection of the Abaqus 2024 cantilever reference model.
|
||||||
** Generated by: Abaqus/CAE Learning Edition 2024
|
** Geometry, material, section, boundary conditions, and load are unchanged.
|
||||||
|
** SCF is set to zero because FESA does not use Abaqus slenderness compensation.
|
||||||
*Preprint, echo=NO, model=NO, history=NO, contact=NO
|
*Preprint, echo=NO, model=NO, history=NO, contact=NO
|
||||||
**
|
|
||||||
** PARTS
|
|
||||||
**
|
|
||||||
*Part, name=PART-1_1
|
*Part, name=PART-1_1
|
||||||
*Node
|
*Node
|
||||||
1, 0., 0., 0.
|
1, 0., 0., 0.
|
||||||
@@ -31,37 +29,23 @@
|
|||||||
10, 10, 11
|
10, 10, 11
|
||||||
*Elset, elset=Set-1, generate
|
*Elset, elset=Set-1, generate
|
||||||
1, 10, 1
|
1, 10, 1
|
||||||
*Elset, elset=Set-2, generate
|
|
||||||
1, 10, 1
|
|
||||||
** Section: Section-1 Profile: Profile-1
|
|
||||||
*Beam General Section, elset=Set-1, material=Material-1, section=GENERAL
|
*Beam General Section, elset=Set-1, material=Material-1, section=GENERAL
|
||||||
1., 0.0833333, 0., 0.0833333, 0.140833
|
1., 0.0833333, 0., 0.0833333, 0.140833
|
||||||
0.,1.,0.
|
0.,1.,0.
|
||||||
|
*Transverse Shear Stiffness
|
||||||
|
6.73077e+10, 6.73077e+10, 0.
|
||||||
*End Part
|
*End Part
|
||||||
**
|
|
||||||
**
|
|
||||||
** ASSEMBLY
|
|
||||||
**
|
|
||||||
*Assembly, name=Assembly
|
*Assembly, name=Assembly
|
||||||
**
|
|
||||||
*Instance, name=PART-1_1-1, part=PART-1_1
|
*Instance, name=PART-1_1-1, part=PART-1_1
|
||||||
*End Instance
|
*End Instance
|
||||||
**
|
|
||||||
*Nset, nset=Set-3, instance=PART-1_1-1
|
*Nset, nset=Set-3, instance=PART-1_1-1
|
||||||
1,
|
1,
|
||||||
*Nset, nset=Set-4, instance=PART-1_1-1
|
*Nset, nset=Set-4, instance=PART-1_1-1
|
||||||
11,
|
11,
|
||||||
*End Assembly
|
*End Assembly
|
||||||
**
|
|
||||||
** MATERIALS
|
|
||||||
**
|
|
||||||
*Material, name=Material-1
|
*Material, name=Material-1
|
||||||
*Elastic
|
*Elastic
|
||||||
2.1e+11, 0.3
|
2.1e+11, 0.3
|
||||||
**
|
|
||||||
** BOUNDARY CONDITIONS
|
|
||||||
**
|
|
||||||
** Name: BC-1 Type: Displacement/Rotation
|
|
||||||
*Boundary
|
*Boundary
|
||||||
Set-3, 1, 1
|
Set-3, 1, 1
|
||||||
Set-3, 2, 2
|
Set-3, 2, 2
|
||||||
@@ -69,34 +53,9 @@ Set-3, 3, 3
|
|||||||
Set-3, 4, 4
|
Set-3, 4, 4
|
||||||
Set-3, 5, 5
|
Set-3, 5, 5
|
||||||
Set-3, 6, 6
|
Set-3, 6, 6
|
||||||
** ----------------------------------------------------------------
|
|
||||||
**
|
|
||||||
** STEP: Step-1
|
|
||||||
**
|
|
||||||
*Step, name=Step-1, nlgeom=NO
|
*Step, name=Step-1, nlgeom=NO
|
||||||
*Static
|
*Static
|
||||||
1., 1., 1e-05, 1.
|
1., 1., 1e-05, 1.
|
||||||
**
|
|
||||||
** LOADS
|
|
||||||
**
|
|
||||||
** Name: Load-1 Type: Concentrated force
|
|
||||||
*Cload
|
*Cload
|
||||||
Set-4, 3, -1e+06
|
Set-4, 3, -1e+06
|
||||||
**
|
|
||||||
** OUTPUT REQUESTS
|
|
||||||
**
|
|
||||||
*Restart, write, frequency=0
|
|
||||||
**
|
|
||||||
** FIELD OUTPUT: F-Output-1
|
|
||||||
**
|
|
||||||
*Output, field
|
|
||||||
*Node Output
|
|
||||||
CF, RF, TF, U
|
|
||||||
*Element Output, directions=YES
|
|
||||||
LE, NFORC, NFORCSO, PE, PEEQ, PEMAG, S, SF
|
|
||||||
*Contact Output, variable=PRESELECT
|
|
||||||
**
|
|
||||||
** HISTORY OUTPUT: H-Output-1
|
|
||||||
**
|
|
||||||
*Output, history, variable=PRESELECT
|
|
||||||
*End Step
|
*End Step
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3
|
Part Instance Name, Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3
|
||||||
1,0,0,1.00E+04,0,-1.00E+05,0
|
PART-1_1-1,1,0.000000E+00,0.000000E+00,1.000000E+06,0.000000E+00,-1.000000E+07,0.000000E+00
|
||||||
2,0,0,0,0,0,0
|
PART-1_1-1,2,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
3,0,0,0,0,0,0
|
PART-1_1-1,3,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
4,0,0,0,0,0,0
|
PART-1_1-1,4,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
5,0,0,0,0,0,0
|
PART-1_1-1,5,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
6,0,0,0,0,0,0
|
PART-1_1-1,6,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
7,0,0,0,0,0,0
|
PART-1_1-1,7,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
8,0,0,0,0,0,0
|
PART-1_1-1,8,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
9,0,0,0,0,0,0
|
PART-1_1-1,9,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
10,0,0,0,0,0,0
|
PART-1_1-1,10,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
11,0,0,0,0,0,0
|
PART-1_1-1,11,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||||
|
|||||||
|
@@ -5,7 +5,7 @@
|
|||||||
**
|
**
|
||||||
** PARTS
|
** PARTS
|
||||||
**
|
**
|
||||||
*Part, name=Part-1
|
*Part, name=PART-1_1
|
||||||
*Node
|
*Node
|
||||||
1, 0., 0., 0.
|
1, 0., 0., 0.
|
||||||
2, 1., 0., 0.
|
2, 1., 0., 0.
|
||||||
@@ -29,18 +29,16 @@
|
|||||||
8, 8, 9
|
8, 8, 9
|
||||||
9, 9, 10
|
9, 9, 10
|
||||||
10, 10, 11
|
10, 10, 11
|
||||||
*Nset, nset=Set-1, generate
|
|
||||||
1, 11, 1
|
|
||||||
*Elset, elset=Set-1, generate
|
*Elset, elset=Set-1, generate
|
||||||
1, 10, 1
|
1, 10, 1
|
||||||
*Nset, nset=Set-2, generate
|
|
||||||
1, 11, 1
|
|
||||||
*Elset, elset=Set-2, generate
|
*Elset, elset=Set-2, generate
|
||||||
1, 10, 1
|
1, 10, 1
|
||||||
** Section: Section-1 Profile: Profile-1
|
** Section: Section-1 Profile: Profile-1
|
||||||
*Beam General Section, elset=Set-1, material=Material-1, section=GENERAL
|
*Beam General Section, elset=Set-1, material=Material-1, section=GENERAL
|
||||||
1., 0.0833333, 0., 0.0833333, 0.140833
|
1., 0.0833333, 0., 0.0833333, 0.140833
|
||||||
0.,1.,0.
|
0.,1.,0.
|
||||||
|
*Transverse Shear Stiffness
|
||||||
|
6.73077e+10, 6.73077e+10, 0.25
|
||||||
*End Part
|
*End Part
|
||||||
**
|
**
|
||||||
**
|
**
|
||||||
@@ -48,13 +46,13 @@
|
|||||||
**
|
**
|
||||||
*Assembly, name=Assembly
|
*Assembly, name=Assembly
|
||||||
**
|
**
|
||||||
*Instance, name=Part-1-1, part=Part-1
|
*Instance, name=PART-1_1-1, part=PART-1_1
|
||||||
*End Instance
|
*End Instance
|
||||||
**
|
**
|
||||||
*Nset, nset=Set-1, instance=Part-1-1
|
*Nset, nset=Set-3, instance=PART-1_1-1
|
||||||
11,
|
|
||||||
*Nset, nset=Set-2, instance=Part-1-1
|
|
||||||
1,
|
1,
|
||||||
|
*Nset, nset=Set-4, instance=PART-1_1-1
|
||||||
|
11,
|
||||||
*End Assembly
|
*End Assembly
|
||||||
**
|
**
|
||||||
** MATERIALS
|
** MATERIALS
|
||||||
@@ -62,6 +60,17 @@
|
|||||||
*Material, name=Material-1
|
*Material, name=Material-1
|
||||||
*Elastic
|
*Elastic
|
||||||
2.1e+11, 0.3
|
2.1e+11, 0.3
|
||||||
|
**
|
||||||
|
** BOUNDARY CONDITIONS
|
||||||
|
**
|
||||||
|
** Name: BC-1 Type: Displacement/Rotation
|
||||||
|
*Boundary
|
||||||
|
Set-3, 1, 1
|
||||||
|
Set-3, 2, 2
|
||||||
|
Set-3, 3, 3
|
||||||
|
Set-3, 4, 4
|
||||||
|
Set-3, 5, 5
|
||||||
|
Set-3, 6, 6
|
||||||
** ----------------------------------------------------------------
|
** ----------------------------------------------------------------
|
||||||
**
|
**
|
||||||
** STEP: Step-1
|
** STEP: Step-1
|
||||||
@@ -70,22 +79,11 @@
|
|||||||
*Static
|
*Static
|
||||||
1., 1., 1e-05, 1.
|
1., 1., 1e-05, 1.
|
||||||
**
|
**
|
||||||
** BOUNDARY CONDITIONS
|
|
||||||
**
|
|
||||||
** Name: BC-1 Type: Displacement/Rotation
|
|
||||||
*Boundary
|
|
||||||
Set-2, 1, 1
|
|
||||||
Set-2, 2, 2
|
|
||||||
Set-2, 3, 3
|
|
||||||
Set-2, 4, 4
|
|
||||||
Set-2, 5, 5
|
|
||||||
Set-2, 6, 6
|
|
||||||
**
|
|
||||||
** LOADS
|
** LOADS
|
||||||
**
|
**
|
||||||
** Name: Load-1 Type: Concentrated force
|
** Name: Load-1 Type: Concentrated force
|
||||||
*Cload
|
*Cload
|
||||||
Set-1, 3, -10000.
|
Set-4, 3, -1e+06
|
||||||
**
|
**
|
||||||
** OUTPUT REQUESTS
|
** OUTPUT REQUESTS
|
||||||
**
|
**
|
||||||
@@ -93,7 +91,12 @@ Set-1, 3, -10000.
|
|||||||
**
|
**
|
||||||
** FIELD OUTPUT: F-Output-1
|
** FIELD OUTPUT: F-Output-1
|
||||||
**
|
**
|
||||||
*Output, field, variable=PRESELECT
|
*Output, field
|
||||||
|
*Node Output
|
||||||
|
CF, RF, TF, U
|
||||||
|
*Element Output, directions=YES
|
||||||
|
LE, NFORC, NFORCSO, PE, PEEQ, PEMAG, S, SF
|
||||||
|
*Contact Output, variable=PRESELECT
|
||||||
**
|
**
|
||||||
** HISTORY OUTPUT: H-Output-1
|
** HISTORY OUTPUT: H-Output-1
|
||||||
**
|
**
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
#include <stdexcept>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include <fesa/assembly/equation_system.hpp>
|
#include <fesa/assembly/equation_system.hpp>
|
||||||
#include <fesa/assembly/serial_assembler.hpp>
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
#include <fesa/constraints/essential_bc.hpp>
|
#include <fesa/constraints/essential_bc.hpp>
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
#include <fesa/fem/dof_manager.hpp>
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
||||||
|
|
||||||
@@ -34,6 +36,18 @@ AnalysisRunResult equation_failure(
|
|||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnalysisRunResult results_failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message) {
|
||||||
|
return failure({{
|
||||||
|
DiagnosticStage::results,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::nullopt,
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
NodalFrame build_nodal_frame(
|
NodalFrame build_nodal_frame(
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
const DofManager& dofs,
|
const DofManager& dofs,
|
||||||
@@ -48,9 +62,11 @@ NodalFrame build_nodal_frame(
|
|||||||
|
|
||||||
NodalFrame nodal;
|
NodalFrame nodal;
|
||||||
nodal.node_ids = std::move(node_ids);
|
nodal.node_ids = std::move(node_ids);
|
||||||
|
nodal.origins.reserve(nodal.node_ids.size());
|
||||||
nodal.displacement.reserve(nodal.node_ids.size());
|
nodal.displacement.reserve(nodal.node_ids.size());
|
||||||
nodal.reaction.reserve(nodal.node_ids.size());
|
nodal.reaction.reserve(nodal.node_ids.size());
|
||||||
for (const NodeId node_id : nodal.node_ids) {
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
|
nodal.origins.push_back(domain.node(node_id).origin);
|
||||||
std::array<double, 6> node_displacement{};
|
std::array<double, 6> node_displacement{};
|
||||||
std::array<double, 6> node_reaction{};
|
std::array<double, 6> node_reaction{};
|
||||||
for (std::size_t component = 0; component < 6; ++component) {
|
for (std::size_t component = 0; component < 6; ++component) {
|
||||||
@@ -67,6 +83,70 @@ NodalFrame build_nodal_frame(
|
|||||||
return nodal;
|
return nodal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElementFrame build_element_frame(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs,
|
||||||
|
const std::vector<double>& displacement) {
|
||||||
|
std::vector<const BeamElement*> elements;
|
||||||
|
elements.reserve(domain.beam_elements().size());
|
||||||
|
for (const BeamElement& element : domain.beam_elements()) {
|
||||||
|
elements.push_back(&element);
|
||||||
|
}
|
||||||
|
std::ranges::sort(
|
||||||
|
elements,
|
||||||
|
{},
|
||||||
|
[](const BeamElement* element) {
|
||||||
|
return element->id.value();
|
||||||
|
});
|
||||||
|
|
||||||
|
ElementFrame frame;
|
||||||
|
frame.beams.reserve(elements.size());
|
||||||
|
for (const BeamElement* element : elements) {
|
||||||
|
const Beam3D2Input input{
|
||||||
|
{
|
||||||
|
domain.node(element->nodes[0]).position,
|
||||||
|
domain.node(element->nodes[1]).position,
|
||||||
|
},
|
||||||
|
element->nodes,
|
||||||
|
domain.material(element->material),
|
||||||
|
domain.section(element->section),
|
||||||
|
};
|
||||||
|
const BeamKernelResult kernel = compute_beam3d2(input);
|
||||||
|
if (!kernel.contribution.has_value()) {
|
||||||
|
const std::string message = kernel.diagnostics.empty()
|
||||||
|
? "Beam recovery requires a valid element input."
|
||||||
|
: kernel.diagnostics.front().message;
|
||||||
|
throw std::runtime_error{message};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<double, 12> element_displacement{};
|
||||||
|
const std::array<std::size_t, 12> full_dofs =
|
||||||
|
dofs.element_full_dofs(*element);
|
||||||
|
for (std::size_t local = 0; local < full_dofs.size(); ++local) {
|
||||||
|
element_displacement[local] = displacement[full_dofs[local]];
|
||||||
|
}
|
||||||
|
std::vector<BeamSectionResult> recovered = recover_beam3d2(
|
||||||
|
input,
|
||||||
|
element_displacement,
|
||||||
|
input.section.recovery_points);
|
||||||
|
if (recovered.size() != 2U) {
|
||||||
|
throw std::logic_error{
|
||||||
|
"Beam recovery must return exactly two end results."};
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.beams.push_back({
|
||||||
|
element->id,
|
||||||
|
element->origin,
|
||||||
|
kernel.contribution->frame,
|
||||||
|
{
|
||||||
|
std::move(recovered[0]),
|
||||||
|
std::move(recovered[1]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||||
@@ -114,14 +194,23 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
|||||||
"equation.reaction_recovery_failed", error.what());
|
"equation.reaction_recovery_failed", error.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElementFrame element;
|
||||||
|
try {
|
||||||
|
element = build_element_frame(domain, dofs, displacement);
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
return results_failure(
|
||||||
|
"results.element_recovery_failed", error.what());
|
||||||
|
}
|
||||||
|
|
||||||
ResultDatabase database{
|
ResultDatabase database{
|
||||||
"1.0.0",
|
"2.0.0",
|
||||||
{{
|
{{
|
||||||
domain.step().name,
|
domain.step().name,
|
||||||
{{
|
{{
|
||||||
1.0,
|
1.0,
|
||||||
build_nodal_frame(
|
build_nodal_frame(
|
||||||
domain, dofs, displacement, reaction),
|
domain, dofs, displacement, reaction),
|
||||||
|
std::move(element),
|
||||||
{},
|
{},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <fesa/analysis/run_solver.hpp>
|
#include <fesa/analysis/run_solver.hpp>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <fesa/io/abaqus/parser.hpp>
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
@@ -8,6 +9,14 @@
|
|||||||
#include <fesa/io/hdf5/writer.hpp>
|
#include <fesa/io/hdf5/writer.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string path_utf8(const std::filesystem::path& path) {
|
||||||
|
const std::u8string value = path.u8string();
|
||||||
|
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
||||||
ParseDeckResult parsed = parse_deck(request.input_path);
|
ParseDeckResult parsed = parse_deck(request.input_path);
|
||||||
@@ -20,13 +29,18 @@ AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
|||||||
return {false, std::nullopt, std::move(mapped.diagnostics)};
|
return {false, std::nullopt, std::move(mapped.diagnostics)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Hdf5InputIdentity identity{
|
||||||
|
path_utf8(request.input_path),
|
||||||
|
parsed.input_fingerprint,
|
||||||
|
};
|
||||||
|
|
||||||
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
|
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
|
||||||
if (!run.succeeded || !run.results.has_value()) {
|
if (!run.succeeded || !run.results.has_value()) {
|
||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Diagnostic> write_diagnostics = write_hdf5(
|
std::vector<Diagnostic> write_diagnostics = write_hdf5(
|
||||||
request.output_path, *mapped.domain, *run.results);
|
request.output_path, *mapped.domain, *run.results, identity);
|
||||||
if (!write_diagnostics.empty()) {
|
if (!write_diagnostics.empty()) {
|
||||||
return {false, std::nullopt, std::move(write_diagnostics)};
|
return {false, std::nullopt, std::move(write_diagnostics)};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include <fesa/assembly/contribution.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::int32_t csr_index(const std::size_t value) {
|
||||||
|
if (value >
|
||||||
|
static_cast<std::size_t>(
|
||||||
|
std::numeric_limits<std::int32_t>::max())) {
|
||||||
|
throw std::overflow_error{
|
||||||
|
"Symmetric CSR exceeds the 32-bit index range."};
|
||||||
|
}
|
||||||
|
return static_cast<std::int32_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contribution_less(
|
||||||
|
const MatrixContribution& left,
|
||||||
|
const MatrixContribution& right) {
|
||||||
|
return std::tuple{
|
||||||
|
left.row,
|
||||||
|
left.column,
|
||||||
|
left.element,
|
||||||
|
left.local_order} <
|
||||||
|
std::tuple{
|
||||||
|
right.row,
|
||||||
|
right.column,
|
||||||
|
right.element,
|
||||||
|
right.local_order};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<MatrixContribution> canonicalize_contributions(
|
||||||
|
const std::span<const MatrixContribution> contributions) {
|
||||||
|
std::vector<MatrixContribution> canonical{
|
||||||
|
contributions.begin(), contributions.end()};
|
||||||
|
std::ranges::sort(canonical, contribution_less);
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
SymmetricCsr merge_contributions(
|
||||||
|
const std::size_t order,
|
||||||
|
const std::span<const MatrixContribution> canonical) {
|
||||||
|
if (!std::ranges::is_sorted(canonical, contribution_less)) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"Matrix contributions are not in canonical order."};
|
||||||
|
}
|
||||||
|
|
||||||
|
using Coordinate = std::pair<std::size_t, std::size_t>;
|
||||||
|
std::vector<Coordinate> coordinates;
|
||||||
|
coordinates.reserve(order + canonical.size());
|
||||||
|
for (std::size_t row = 0; row < order; ++row) {
|
||||||
|
coordinates.emplace_back(row, row);
|
||||||
|
}
|
||||||
|
for (const MatrixContribution& contribution : canonical) {
|
||||||
|
if (contribution.row > contribution.column ||
|
||||||
|
contribution.column >= order) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"Matrix contribution is outside the upper triangle."};
|
||||||
|
}
|
||||||
|
coordinates.emplace_back(
|
||||||
|
contribution.row, contribution.column);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::sort(coordinates);
|
||||||
|
coordinates.erase(
|
||||||
|
std::ranges::unique(coordinates).begin(),
|
||||||
|
coordinates.end());
|
||||||
|
|
||||||
|
SymmetricCsr matrix{
|
||||||
|
order,
|
||||||
|
std::vector<std::int32_t>(order + 1, 0),
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
matrix.column_indices.reserve(coordinates.size());
|
||||||
|
for (const auto [row, column] : coordinates) {
|
||||||
|
++matrix.row_offsets[row + 1];
|
||||||
|
matrix.column_indices.push_back(csr_index(column));
|
||||||
|
}
|
||||||
|
for (std::size_t row = 0; row < order; ++row) {
|
||||||
|
const std::size_t offset =
|
||||||
|
static_cast<std::size_t>(matrix.row_offsets[row]) +
|
||||||
|
static_cast<std::size_t>(matrix.row_offsets[row + 1]);
|
||||||
|
matrix.row_offsets[row + 1] = csr_index(offset);
|
||||||
|
}
|
||||||
|
matrix.values.resize(matrix.column_indices.size(), 0.0);
|
||||||
|
|
||||||
|
std::size_t contribution_index = 0;
|
||||||
|
while (contribution_index < canonical.size()) {
|
||||||
|
const MatrixContribution& first =
|
||||||
|
canonical[contribution_index];
|
||||||
|
double value = 0.0;
|
||||||
|
do {
|
||||||
|
value += canonical[contribution_index].value;
|
||||||
|
++contribution_index;
|
||||||
|
} while (
|
||||||
|
contribution_index < canonical.size() &&
|
||||||
|
canonical[contribution_index].row == first.row &&
|
||||||
|
canonical[contribution_index].column == first.column);
|
||||||
|
|
||||||
|
const auto row_begin =
|
||||||
|
matrix.column_indices.begin() + matrix.row_offsets[first.row];
|
||||||
|
const auto row_end =
|
||||||
|
matrix.column_indices.begin() +
|
||||||
|
matrix.row_offsets[first.row + 1];
|
||||||
|
const auto entry = std::lower_bound(
|
||||||
|
row_begin,
|
||||||
|
row_end,
|
||||||
|
csr_index(first.column));
|
||||||
|
if (entry == row_end || *entry != csr_index(first.column)) {
|
||||||
|
throw std::logic_error{
|
||||||
|
"Numeric contribution is absent from the CSR pattern."};
|
||||||
|
}
|
||||||
|
matrix.values[static_cast<std::size_t>(
|
||||||
|
std::distance(matrix.column_indices.begin(), entry))] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
#include <fesa/assembly/assembler.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <numeric>
|
||||||
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/assembly/contribution.hpp>
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
|
|
||||||
|
#include <oneapi/tbb/blocked_range.h>
|
||||||
|
#include <oneapi/tbb/parallel_for.h>
|
||||||
|
#include <oneapi/tbb/task_arena.h>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ElementEvaluation final {
|
||||||
|
std::vector<MatrixContribution> contributions;
|
||||||
|
std::optional<std::string> error;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto origin_key(const EntityOrigin& origin) {
|
||||||
|
return std::tie(
|
||||||
|
origin.instance_name,
|
||||||
|
origin.local_label,
|
||||||
|
origin.part_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string kernel_error_message(
|
||||||
|
const BeamElement& element,
|
||||||
|
const BeamKernelResult& result) {
|
||||||
|
std::string message =
|
||||||
|
"Beam element " + std::to_string(element.origin.local_label) +
|
||||||
|
" kernel failed";
|
||||||
|
for (const Diagnostic& diagnostic : result.diagnostics) {
|
||||||
|
message += ": " + diagnostic.code + " - " + diagnostic.message;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::size_t> canonical_element_order(const Domain& domain) {
|
||||||
|
std::vector<std::size_t> order(domain.beam_elements().size());
|
||||||
|
std::iota(order.begin(), order.end(), std::size_t{0});
|
||||||
|
std::ranges::sort(
|
||||||
|
order,
|
||||||
|
[&domain](const std::size_t left, const std::size_t right) {
|
||||||
|
return origin_key(domain.beam_elements()[left].origin) <
|
||||||
|
origin_key(domain.beam_elements()[right].origin);
|
||||||
|
});
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ElementId> canonical_contribution_element_ids(
|
||||||
|
const std::vector<std::size_t>& order) {
|
||||||
|
// Domain ElementIds are not ordered by input identity. These tie-break
|
||||||
|
// IDs encode the existing serial assembler's element-origin order.
|
||||||
|
std::vector<ElementId> ids(order.size(), ElementId{0});
|
||||||
|
for (std::size_t rank = 0; rank < order.size(); ++rank) {
|
||||||
|
ids[order[rank]] = ElementId{static_cast<std::int64_t>(rank)};
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementEvaluation evaluate_element(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs,
|
||||||
|
const BeamElement& element,
|
||||||
|
const ElementId canonical_id) {
|
||||||
|
const BeamKernelResult result = compute_beam3d2({
|
||||||
|
{
|
||||||
|
domain.node(element.nodes[0]).position,
|
||||||
|
domain.node(element.nodes[1]).position,
|
||||||
|
},
|
||||||
|
element.nodes,
|
||||||
|
domain.material(element.material),
|
||||||
|
domain.section(element.section),
|
||||||
|
});
|
||||||
|
if (!result.contribution.has_value()) {
|
||||||
|
return {{}, kernel_error_message(element, result)};
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementEvaluation evaluation;
|
||||||
|
evaluation.contributions.reserve(78);
|
||||||
|
const std::array<std::size_t, 12> full_dofs =
|
||||||
|
dofs.element_full_dofs(element);
|
||||||
|
for (std::size_t local_row = 0; local_row < full_dofs.size();
|
||||||
|
++local_row) {
|
||||||
|
for (std::size_t local_column = local_row;
|
||||||
|
local_column < full_dofs.size();
|
||||||
|
++local_column) {
|
||||||
|
evaluation.contributions.push_back({
|
||||||
|
std::min(
|
||||||
|
full_dofs[local_row],
|
||||||
|
full_dofs[local_column]),
|
||||||
|
std::max(
|
||||||
|
full_dofs[local_row],
|
||||||
|
full_dofs[local_column]),
|
||||||
|
canonical_id,
|
||||||
|
static_cast<std::uint16_t>(
|
||||||
|
local_row * full_dofs.size() + local_column),
|
||||||
|
result.contribution
|
||||||
|
->global_stiffness[local_row][local_column],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return evaluation;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> assemble_force(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs) {
|
||||||
|
std::vector<double> force(dofs.full_dof_count(), 0.0);
|
||||||
|
for (const NodalLoad& load : domain.step().nodal_loads) {
|
||||||
|
for (std::size_t component = 0; component < load.values.size();
|
||||||
|
++component) {
|
||||||
|
const auto dof = static_cast<NodeDof>(component);
|
||||||
|
force[dofs.full_dof({load.node, dof})] +=
|
||||||
|
load.values[component];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return force;
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate_options(const AssemblyOptions options) {
|
||||||
|
if (options.max_threads == 0) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"Assembly max_threads must be greater than zero."};
|
||||||
|
}
|
||||||
|
if (options.max_threads >
|
||||||
|
static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"Assembly max_threads exceeds the TBB task arena range."};
|
||||||
|
}
|
||||||
|
if (options.grain_size == 0) {
|
||||||
|
throw std::invalid_argument{
|
||||||
|
"Assembly grain_size must be greater than zero."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EquationSystem assemble_parallel(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs,
|
||||||
|
const AssemblyOptions options) {
|
||||||
|
validate_options(options);
|
||||||
|
|
||||||
|
const std::vector<std::size_t> element_order =
|
||||||
|
canonical_element_order(domain);
|
||||||
|
const std::vector<ElementId> element_ids =
|
||||||
|
canonical_contribution_element_ids(element_order);
|
||||||
|
std::vector<ElementEvaluation> evaluations(
|
||||||
|
domain.beam_elements().size());
|
||||||
|
|
||||||
|
oneapi::tbb::task_arena arena{
|
||||||
|
static_cast<int>(options.max_threads)};
|
||||||
|
arena.execute([&] {
|
||||||
|
oneapi::tbb::parallel_for(
|
||||||
|
oneapi::tbb::blocked_range<std::size_t>{
|
||||||
|
0,
|
||||||
|
evaluations.size(),
|
||||||
|
options.grain_size,
|
||||||
|
},
|
||||||
|
[&](const oneapi::tbb::blocked_range<std::size_t>& range) {
|
||||||
|
for (std::size_t index = range.begin();
|
||||||
|
index != range.end();
|
||||||
|
++index) {
|
||||||
|
ElementEvaluation local = evaluate_element(
|
||||||
|
domain,
|
||||||
|
dofs,
|
||||||
|
domain.beam_elements()[index],
|
||||||
|
element_ids[index]);
|
||||||
|
evaluations[index] = std::move(local);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<MatrixContribution> contributions;
|
||||||
|
contributions.reserve(domain.beam_elements().size() * 78);
|
||||||
|
for (const std::size_t element_index : element_order) {
|
||||||
|
ElementEvaluation& evaluation = evaluations[element_index];
|
||||||
|
if (evaluation.error.has_value()) {
|
||||||
|
throw std::runtime_error{std::move(*evaluation.error)};
|
||||||
|
}
|
||||||
|
contributions.insert(
|
||||||
|
contributions.end(),
|
||||||
|
std::make_move_iterator(evaluation.contributions.begin()),
|
||||||
|
std::make_move_iterator(evaluation.contributions.end()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<MatrixContribution> canonical =
|
||||||
|
canonicalize_contributions(contributions);
|
||||||
|
return {
|
||||||
|
merge_contributions(dofs.full_dof_count(), canonical),
|
||||||
|
assemble_force(domain, dofs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -128,6 +128,7 @@ std::vector<NumericContribution> collect_numeric_contributions(
|
|||||||
domain.node(element.nodes[0]).position,
|
domain.node(element.nodes[0]).position,
|
||||||
domain.node(element.nodes[1]).position,
|
domain.node(element.nodes[1]).position,
|
||||||
},
|
},
|
||||||
|
element.nodes,
|
||||||
domain.material(element.material),
|
domain.material(element.material),
|
||||||
domain.section(element.section),
|
domain.section(element.section),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/fem/gauss_rule.hpp>
|
#include <fesa/fem/gauss_rule.hpp>
|
||||||
#include <fesa/fem/line2_shape.hpp>
|
#include <fesa/fem/line2_shape.hpp>
|
||||||
@@ -32,6 +34,20 @@ bool is_positive_finite(const double value) {
|
|||||||
return std::isfinite(value) && value > 0.0;
|
return std::isfinite(value) && value > 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<double, 6> constitutive_values(
|
||||||
|
const Beam3D2Input& input) {
|
||||||
|
const double shear_modulus =
|
||||||
|
input.material.young / (2.0 * (1.0 + input.material.poisson));
|
||||||
|
return {
|
||||||
|
input.material.young * input.section.area,
|
||||||
|
shear_modulus * input.section.shear_area_y,
|
||||||
|
shear_modulus * input.section.shear_area_z,
|
||||||
|
shear_modulus * input.section.torsion_j,
|
||||||
|
input.material.young * input.section.iy,
|
||||||
|
input.material.young * input.section.iz,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<BeamKernelResult> validate_properties(
|
std::optional<BeamKernelResult> validate_properties(
|
||||||
const Beam3D2Input& input) {
|
const Beam3D2Input& input) {
|
||||||
if (!std::isfinite(input.material.young) ||
|
if (!std::isfinite(input.material.young) ||
|
||||||
@@ -149,6 +165,34 @@ Matrix12 transform_stiffness(
|
|||||||
return global;
|
return global;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<double, 12> transform_displacement(
|
||||||
|
const Matrix12& transformation,
|
||||||
|
const std::span<const double, 12> global_displacement) {
|
||||||
|
std::array<double, 12> local_displacement{};
|
||||||
|
for (std::size_t row = 0; row < transformation.size(); ++row) {
|
||||||
|
for (std::size_t column = 0;
|
||||||
|
column < transformation[row].size();
|
||||||
|
++column) {
|
||||||
|
local_displacement[row] +=
|
||||||
|
transformation[row][column] * global_displacement[column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return local_displacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<double, 6> evaluate_strain(
|
||||||
|
const StrainMatrix& strain_matrix,
|
||||||
|
const std::array<double, 12>& local_displacement) {
|
||||||
|
std::array<double, 6> strain{};
|
||||||
|
for (std::size_t component = 0; component < strain.size(); ++component) {
|
||||||
|
for (std::size_t dof = 0; dof < local_displacement.size(); ++dof) {
|
||||||
|
strain[component] +=
|
||||||
|
strain_matrix[component][dof] * local_displacement[dof];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strain;
|
||||||
|
}
|
||||||
|
|
||||||
bool is_finite(const Matrix12& matrix) {
|
bool is_finite(const Matrix12& matrix) {
|
||||||
for (const auto& row : matrix) {
|
for (const auto& row : matrix) {
|
||||||
for (const double value : row) {
|
for (const double value : row) {
|
||||||
@@ -189,16 +233,8 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
|
|||||||
"Beam kernel requires a representable positive Jacobian.");
|
"Beam kernel requires a representable positive Jacobian.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const double shear_modulus =
|
const std::array<double, 6> constitutive =
|
||||||
input.material.young / (2.0 * (1.0 + input.material.poisson));
|
constitutive_values(input);
|
||||||
const std::array<double, 6> constitutive{
|
|
||||||
input.material.young * input.section.area,
|
|
||||||
shear_modulus * input.section.shear_area_y,
|
|
||||||
shear_modulus * input.section.shear_area_z,
|
|
||||||
shear_modulus * input.section.torsion_j,
|
|
||||||
input.material.young * input.section.iy,
|
|
||||||
input.material.young * input.section.iz,
|
|
||||||
};
|
|
||||||
for (const double value : constitutive) {
|
for (const double value : constitutive) {
|
||||||
if (!is_positive_finite(value)) {
|
if (!is_positive_finite(value)) {
|
||||||
return error_result(
|
return error_result(
|
||||||
@@ -241,4 +277,73 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<BeamSectionResult> recover_beam3d2(
|
||||||
|
const Beam3D2Input& input,
|
||||||
|
const std::span<const double, 12> element_displacement,
|
||||||
|
const std::span<const std::array<double, 2>> recovery_points) {
|
||||||
|
const BeamKernelResult kernel = compute_beam3d2(input);
|
||||||
|
if (!kernel.contribution.has_value()) {
|
||||||
|
const std::string message = kernel.diagnostics.empty()
|
||||||
|
? "Beam recovery requires a valid Beam3D2 input."
|
||||||
|
: kernel.diagnostics.front().message;
|
||||||
|
throw std::invalid_argument{message};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec3 axis{
|
||||||
|
input.coordinates[1].x - input.coordinates[0].x,
|
||||||
|
input.coordinates[1].y - input.coordinates[0].y,
|
||||||
|
input.coordinates[1].z - input.coordinates[0].z,
|
||||||
|
};
|
||||||
|
const double jacobian = std::hypot(axis.x, axis.y, axis.z) / 2.0;
|
||||||
|
const Matrix12 transformation =
|
||||||
|
beam_transformation(kernel.contribution->frame);
|
||||||
|
const std::array<double, 12> local_displacement =
|
||||||
|
transform_displacement(transformation, element_displacement);
|
||||||
|
const std::array<double, 6> center_strain = evaluate_strain(
|
||||||
|
strain_matrix(0.0, jacobian),
|
||||||
|
local_displacement);
|
||||||
|
const std::array<double, 6> constitutive =
|
||||||
|
constitutive_values(input);
|
||||||
|
|
||||||
|
std::vector<BeamSectionResult> results;
|
||||||
|
results.reserve(input.node_ids.size());
|
||||||
|
for (std::size_t end = 0; end < input.node_ids.size(); ++end) {
|
||||||
|
const double xi = end == 0 ? -1.0 : 1.0;
|
||||||
|
std::array<double, 6> section_strain = evaluate_strain(
|
||||||
|
strain_matrix(xi, jacobian),
|
||||||
|
local_displacement);
|
||||||
|
section_strain[1] = center_strain[1];
|
||||||
|
section_strain[2] = center_strain[2];
|
||||||
|
|
||||||
|
std::array<double, 6> section_force{};
|
||||||
|
for (std::size_t component = 0;
|
||||||
|
component < section_force.size();
|
||||||
|
++component) {
|
||||||
|
section_force[component] =
|
||||||
|
constitutive[component] * section_strain[component];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> sigma_xx;
|
||||||
|
sigma_xx.reserve(recovery_points.size());
|
||||||
|
for (const auto& point : recovery_points) {
|
||||||
|
const double y = point[0];
|
||||||
|
const double z = point[1];
|
||||||
|
sigma_xx.push_back(
|
||||||
|
input.material.young *
|
||||||
|
(section_strain[0] + z * section_strain[4] -
|
||||||
|
y * section_strain[5]));
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push_back({
|
||||||
|
xi,
|
||||||
|
input.node_ids[end],
|
||||||
|
section_strain,
|
||||||
|
section_force,
|
||||||
|
section_force[0] / input.section.area,
|
||||||
|
std::move(sigma_xx),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <iterator>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -50,6 +53,20 @@ std::string uppercase_ascii(std::string value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string input_fingerprint(const std::string_view bytes) {
|
||||||
|
std::uint64_t fingerprint = 14695981039346656037ULL;
|
||||||
|
for (const char byte : bytes) {
|
||||||
|
fingerprint ^=
|
||||||
|
static_cast<std::uint8_t>(static_cast<unsigned char>(byte));
|
||||||
|
fingerprint *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream encoded;
|
||||||
|
encoded << "fnv1a64:" << std::hex << std::setfill('0')
|
||||||
|
<< std::setw(16) << fingerprint;
|
||||||
|
return encoded.str();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> split_fields(const std::string_view value) {
|
std::vector<std::string> split_fields(const std::string_view value) {
|
||||||
std::vector<std::string> fields;
|
std::vector<std::string> fields;
|
||||||
std::size_t first = 0;
|
std::size_t first = 0;
|
||||||
@@ -374,14 +391,26 @@ ParseDeckResult missing_parameter(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||||
std::ifstream input{path, std::ios::binary};
|
std::ifstream file{path, std::ios::binary};
|
||||||
if (!input) {
|
if (!file) {
|
||||||
return failure(
|
return failure(
|
||||||
DiagnosticStage::io,
|
DiagnosticStage::io,
|
||||||
"abaqus.io.open_failed",
|
"abaqus.io.open_failed",
|
||||||
"Unable to open Abaqus input file.",
|
"Unable to open Abaqus input file.",
|
||||||
SourceLocation{path, 0U, 0U});
|
SourceLocation{path, 0U, 0U});
|
||||||
}
|
}
|
||||||
|
const std::string source_bytes{
|
||||||
|
std::istreambuf_iterator<char>{file},
|
||||||
|
std::istreambuf_iterator<char>{},
|
||||||
|
};
|
||||||
|
if (file.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"abaqus.io.read_failed",
|
||||||
|
"Failed while reading Abaqus input file.",
|
||||||
|
SourceLocation{path, 0U, 0U});
|
||||||
|
}
|
||||||
|
std::istringstream input{source_bytes};
|
||||||
|
|
||||||
ParsedDeck deck;
|
ParsedDeck deck;
|
||||||
Scope scope = Scope::global;
|
Scope scope = Scope::global;
|
||||||
@@ -846,7 +875,11 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
*current_step_source);
|
*current_step_source);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {std::move(deck), {}};
|
return {
|
||||||
|
std::move(deck),
|
||||||
|
{},
|
||||||
|
input_fingerprint(source_bytes),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
+1678
-176
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,12 @@ void validate_nodal_frame(
|
|||||||
"Nodal IDs, displacement, and reaction fields must have "
|
"Nodal IDs, displacement, and reaction fields must have "
|
||||||
"matching sizes.");
|
"matching sizes.");
|
||||||
}
|
}
|
||||||
|
if (nodal.origins.size() != nodal.node_ids.size()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nodal_origin_size_mismatch",
|
||||||
|
"Nodal IDs and origins must have matching sizes.");
|
||||||
|
}
|
||||||
|
|
||||||
std::set<std::int64_t> node_ids;
|
std::set<std::int64_t> node_ids;
|
||||||
for (const NodeId node_id : nodal.node_ids) {
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
@@ -74,6 +80,84 @@ void validate_nodal_frame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void validate_beam_section_result(
|
||||||
|
const BeamSectionResult& result,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
if (
|
||||||
|
!std::isfinite(result.xi) ||
|
||||||
|
!is_finite(result.section_strain) ||
|
||||||
|
!is_finite(result.section_force) ||
|
||||||
|
!std::isfinite(result.centroid_sigma_xx) ||
|
||||||
|
!std::ranges::all_of(
|
||||||
|
result.sigma_xx,
|
||||||
|
[](const double value) { return std::isfinite(value); })) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nonfinite_value",
|
||||||
|
"Beam section result contains a nonfinite value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate_element_frame(
|
||||||
|
const ElementFrame& element,
|
||||||
|
const NodalFrame& nodal,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
std::set<std::int64_t> nodal_ids;
|
||||||
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
|
nodal_ids.insert(node_id.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::int64_t> element_ids;
|
||||||
|
for (const BeamElementFrame& beam : element.beams) {
|
||||||
|
if (!element_ids.insert(beam.element.value()).second) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.duplicate_element_id",
|
||||||
|
"Element frame contains duplicate element ID " +
|
||||||
|
std::to_string(beam.element.value()) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!is_finite(beam.local_frame.ex) ||
|
||||||
|
!is_finite(beam.local_frame.ey) ||
|
||||||
|
!is_finite(beam.local_frame.ez)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nonfinite_value",
|
||||||
|
"Beam local frame contains a nonfinite component.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeamSectionResult& first = beam.end_results[0];
|
||||||
|
const BeamSectionResult& second = beam.end_results[1];
|
||||||
|
if (first.end_node == second.end_node) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.duplicate_beam_end_node",
|
||||||
|
"Beam element result contains the same node at both ends.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
first.xi != -1.0 || second.xi != 1.0 ||
|
||||||
|
!nodal_ids.contains(first.end_node.value()) ||
|
||||||
|
!nodal_ids.contains(second.end_node.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.invalid_beam_connectivity",
|
||||||
|
"Beam end results must follow (-1, +1) connectivity and "
|
||||||
|
"reference nodes in the nodal frame.");
|
||||||
|
}
|
||||||
|
if (first.sigma_xx.size() != second.sigma_xx.size()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.recovery_point_count_mismatch",
|
||||||
|
"Beam end results must have matching recovery-point "
|
||||||
|
"counts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_beam_section_result(first, diagnostics);
|
||||||
|
validate_beam_section_result(second, diagnostics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Status validate_result_database(const ResultDatabase& database) {
|
Status validate_result_database(const ResultDatabase& database) {
|
||||||
@@ -106,6 +190,8 @@ Status validate_result_database(const ResultDatabase& database) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_nodal_frame(frame.nodal, diagnostics);
|
validate_nodal_frame(frame.nodal, diagnostics);
|
||||||
|
validate_element_frame(
|
||||||
|
frame.element, frame.nodal, diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,591 @@
|
|||||||
|
#include <fesa/validation/comparison.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using PositionKey = std::tuple<
|
||||||
|
ReferenceQuantity,
|
||||||
|
std::string,
|
||||||
|
std::int64_t,
|
||||||
|
std::optional<std::int64_t>>;
|
||||||
|
|
||||||
|
void add_failure(
|
||||||
|
std::vector<Diagnostic>& failures,
|
||||||
|
std::string code,
|
||||||
|
std::string message) {
|
||||||
|
failures.push_back({
|
||||||
|
DiagnosticStage::validation,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view quantity_name(const ReferenceQuantity quantity) {
|
||||||
|
switch (quantity) {
|
||||||
|
case ReferenceQuantity::displacement:
|
||||||
|
return "displacement";
|
||||||
|
case ReferenceQuantity::reaction:
|
||||||
|
return "reaction";
|
||||||
|
case ReferenceQuantity::internal_force:
|
||||||
|
return "internal_force";
|
||||||
|
case ReferenceQuantity::centroid_stress:
|
||||||
|
return "centroid_stress";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t expected_component_count(
|
||||||
|
const ReferenceQuantity quantity) {
|
||||||
|
switch (quantity) {
|
||||||
|
case ReferenceQuantity::displacement:
|
||||||
|
case ReferenceQuantity::reaction:
|
||||||
|
case ReferenceQuantity::internal_force:
|
||||||
|
return 6;
|
||||||
|
case ReferenceQuantity::centroid_stress:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string component_name(
|
||||||
|
const ReferenceQuantity quantity,
|
||||||
|
const std::size_t index) {
|
||||||
|
switch (quantity) {
|
||||||
|
case ReferenceQuantity::displacement:
|
||||||
|
return std::string{NodalFrame::displacement_components[index]};
|
||||||
|
case ReferenceQuantity::reaction:
|
||||||
|
return std::string{NodalFrame::reaction_components[index]};
|
||||||
|
case ReferenceQuantity::internal_force:
|
||||||
|
return std::string{
|
||||||
|
BeamElementFrame::section_force_components[index]};
|
||||||
|
case ReferenceQuantity::centroid_stress:
|
||||||
|
return std::string{BeamElementFrame::axial_stress_component};
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string number_text(const double value) {
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << std::setprecision(std::numeric_limits<double>::max_digits10)
|
||||||
|
<< value;
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string tolerance_text(const Tolerance tolerance) {
|
||||||
|
return " relative_tolerance=" + number_text(tolerance.relative) +
|
||||||
|
" absolute_scale=" + number_text(tolerance.absolute_scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string position_text(
|
||||||
|
const ReferenceQuantity quantity,
|
||||||
|
const ResultPosition& position) {
|
||||||
|
std::string text = "quantity=" + std::string{quantity_name(quantity)} +
|
||||||
|
" instance=" + position.instance_name +
|
||||||
|
" entity=" + std::to_string(position.entity_label);
|
||||||
|
if (position.end_node_label.has_value()) {
|
||||||
|
text += " end_node=" +
|
||||||
|
std::to_string(*position.end_node_label);
|
||||||
|
} else {
|
||||||
|
text += " end_node=n/a";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string scalar_failure_text(
|
||||||
|
const ComparisonSample& sample,
|
||||||
|
const std::size_t component,
|
||||||
|
const double normalized_error) {
|
||||||
|
return position_text(sample.quantity, sample.position) +
|
||||||
|
" component=" + component_name(sample.quantity, component) +
|
||||||
|
" reference=" + number_text(sample.reference[component]) +
|
||||||
|
" actual=" + number_text(sample.actual[component]) +
|
||||||
|
" normalized_error=" + number_text(normalized_error) +
|
||||||
|
tolerance_text(sample.tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string unevaluable_failure_text(
|
||||||
|
const ReferenceQuantity quantity,
|
||||||
|
const ResultPosition& position,
|
||||||
|
const Tolerance tolerance,
|
||||||
|
const std::string_view reason) {
|
||||||
|
return position_text(quantity, position) +
|
||||||
|
" component=n/a reference=n/a actual=n/a "
|
||||||
|
"normalized_error=inf" + tolerance_text(tolerance) +
|
||||||
|
" reason=" + std::string{reason};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool origin_matches(
|
||||||
|
const EntityOrigin& origin,
|
||||||
|
const std::string& instance_name,
|
||||||
|
const std::int64_t local_label) {
|
||||||
|
return origin.instance_name == instance_name &&
|
||||||
|
origin.local_label == local_label;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComparisonSampleMatch matching_failure(
|
||||||
|
const ReferenceQuantity quantity,
|
||||||
|
const ResultPosition& position,
|
||||||
|
const Tolerance tolerance,
|
||||||
|
std::string code,
|
||||||
|
const std::string_view reason) {
|
||||||
|
std::vector<Diagnostic> failures;
|
||||||
|
add_failure(
|
||||||
|
failures,
|
||||||
|
std::move(code),
|
||||||
|
unevaluable_failure_text(
|
||||||
|
quantity, position, tolerance, reason));
|
||||||
|
return {std::nullopt, std::move(failures)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> as_vector(const std::array<double, 6>& values) {
|
||||||
|
return {values.begin(), values.end()};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ComparisonSampleMatch make_comparison_sample(
|
||||||
|
const ResultFrame& frame,
|
||||||
|
const ReferenceQuantity quantity,
|
||||||
|
const ResultPosition& position,
|
||||||
|
const std::span<const double> reference,
|
||||||
|
const Tolerance tolerance) {
|
||||||
|
std::vector<double> actual;
|
||||||
|
|
||||||
|
if (
|
||||||
|
quantity == ReferenceQuantity::displacement ||
|
||||||
|
quantity == ReferenceQuantity::reaction) {
|
||||||
|
if (position.end_node_label.has_value()) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.invalid_result_position",
|
||||||
|
"nodal_position_has_end_node");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::size_t> matched_index;
|
||||||
|
for (
|
||||||
|
std::size_t index = 0;
|
||||||
|
index < frame.nodal.origins.size();
|
||||||
|
++index) {
|
||||||
|
if (origin_matches(
|
||||||
|
frame.nodal.origins[index],
|
||||||
|
position.instance_name,
|
||||||
|
position.entity_label)) {
|
||||||
|
if (matched_index.has_value()) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.unknown_result_origin",
|
||||||
|
"ambiguous_result_origin");
|
||||||
|
}
|
||||||
|
matched_index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched_index.has_value()) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.unknown_result_origin",
|
||||||
|
"unknown_result_origin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& field =
|
||||||
|
quantity == ReferenceQuantity::displacement
|
||||||
|
? frame.nodal.displacement
|
||||||
|
: frame.nodal.reaction;
|
||||||
|
if (*matched_index >= field.size()) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.component_count_mismatch",
|
||||||
|
"missing_actual_components");
|
||||||
|
}
|
||||||
|
actual = as_vector(field[*matched_index]);
|
||||||
|
} else {
|
||||||
|
if (!position.end_node_label.has_value()) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.invalid_element_node_pair",
|
||||||
|
"missing_end_node");
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeamElementFrame* matched_beam = nullptr;
|
||||||
|
for (const BeamElementFrame& beam : frame.element.beams) {
|
||||||
|
if (origin_matches(
|
||||||
|
beam.origin,
|
||||||
|
position.instance_name,
|
||||||
|
position.entity_label)) {
|
||||||
|
if (matched_beam != nullptr) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.unknown_result_origin",
|
||||||
|
"ambiguous_result_origin");
|
||||||
|
}
|
||||||
|
matched_beam = &beam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched_beam == nullptr) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.unknown_result_origin",
|
||||||
|
"unknown_result_origin");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<NodeId> end_node;
|
||||||
|
for (
|
||||||
|
std::size_t index = 0;
|
||||||
|
index < frame.nodal.origins.size() &&
|
||||||
|
index < frame.nodal.node_ids.size();
|
||||||
|
++index) {
|
||||||
|
if (origin_matches(
|
||||||
|
frame.nodal.origins[index],
|
||||||
|
position.instance_name,
|
||||||
|
*position.end_node_label)) {
|
||||||
|
if (end_node.has_value()) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.unknown_result_origin",
|
||||||
|
"ambiguous_end_node_origin");
|
||||||
|
}
|
||||||
|
end_node = frame.nodal.node_ids[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!end_node.has_value()) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.unknown_result_origin",
|
||||||
|
"unknown_end_node_origin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeamSectionResult* matched_end = nullptr;
|
||||||
|
for (const BeamSectionResult& end : matched_beam->end_results) {
|
||||||
|
if (end.end_node == *end_node) {
|
||||||
|
matched_end = &end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched_end == nullptr) {
|
||||||
|
return matching_failure(
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
tolerance,
|
||||||
|
"validation.invalid_element_node_pair",
|
||||||
|
"node_is_not_element_end");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantity == ReferenceQuantity::internal_force) {
|
||||||
|
actual = as_vector(matched_end->section_force);
|
||||||
|
} else {
|
||||||
|
actual = {matched_end->centroid_sigma_xx};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ComparisonSample matched{
|
||||||
|
quantity,
|
||||||
|
position,
|
||||||
|
{reference.begin(), reference.end()},
|
||||||
|
std::move(actual),
|
||||||
|
tolerance,
|
||||||
|
};
|
||||||
|
return {std::move(matched), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
ComparisonReport compare_samples(
|
||||||
|
const std::span<const ComparisonSample> samples) {
|
||||||
|
ComparisonReport report{true, 0.0, {}};
|
||||||
|
std::set<PositionKey> positions;
|
||||||
|
|
||||||
|
for (const ComparisonSample& sample : samples) {
|
||||||
|
const PositionKey key{
|
||||||
|
sample.quantity,
|
||||||
|
sample.position.instance_name,
|
||||||
|
sample.position.entity_label,
|
||||||
|
sample.position.end_node_label,
|
||||||
|
};
|
||||||
|
if (!positions.insert(key).second) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.duplicate_result_position",
|
||||||
|
unevaluable_failure_text(
|
||||||
|
sample.quantity,
|
||||||
|
sample.position,
|
||||||
|
sample.tolerance,
|
||||||
|
"duplicate_result_position"));
|
||||||
|
report.maximum_normalized_error =
|
||||||
|
std::numeric_limits<double>::infinity();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t expected =
|
||||||
|
expected_component_count(sample.quantity);
|
||||||
|
if (
|
||||||
|
sample.reference.size() != expected ||
|
||||||
|
sample.actual.size() != expected) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.component_count_mismatch",
|
||||||
|
unevaluable_failure_text(
|
||||||
|
sample.quantity,
|
||||||
|
sample.position,
|
||||||
|
sample.tolerance,
|
||||||
|
"component_count_mismatch") +
|
||||||
|
" expected=" + std::to_string(expected) +
|
||||||
|
" reference_count=" +
|
||||||
|
std::to_string(sample.reference.size()) +
|
||||||
|
" actual_count=" +
|
||||||
|
std::to_string(sample.actual.size()));
|
||||||
|
report.maximum_normalized_error =
|
||||||
|
std::numeric_limits<double>::infinity();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!std::isfinite(sample.tolerance.relative) ||
|
||||||
|
!std::isfinite(sample.tolerance.absolute_scale) ||
|
||||||
|
sample.tolerance.relative < 0.0 ||
|
||||||
|
sample.tolerance.absolute_scale < 0.0) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.invalid_tolerance",
|
||||||
|
unevaluable_failure_text(
|
||||||
|
sample.quantity,
|
||||||
|
sample.position,
|
||||||
|
sample.tolerance,
|
||||||
|
"invalid_tolerance"));
|
||||||
|
report.maximum_normalized_error =
|
||||||
|
std::numeric_limits<double>::infinity();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t component = 0; component < expected; ++component) {
|
||||||
|
const double reference = sample.reference[component];
|
||||||
|
const double actual = sample.actual[component];
|
||||||
|
if (!std::isfinite(reference) || !std::isfinite(actual)) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.nonfinite_comparison_value",
|
||||||
|
scalar_failure_text(
|
||||||
|
sample,
|
||||||
|
component,
|
||||||
|
std::numeric_limits<double>::infinity()));
|
||||||
|
report.maximum_normalized_error =
|
||||||
|
std::numeric_limits<double>::infinity();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double denominator =
|
||||||
|
sample.tolerance.absolute_scale +
|
||||||
|
sample.tolerance.relative * std::abs(reference);
|
||||||
|
if (!(denominator > 0.0) || !std::isfinite(denominator)) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.invalid_tolerance",
|
||||||
|
scalar_failure_text(
|
||||||
|
sample,
|
||||||
|
component,
|
||||||
|
std::numeric_limits<double>::infinity()));
|
||||||
|
report.maximum_normalized_error =
|
||||||
|
std::numeric_limits<double>::infinity();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double normalized_error =
|
||||||
|
std::abs(actual - reference) / denominator;
|
||||||
|
report.maximum_normalized_error = std::max(
|
||||||
|
report.maximum_normalized_error,
|
||||||
|
normalized_error);
|
||||||
|
if (!(normalized_error <= 1.0)) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.tolerance_exceeded",
|
||||||
|
scalar_failure_text(
|
||||||
|
sample, component, normalized_error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report.passed = report.failures.empty();
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
CorrelationReport correlate_samples(
|
||||||
|
const std::span<const ComparisonSample> samples) {
|
||||||
|
struct Accumulator final {
|
||||||
|
double squared_error{};
|
||||||
|
double squared_reference{};
|
||||||
|
double squared_absolute_scale{};
|
||||||
|
std::size_t value_count{};
|
||||||
|
};
|
||||||
|
|
||||||
|
CorrelationReport report{true, {}, {}};
|
||||||
|
if (samples.empty()) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.empty_comparison",
|
||||||
|
"No comparison samples were provided for correlation.");
|
||||||
|
report.evaluable = false;
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<PositionKey> positions;
|
||||||
|
std::map<std::pair<ReferenceQuantity, std::size_t>, Accumulator>
|
||||||
|
accumulators;
|
||||||
|
|
||||||
|
for (const ComparisonSample& sample : samples) {
|
||||||
|
const PositionKey key{
|
||||||
|
sample.quantity,
|
||||||
|
sample.position.instance_name,
|
||||||
|
sample.position.entity_label,
|
||||||
|
sample.position.end_node_label,
|
||||||
|
};
|
||||||
|
if (!positions.insert(key).second) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.duplicate_result_position",
|
||||||
|
unevaluable_failure_text(
|
||||||
|
sample.quantity,
|
||||||
|
sample.position,
|
||||||
|
sample.tolerance,
|
||||||
|
"duplicate_result_position"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t expected =
|
||||||
|
expected_component_count(sample.quantity);
|
||||||
|
if (
|
||||||
|
sample.reference.size() != expected ||
|
||||||
|
sample.actual.size() != expected) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.component_count_mismatch",
|
||||||
|
unevaluable_failure_text(
|
||||||
|
sample.quantity,
|
||||||
|
sample.position,
|
||||||
|
sample.tolerance,
|
||||||
|
"component_count_mismatch") +
|
||||||
|
" expected=" + std::to_string(expected) +
|
||||||
|
" reference_count=" +
|
||||||
|
std::to_string(sample.reference.size()) +
|
||||||
|
" actual_count=" +
|
||||||
|
std::to_string(sample.actual.size()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!std::isfinite(sample.tolerance.relative) ||
|
||||||
|
!std::isfinite(sample.tolerance.absolute_scale) ||
|
||||||
|
sample.tolerance.relative < 0.0 ||
|
||||||
|
sample.tolerance.absolute_scale < 0.0) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.invalid_tolerance",
|
||||||
|
unevaluable_failure_text(
|
||||||
|
sample.quantity,
|
||||||
|
sample.position,
|
||||||
|
sample.tolerance,
|
||||||
|
"invalid_tolerance"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t component = 0; component < expected; ++component) {
|
||||||
|
const double reference = sample.reference[component];
|
||||||
|
const double actual = sample.actual[component];
|
||||||
|
if (!std::isfinite(reference) || !std::isfinite(actual)) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.nonfinite_comparison_value",
|
||||||
|
scalar_failure_text(
|
||||||
|
sample,
|
||||||
|
component,
|
||||||
|
std::numeric_limits<double>::infinity()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double error = actual - reference;
|
||||||
|
Accumulator& accumulator =
|
||||||
|
accumulators[{sample.quantity, component}];
|
||||||
|
accumulator.squared_error += error * error;
|
||||||
|
accumulator.squared_reference += reference * reference;
|
||||||
|
accumulator.squared_absolute_scale +=
|
||||||
|
sample.tolerance.absolute_scale *
|
||||||
|
sample.tolerance.absolute_scale;
|
||||||
|
++accumulator.value_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [key, accumulator] : accumulators) {
|
||||||
|
const auto [quantity, component] = key;
|
||||||
|
const double error_l2 = std::sqrt(accumulator.squared_error);
|
||||||
|
const double root_mean_square_error =
|
||||||
|
error_l2 /
|
||||||
|
std::sqrt(static_cast<double>(accumulator.value_count));
|
||||||
|
const double reference_l2 =
|
||||||
|
std::sqrt(accumulator.squared_reference);
|
||||||
|
const double absolute_scale_l2 =
|
||||||
|
std::sqrt(accumulator.squared_absolute_scale);
|
||||||
|
const double denominator =
|
||||||
|
std::max(reference_l2, absolute_scale_l2);
|
||||||
|
const double relative_l2_error = denominator > 0.0
|
||||||
|
? error_l2 / denominator
|
||||||
|
: error_l2 == 0.0
|
||||||
|
? 0.0
|
||||||
|
: std::numeric_limits<
|
||||||
|
double>::infinity();
|
||||||
|
|
||||||
|
report.metrics.push_back({
|
||||||
|
quantity,
|
||||||
|
component,
|
||||||
|
accumulator.value_count,
|
||||||
|
root_mean_square_error,
|
||||||
|
relative_l2_error,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!std::isfinite(root_mean_square_error) ||
|
||||||
|
!std::isfinite(relative_l2_error)) {
|
||||||
|
add_failure(
|
||||||
|
report.failures,
|
||||||
|
"validation.nonfinite_correlation_metric",
|
||||||
|
"quantity=" + std::string{quantity_name(quantity)} +
|
||||||
|
" component=" + component_name(quantity, component) +
|
||||||
|
" rmse=" + number_text(root_mean_square_error) +
|
||||||
|
" relative_l2=" + number_text(relative_l2_error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report.evaluable = report.failures.empty();
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
#include <fesa/io/hdf5/writer.hpp>
|
||||||
|
#include <fesa/validation/comparison.hpp>
|
||||||
|
#include <fesa/validation/reference_csv.hpp>
|
||||||
|
|
||||||
|
#include <charconv>
|
||||||
|
#include <cmath>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ComparisonRequest final {
|
||||||
|
std::filesystem::path results;
|
||||||
|
std::string instance;
|
||||||
|
std::filesystem::path displacements;
|
||||||
|
std::filesystem::path reactions;
|
||||||
|
std::filesystem::path internal_forces;
|
||||||
|
double displacement_absolute_scale;
|
||||||
|
double reaction_absolute_scale;
|
||||||
|
double internal_force_absolute_scale;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string_view quantity_name(const fesa::ReferenceQuantity quantity) {
|
||||||
|
switch (quantity) {
|
||||||
|
case fesa::ReferenceQuantity::displacement:
|
||||||
|
return "displacement";
|
||||||
|
case fesa::ReferenceQuantity::reaction:
|
||||||
|
return "reaction";
|
||||||
|
case fesa::ReferenceQuantity::internal_force:
|
||||||
|
return "internal_force";
|
||||||
|
case fesa::ReferenceQuantity::centroid_stress:
|
||||||
|
return "centroid_stress";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view component_name(
|
||||||
|
const fesa::ReferenceQuantity quantity,
|
||||||
|
const std::size_t component) {
|
||||||
|
switch (quantity) {
|
||||||
|
case fesa::ReferenceQuantity::displacement:
|
||||||
|
return fesa::NodalFrame::displacement_components[component];
|
||||||
|
case fesa::ReferenceQuantity::reaction:
|
||||||
|
return fesa::NodalFrame::reaction_components[component];
|
||||||
|
case fesa::ReferenceQuantity::internal_force:
|
||||||
|
return fesa::BeamElementFrame::section_force_components[component];
|
||||||
|
case fesa::ReferenceQuantity::centroid_stress:
|
||||||
|
return fesa::BeamElementFrame::axial_stress_component;
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view stage_name(const fesa::DiagnosticStage stage) {
|
||||||
|
switch (stage) {
|
||||||
|
case fesa::DiagnosticStage::io:
|
||||||
|
return "io";
|
||||||
|
case fesa::DiagnosticStage::syntax:
|
||||||
|
return "syntax";
|
||||||
|
case fesa::DiagnosticStage::semantic:
|
||||||
|
return "semantic";
|
||||||
|
case fesa::DiagnosticStage::model:
|
||||||
|
return "model";
|
||||||
|
case fesa::DiagnosticStage::equation:
|
||||||
|
return "equation";
|
||||||
|
case fesa::DiagnosticStage::solver:
|
||||||
|
return "solver";
|
||||||
|
case fesa::DiagnosticStage::results:
|
||||||
|
return "results";
|
||||||
|
case fesa::DiagnosticStage::validation:
|
||||||
|
return "validation";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_diagnostic(const fesa::Diagnostic& diagnostic) {
|
||||||
|
std::cerr << stage_name(diagnostic.stage) << " [" << diagnostic.code
|
||||||
|
<< "]";
|
||||||
|
if (diagnostic.source.has_value()) {
|
||||||
|
const fesa::SourceLocation& source = *diagnostic.source;
|
||||||
|
std::cerr << ' ' << source.file.string() << ':' << source.line << ':'
|
||||||
|
<< source.column;
|
||||||
|
}
|
||||||
|
std::cerr << ": " << diagnostic.message << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_diagnostics(const std::vector<fesa::Diagnostic>& diagnostics) {
|
||||||
|
for (const fesa::Diagnostic& diagnostic : diagnostics) {
|
||||||
|
print_diagnostic(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_usage() {
|
||||||
|
std::cerr
|
||||||
|
<< "Usage:\n"
|
||||||
|
<< " fesa-reference-compare --results <results.h5> "
|
||||||
|
"--instance <name> --displacements <displacements.csv> "
|
||||||
|
"--reactions <reactions.csv> "
|
||||||
|
"--internal-forces <internal-forces.csv> "
|
||||||
|
"--displacement-absolute-scale <value> "
|
||||||
|
"--reaction-absolute-scale <value> "
|
||||||
|
"--internal-force-absolute-scale <value>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<double> parse_number(const std::string_view text) {
|
||||||
|
double value = 0.0;
|
||||||
|
const auto parsed = std::from_chars(
|
||||||
|
text.data(), text.data() + text.size(), value);
|
||||||
|
if (
|
||||||
|
parsed.ec != std::errc{} ||
|
||||||
|
parsed.ptr != text.data() + text.size() ||
|
||||||
|
!std::isfinite(value)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ComparisonRequest> parse_request(
|
||||||
|
const int argc,
|
||||||
|
char* argv[]) {
|
||||||
|
if (argc != 17) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::filesystem::path> results;
|
||||||
|
std::optional<std::string> instance;
|
||||||
|
std::optional<std::filesystem::path> displacements;
|
||||||
|
std::optional<std::filesystem::path> reactions;
|
||||||
|
std::optional<std::filesystem::path> internal_forces;
|
||||||
|
std::optional<double> displacement_absolute_scale;
|
||||||
|
std::optional<double> reaction_absolute_scale;
|
||||||
|
std::optional<double> internal_force_absolute_scale;
|
||||||
|
|
||||||
|
for (int index = 1; index < argc; index += 2) {
|
||||||
|
const std::string_view option{argv[index]};
|
||||||
|
const std::string_view value{argv[index + 1]};
|
||||||
|
if (option == "--results" && !results.has_value()) {
|
||||||
|
results = std::filesystem::path{value};
|
||||||
|
} else if (option == "--instance" && !instance.has_value()) {
|
||||||
|
instance = value;
|
||||||
|
} else if (
|
||||||
|
option == "--displacements" &&
|
||||||
|
!displacements.has_value()) {
|
||||||
|
displacements = std::filesystem::path{value};
|
||||||
|
} else if (option == "--reactions" && !reactions.has_value()) {
|
||||||
|
reactions = std::filesystem::path{value};
|
||||||
|
} else if (
|
||||||
|
option == "--internal-forces" &&
|
||||||
|
!internal_forces.has_value()) {
|
||||||
|
internal_forces = std::filesystem::path{value};
|
||||||
|
} else if (
|
||||||
|
option == "--displacement-absolute-scale" &&
|
||||||
|
!displacement_absolute_scale.has_value()) {
|
||||||
|
displacement_absolute_scale = parse_number(value);
|
||||||
|
} else if (
|
||||||
|
option == "--reaction-absolute-scale" &&
|
||||||
|
!reaction_absolute_scale.has_value()) {
|
||||||
|
reaction_absolute_scale = parse_number(value);
|
||||||
|
} else if (
|
||||||
|
option == "--internal-force-absolute-scale" &&
|
||||||
|
!internal_force_absolute_scale.has_value()) {
|
||||||
|
internal_force_absolute_scale = parse_number(value);
|
||||||
|
} else {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!results.has_value() || results->empty() ||
|
||||||
|
!instance.has_value() || instance->empty() ||
|
||||||
|
!displacements.has_value() || displacements->empty() ||
|
||||||
|
!reactions.has_value() || reactions->empty() ||
|
||||||
|
!internal_forces.has_value() || internal_forces->empty() ||
|
||||||
|
!displacement_absolute_scale.has_value() ||
|
||||||
|
!reaction_absolute_scale.has_value() ||
|
||||||
|
!internal_force_absolute_scale.has_value() ||
|
||||||
|
*displacement_absolute_scale < 0.0 ||
|
||||||
|
*reaction_absolute_scale < 0.0 ||
|
||||||
|
*internal_force_absolute_scale < 0.0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComparisonRequest{
|
||||||
|
std::move(*results),
|
||||||
|
std::move(*instance),
|
||||||
|
std::move(*displacements),
|
||||||
|
std::move(*reactions),
|
||||||
|
std::move(*internal_forces),
|
||||||
|
*displacement_absolute_scale,
|
||||||
|
*reaction_absolute_scale,
|
||||||
|
*internal_force_absolute_scale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool append_samples(
|
||||||
|
const fesa::ResultFrame& frame,
|
||||||
|
const std::vector<fesa::ReferenceRow>& rows,
|
||||||
|
const fesa::Tolerance tolerance,
|
||||||
|
std::vector<fesa::ComparisonSample>& samples) {
|
||||||
|
bool matched = true;
|
||||||
|
for (const fesa::ReferenceRow& row : rows) {
|
||||||
|
fesa::ComparisonSampleMatch match = fesa::make_comparison_sample(
|
||||||
|
frame,
|
||||||
|
row.quantity,
|
||||||
|
row.position,
|
||||||
|
row.values,
|
||||||
|
tolerance);
|
||||||
|
if (!match.sample.has_value()) {
|
||||||
|
print_diagnostics(match.failures);
|
||||||
|
matched = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
samples.push_back(std::move(*match.sample));
|
||||||
|
}
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
const std::optional<ComparisonRequest> request =
|
||||||
|
parse_request(argc, argv);
|
||||||
|
if (!request.has_value()) {
|
||||||
|
print_usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::Hdf5ReadResult results =
|
||||||
|
fesa::read_hdf5_results(request->results);
|
||||||
|
if (!results.database.has_value()) {
|
||||||
|
print_diagnostics(results.diagnostics);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::ReferenceCsvReadResult displacements =
|
||||||
|
fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
request->displacements,
|
||||||
|
request->instance);
|
||||||
|
const fesa::ReferenceCsvReadResult reactions =
|
||||||
|
fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
request->reactions,
|
||||||
|
request->instance);
|
||||||
|
const fesa::ReferenceCsvReadResult internal_forces =
|
||||||
|
fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::internal_force,
|
||||||
|
request->internal_forces,
|
||||||
|
request->instance);
|
||||||
|
if (
|
||||||
|
!displacements.diagnostics.empty() ||
|
||||||
|
!reactions.diagnostics.empty() ||
|
||||||
|
!internal_forces.diagnostics.empty()) {
|
||||||
|
print_diagnostics(displacements.diagnostics);
|
||||||
|
print_diagnostics(reactions.diagnostics);
|
||||||
|
print_diagnostics(internal_forces.diagnostics);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::ResultFrame& frame =
|
||||||
|
results.database->steps.front().frames.front();
|
||||||
|
std::vector<fesa::ComparisonSample> samples;
|
||||||
|
samples.reserve(
|
||||||
|
displacements.rows.size() + reactions.rows.size() +
|
||||||
|
internal_forces.rows.size());
|
||||||
|
const bool displacements_matched = append_samples(
|
||||||
|
frame,
|
||||||
|
displacements.rows,
|
||||||
|
{0.0, request->displacement_absolute_scale},
|
||||||
|
samples);
|
||||||
|
const bool reactions_matched = append_samples(
|
||||||
|
frame,
|
||||||
|
reactions.rows,
|
||||||
|
{0.0, request->reaction_absolute_scale},
|
||||||
|
samples);
|
||||||
|
const bool internal_forces_matched = append_samples(
|
||||||
|
frame,
|
||||||
|
internal_forces.rows,
|
||||||
|
{0.0, request->internal_force_absolute_scale},
|
||||||
|
samples);
|
||||||
|
if (
|
||||||
|
!displacements_matched || !reactions_matched ||
|
||||||
|
!internal_forces_matched) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::CorrelationReport report = fesa::correlate_samples(samples);
|
||||||
|
std::cout << std::setprecision(std::numeric_limits<double>::max_digits10);
|
||||||
|
for (const fesa::ComponentCorrelationMetric& metric : report.metrics) {
|
||||||
|
std::cout << "metric quantity=" << quantity_name(metric.quantity)
|
||||||
|
<< " component="
|
||||||
|
<< component_name(metric.quantity, metric.component_index)
|
||||||
|
<< " count=" << metric.value_count
|
||||||
|
<< " rmse=" << metric.root_mean_square_error
|
||||||
|
<< " relative_l2=" << metric.relative_l2_error << '\n';
|
||||||
|
}
|
||||||
|
print_diagnostics(report.failures);
|
||||||
|
return report.evaluable ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
#include <fesa/validation/reference_csv.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::string_view instance_column{"Part Instance Name"};
|
||||||
|
constexpr std::string_view node_column{"Node Label"};
|
||||||
|
constexpr std::string_view element_column{"Element Label"};
|
||||||
|
|
||||||
|
using PositionKey =
|
||||||
|
std::tuple<std::string, std::int64_t, std::optional<std::int64_t>>;
|
||||||
|
using ColumnIndices =
|
||||||
|
std::map<std::string, std::size_t, std::less<>>;
|
||||||
|
|
||||||
|
std::size_t column_index(
|
||||||
|
const ColumnIndices& column_indices,
|
||||||
|
const std::string_view name) {
|
||||||
|
return column_indices.find(name)->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view trim(const std::string_view value) {
|
||||||
|
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
||||||
|
const std::size_t first = value.find_first_not_of(whitespace);
|
||||||
|
if (first == std::string_view::npos) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const std::size_t last = value.find_last_not_of(whitespace);
|
||||||
|
return value.substr(first, last - first + 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> split_fields(const std::string_view line) {
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
std::size_t first = 0U;
|
||||||
|
while (true) {
|
||||||
|
const std::size_t comma = line.find(',', first);
|
||||||
|
const std::string_view field =
|
||||||
|
comma == std::string_view::npos
|
||||||
|
? line.substr(first)
|
||||||
|
: line.substr(first, comma - first);
|
||||||
|
fields.emplace_back(trim(field));
|
||||||
|
if (comma == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
first = comma + 1U;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove_trailing_empty_fields(std::vector<std::string>& fields) {
|
||||||
|
while (!fields.empty() && fields.back().empty()) {
|
||||||
|
fields.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReferenceCsvReadResult failure(
|
||||||
|
const DiagnosticStage stage,
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::size_t line) {
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
diagnostics.push_back({
|
||||||
|
stage,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
SourceLocation{path, line, line == 0U ? 0U : 1U},
|
||||||
|
});
|
||||||
|
return {{}, std::move(diagnostics)};
|
||||||
|
}
|
||||||
|
|
||||||
|
ReferenceCsvReadResult validation_failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::size_t line) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::validation,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
path,
|
||||||
|
line);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_valid_utf8(const std::string_view text) {
|
||||||
|
std::size_t index = 0U;
|
||||||
|
while (index < text.size()) {
|
||||||
|
const auto first = static_cast<unsigned char>(text[index]);
|
||||||
|
if (first <= 0x7FU) {
|
||||||
|
++index;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t continuation_count = 0U;
|
||||||
|
std::uint32_t code_point = 0U;
|
||||||
|
std::uint32_t minimum = 0U;
|
||||||
|
if (first >= 0xC2U && first <= 0xDFU) {
|
||||||
|
continuation_count = 1U;
|
||||||
|
code_point = first & 0x1FU;
|
||||||
|
minimum = 0x80U;
|
||||||
|
} else if (first >= 0xE0U && first <= 0xEFU) {
|
||||||
|
continuation_count = 2U;
|
||||||
|
code_point = first & 0x0FU;
|
||||||
|
minimum = 0x800U;
|
||||||
|
} else if (first >= 0xF0U && first <= 0xF4U) {
|
||||||
|
continuation_count = 3U;
|
||||||
|
code_point = first & 0x07U;
|
||||||
|
minimum = 0x10000U;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (index + continuation_count >= text.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t offset = 1U; offset <= continuation_count; ++offset) {
|
||||||
|
const auto continuation =
|
||||||
|
static_cast<unsigned char>(text[index + offset]);
|
||||||
|
if ((continuation & 0xC0U) != 0x80U) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
code_point = (code_point << 6U) | (continuation & 0x3FU);
|
||||||
|
}
|
||||||
|
if (code_point < minimum || code_point > 0x10FFFFU ||
|
||||||
|
(code_point >= 0xD800U && code_point <= 0xDFFFU)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
index += continuation_count + 1U;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t line_at_offset(
|
||||||
|
const std::string_view text,
|
||||||
|
const std::size_t offset) {
|
||||||
|
return 1U + static_cast<std::size_t>(std::ranges::count(
|
||||||
|
text.substr(0U, offset), '\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string_view> required_columns(
|
||||||
|
const ReferenceQuantity quantity) {
|
||||||
|
switch (quantity) {
|
||||||
|
case ReferenceQuantity::displacement:
|
||||||
|
return {
|
||||||
|
node_column,
|
||||||
|
"U-U1",
|
||||||
|
"U-U2",
|
||||||
|
"U-U3",
|
||||||
|
"UR-UR1",
|
||||||
|
"UR-UR2",
|
||||||
|
"UR-UR3",
|
||||||
|
};
|
||||||
|
case ReferenceQuantity::reaction:
|
||||||
|
return {
|
||||||
|
node_column,
|
||||||
|
"RF-RF1",
|
||||||
|
"RF-RF2",
|
||||||
|
"RF-RF3",
|
||||||
|
"RM-RM1",
|
||||||
|
"RM-RM2",
|
||||||
|
"RM-RM3",
|
||||||
|
};
|
||||||
|
case ReferenceQuantity::internal_force:
|
||||||
|
return {
|
||||||
|
element_column,
|
||||||
|
node_column,
|
||||||
|
"SF-SF1",
|
||||||
|
"SF-SF2",
|
||||||
|
"SF-SF3",
|
||||||
|
"SM-SM1",
|
||||||
|
"SM-SM2",
|
||||||
|
"SM-SM3",
|
||||||
|
};
|
||||||
|
case ReferenceQuantity::centroid_stress:
|
||||||
|
return {element_column, node_column, "Sxx"};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string_view> value_columns(
|
||||||
|
const ReferenceQuantity quantity) {
|
||||||
|
switch (quantity) {
|
||||||
|
case ReferenceQuantity::displacement:
|
||||||
|
return {"U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"};
|
||||||
|
case ReferenceQuantity::reaction:
|
||||||
|
return {"RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"};
|
||||||
|
case ReferenceQuantity::internal_force:
|
||||||
|
return {"SF-SF1", "SF-SF3", "SF-SF2", "SM-SM3", "SM-SM1", "SM-SM2"};
|
||||||
|
case ReferenceQuantity::centroid_stress:
|
||||||
|
return {"Sxx"};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_positive_label(
|
||||||
|
std::string_view text,
|
||||||
|
std::int64_t& value) {
|
||||||
|
if (text.starts_with('+')) {
|
||||||
|
text.remove_prefix(1U);
|
||||||
|
}
|
||||||
|
const auto parsed = std::from_chars(
|
||||||
|
text.data(), text.data() + text.size(), value);
|
||||||
|
return !text.empty() && parsed.ec == std::errc{} &&
|
||||||
|
parsed.ptr == text.data() + text.size() && value > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_finite_double(std::string_view text, double& value) {
|
||||||
|
if (text.starts_with('+')) {
|
||||||
|
text.remove_prefix(1U);
|
||||||
|
}
|
||||||
|
const auto parsed = std::from_chars(
|
||||||
|
text.data(),
|
||||||
|
text.data() + text.size(),
|
||||||
|
value,
|
||||||
|
std::chars_format::general);
|
||||||
|
return !text.empty() && parsed.ec == std::errc{} &&
|
||||||
|
parsed.ptr == text.data() + text.size() &&
|
||||||
|
std::isfinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ReferenceCsvReadResult read_reference_csv(
|
||||||
|
const ReferenceQuantity quantity,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view single_instance_name) {
|
||||||
|
std::ifstream file{path, std::ios::binary};
|
||||||
|
if (!file) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"validation.reference_csv_open_failed",
|
||||||
|
"Unable to open reference CSV file.",
|
||||||
|
path,
|
||||||
|
0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string bytes{
|
||||||
|
std::istreambuf_iterator<char>{file},
|
||||||
|
std::istreambuf_iterator<char>{},
|
||||||
|
};
|
||||||
|
if (file.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"validation.reference_csv_read_failed",
|
||||||
|
"Failed while reading reference CSV file.",
|
||||||
|
path,
|
||||||
|
0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string_view bom{"\xEF\xBB\xBF"};
|
||||||
|
if (bytes.starts_with(bom)) {
|
||||||
|
bytes.erase(0U, bom.size());
|
||||||
|
}
|
||||||
|
const std::size_t misplaced_bom = bytes.find(bom);
|
||||||
|
if (misplaced_bom != std::string::npos || !is_valid_utf8(bytes)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_encoding",
|
||||||
|
"Reference CSV must be UTF-8 with an optional BOM only at the file start.",
|
||||||
|
path,
|
||||||
|
misplaced_bom == std::string::npos
|
||||||
|
? 1U
|
||||||
|
: line_at_offset(bytes, misplaced_bom));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::istringstream input{bytes};
|
||||||
|
std::string line;
|
||||||
|
if (!std::getline(input, line)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_header",
|
||||||
|
"Reference CSV is missing its header row.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> headers = split_fields(line);
|
||||||
|
remove_trailing_empty_fields(headers);
|
||||||
|
ColumnIndices column_indices;
|
||||||
|
for (std::size_t index = 0U; index < headers.size(); ++index) {
|
||||||
|
if (headers[index].empty() ||
|
||||||
|
!column_indices.emplace(headers[index], index).second) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_duplicate_column",
|
||||||
|
"Reference CSV contains an empty or duplicate column name.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string_view> required = required_columns(quantity);
|
||||||
|
for (const std::string_view name : required) {
|
||||||
|
if (!column_indices.contains(name)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_column",
|
||||||
|
"Reference CSV is missing required column '" +
|
||||||
|
std::string{name} + "'.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool has_instance = column_indices.contains(instance_column);
|
||||||
|
if (!has_instance && single_instance_name.empty()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_column",
|
||||||
|
"Reference CSV omits 'Part Instance Name' without a single-Instance name.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string, std::less<>> allowed_columns;
|
||||||
|
allowed_columns.emplace(instance_column);
|
||||||
|
for (const std::string_view name : required) {
|
||||||
|
allowed_columns.emplace(name);
|
||||||
|
}
|
||||||
|
for (const std::string& header : headers) {
|
||||||
|
if (!allowed_columns.contains(header)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_unsupported_column",
|
||||||
|
"Reference CSV contains unsupported column '" + header + "'.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string_view> components = value_columns(quantity);
|
||||||
|
std::vector<ReferenceRow> rows;
|
||||||
|
std::set<PositionKey> positions;
|
||||||
|
std::size_t line_number = 1U;
|
||||||
|
while (std::getline(input, line)) {
|
||||||
|
++line_number;
|
||||||
|
if (trim(line).empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::vector<std::string> fields = split_fields(line);
|
||||||
|
remove_trailing_empty_fields(fields);
|
||||||
|
if (fields.size() != headers.size()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_row",
|
||||||
|
"Reference CSV row field count does not match the header.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string instance_name = has_instance
|
||||||
|
? fields[column_index(
|
||||||
|
column_indices,
|
||||||
|
instance_column)]
|
||||||
|
: std::string{single_instance_name};
|
||||||
|
if (instance_name.empty()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_row",
|
||||||
|
"Reference CSV row has an empty Instance name.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t entity_label = 0;
|
||||||
|
const std::string_view entity_column =
|
||||||
|
quantity == ReferenceQuantity::displacement ||
|
||||||
|
quantity == ReferenceQuantity::reaction
|
||||||
|
? node_column
|
||||||
|
: element_column;
|
||||||
|
if (!parse_positive_label(
|
||||||
|
fields[column_index(column_indices, entity_column)],
|
||||||
|
entity_label)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_number",
|
||||||
|
"Reference CSV entity label must be a positive integer.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::int64_t> end_node_label;
|
||||||
|
if (quantity == ReferenceQuantity::internal_force ||
|
||||||
|
quantity == ReferenceQuantity::centroid_stress) {
|
||||||
|
std::int64_t parsed_end_node = 0;
|
||||||
|
if (!parse_positive_label(
|
||||||
|
fields[column_index(column_indices, node_column)],
|
||||||
|
parsed_end_node)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_number",
|
||||||
|
"Reference CSV end-node label must be a positive integer.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
end_node_label = parsed_end_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> values;
|
||||||
|
values.reserve(components.size());
|
||||||
|
for (const std::string_view component : components) {
|
||||||
|
double value = 0.0;
|
||||||
|
if (!parse_finite_double(
|
||||||
|
fields[column_index(column_indices, component)],
|
||||||
|
value)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_number",
|
||||||
|
"Reference CSV component '" + std::string{component} +
|
||||||
|
"' must be a finite number.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
values.push_back(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
PositionKey key{instance_name, entity_label, end_node_label};
|
||||||
|
if (!positions.insert(key).second) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_duplicate_row",
|
||||||
|
"Reference CSV contains a duplicate result position.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
rows.push_back({
|
||||||
|
quantity,
|
||||||
|
{std::move(instance_name), entity_label, end_node_label},
|
||||||
|
std::move(values),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"validation.reference_csv_read_failed",
|
||||||
|
"Failed while reading reference CSV file.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
if (rows.empty()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_rows",
|
||||||
|
"Reference CSV contains no result rows.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
return {std::move(rows), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
+190
-2
@@ -360,6 +360,24 @@ add_test(
|
|||||||
--gtest_filter=RigidBody.*
|
--gtest_filter=RigidBody.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME BeamRecovery
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||||
|
--gtest_filter=BeamRecovery.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SectionForce
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||||
|
--gtest_filter=SectionForce.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CentroidStress
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||||
|
--gtest_filter=CentroidStress.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_serial_assembly_tests
|
add_executable(fesa_serial_assembly_tests
|
||||||
unit/assembly/serial_assembler_test.cpp
|
unit/assembly/serial_assembler_test.cpp
|
||||||
)
|
)
|
||||||
@@ -397,6 +415,91 @@ add_test(
|
|||||||
--gtest_filter=SymmetricCsr.*
|
--gtest_filter=SymmetricCsr.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CanonicalContribution
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_serial_assembly_tests>"
|
||||||
|
--gtest_filter=CanonicalContribution.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME DeterministicMerge
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_serial_assembly_tests>"
|
||||||
|
--gtest_filter=DeterministicMerge.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(fesa_parallel_assembly_tests
|
||||||
|
unit/assembly/parallel_assembler_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(fesa_parallel_assembly_tests PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(
|
||||||
|
fesa_parallel_assembly_tests
|
||||||
|
PRIVATE
|
||||||
|
/W4
|
||||||
|
/permissive-
|
||||||
|
/EHsc
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(fesa_parallel_assembly_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ParallelAssembly
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_parallel_assembly_tests>"
|
||||||
|
--gtest_filter=ParallelAssembly.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME TbbElementEvaluation
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_parallel_assembly_tests>"
|
||||||
|
--gtest_filter=TbbElementEvaluation.*
|
||||||
|
)
|
||||||
|
|
||||||
|
set_property(
|
||||||
|
TEST ParallelAssembly TbbElementEvaluation
|
||||||
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(fesa_thread_count_determinism_tests
|
||||||
|
integration/assembly/thread_count_determinism_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(
|
||||||
|
fesa_thread_count_determinism_tests PRIVATE cxx_std_20
|
||||||
|
)
|
||||||
|
target_compile_options(
|
||||||
|
fesa_thread_count_determinism_tests
|
||||||
|
PRIVATE
|
||||||
|
/W4
|
||||||
|
/permissive-
|
||||||
|
/EHsc
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(fesa_thread_count_determinism_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ThreadCountDeterminism
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_thread_count_determinism_tests>"
|
||||||
|
)
|
||||||
|
|
||||||
|
set_property(
|
||||||
|
TEST ThreadCountDeterminism
|
||||||
|
PROPERTY ENVIRONMENT "MKL_NUM_THREADS=1"
|
||||||
|
)
|
||||||
|
set_property(
|
||||||
|
TEST ThreadCountDeterminism
|
||||||
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_constraint_tests
|
add_executable(fesa_constraint_tests
|
||||||
unit/constraints/essential_bc_test.cpp
|
unit/constraints/essential_bc_test.cpp
|
||||||
)
|
)
|
||||||
@@ -495,6 +598,18 @@ add_test(
|
|||||||
--gtest_filter=ResultDatabase.*
|
--gtest_filter=ResultDatabase.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ElementFrame
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=ElementFrame.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ResultContractMetadata
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=CompleteResultContract.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_hdf5_results_tests
|
add_executable(fesa_hdf5_results_tests
|
||||||
integration/io/hdf5_results_test.cpp
|
integration/io/hdf5_results_test.cpp
|
||||||
)
|
)
|
||||||
@@ -532,8 +647,14 @@ add_test(
|
|||||||
--gtest_filter=ResultRoundTrip.*
|
--gtest_filter=ResultRoundTrip.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SelfContainedHdf5
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
|
||||||
|
--gtest_filter=SelfContainedHdf5.*
|
||||||
|
)
|
||||||
|
|
||||||
set_property(
|
set_property(
|
||||||
TEST Hdf5 ResultRoundTrip
|
TEST Hdf5 ResultRoundTrip SelfContainedHdf5
|
||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
)
|
)
|
||||||
@@ -571,8 +692,14 @@ add_test(
|
|||||||
--gtest_filter=StaticEquilibrium.*
|
--gtest_filter=StaticEquilibrium.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CompleteResultContract
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_linear_static_analysis_tests>"
|
||||||
|
--gtest_filter=CompleteResultContract.*
|
||||||
|
)
|
||||||
|
|
||||||
set_property(
|
set_property(
|
||||||
TEST LinearStaticAnalysis StaticEquilibrium
|
TEST LinearStaticAnalysis StaticEquilibrium CompleteResultContract
|
||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
)
|
)
|
||||||
@@ -616,3 +743,64 @@ set_property(
|
|||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_executable(fesa_validation_comparison_tests
|
||||||
|
unit/validation/comparison_test.cpp
|
||||||
|
unit/validation/reference_csv_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(
|
||||||
|
fesa_validation_comparison_tests PRIVATE cxx_std_20
|
||||||
|
)
|
||||||
|
target_compile_options(
|
||||||
|
fesa_validation_comparison_tests PRIVATE /W4 /permissive- /EHsc
|
||||||
|
)
|
||||||
|
target_compile_definitions(
|
||||||
|
fesa_validation_comparison_tests
|
||||||
|
PRIVATE
|
||||||
|
FESA_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(
|
||||||
|
fesa_validation_comparison_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ComparisonMetric
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=ComparisonMetric.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CorrelationMetric
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=CorrelationMetric.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME EntityMatching
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=EntityMatching.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ReferenceCsv
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=ReferenceCsv.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME InternalForceCsv
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=InternalForceCsv.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME StressCsv
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=StressCsv.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_subdirectory(reference)
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
Part Instance Name, Element Label, Node Label, SF-SF1, SF-SF2, SF-SF3, SM-SM1, SM-SM2, SM-SM3
|
||||||
|
Part-1-1 , 501 , 101 , 1.25 , -2.5 , 3.75 , -4.0 , 5.5 , -6.25
|
||||||
|
Part-1-1 , 501 , 102 , 7.0 , 8.0 , 9.0 , 10.0 , 11.0 , 12.0
|
||||||
|
+3
@@ -0,0 +1,3 @@
|
|||||||
|
Element Label, Node Label, Sxx
|
||||||
|
501, 101, 42.5
|
||||||
|
501, 102, -17.25
|
||||||
|
@@ -0,0 +1,211 @@
|
|||||||
|
#include <fesa/assembly/assembler.hpp>
|
||||||
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <bit>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/constraints/essential_bc.hpp>
|
||||||
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::size_t branch_count = 24;
|
||||||
|
constexpr std::size_t repeat_count = 10;
|
||||||
|
|
||||||
|
fesa::Domain build_branched_cantilever() {
|
||||||
|
fesa::DomainBuilder builder;
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
|
||||||
|
fesa::Vec3{0.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
|
||||||
|
fesa::Vec3{1.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
for (std::size_t index = 0; index < branch_count; ++index) {
|
||||||
|
const double y = static_cast<double>(
|
||||||
|
static_cast<int>(index % 7) - 3) * 0.35;
|
||||||
|
const double z = static_cast<double>(
|
||||||
|
static_cast<int>((index * 3) % 11) - 5) * 0.22 + 0.1;
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{static_cast<std::int64_t>(index + 2)},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart",
|
||||||
|
"Beam-1",
|
||||||
|
static_cast<std::int64_t>(index + 3),
|
||||||
|
},
|
||||||
|
fesa::Vec3{
|
||||||
|
2.0 + static_cast<double>(index) * 0.05,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
builder.add_material({
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
"Steel",
|
||||||
|
210.0e9,
|
||||||
|
0.3,
|
||||||
|
});
|
||||||
|
builder.add_section({
|
||||||
|
fesa::SectionId{0},
|
||||||
|
"General",
|
||||||
|
0.02,
|
||||||
|
3.0e-5,
|
||||||
|
4.0e-5,
|
||||||
|
2.0e-5,
|
||||||
|
0.015,
|
||||||
|
0.016,
|
||||||
|
fesa::ShearPropertySource::input,
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
{},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (std::size_t offset = 0; offset < branch_count; ++offset) {
|
||||||
|
const std::size_t index = branch_count - offset - 1;
|
||||||
|
builder.add_beam_element({
|
||||||
|
fesa::ElementId{static_cast<std::int64_t>(index + 1)},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart",
|
||||||
|
"Beam-1",
|
||||||
|
static_cast<std::int64_t>(index + 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::NodeId{1},
|
||||||
|
fesa::NodeId{static_cast<std::int64_t>(index + 2)},
|
||||||
|
},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
builder.add_beam_element({
|
||||||
|
fesa::ElementId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<fesa::PrescribedDof> fixed;
|
||||||
|
fixed.reserve(6);
|
||||||
|
for (std::uint8_t dof = 1; dof <= 6; ++dof) {
|
||||||
|
fixed.push_back({fesa::NodeId{0}, dof, 0.0});
|
||||||
|
}
|
||||||
|
builder.set_step({
|
||||||
|
"Load",
|
||||||
|
std::move(fixed),
|
||||||
|
{{
|
||||||
|
fesa::NodeId{static_cast<std::int64_t>(branch_count + 1)},
|
||||||
|
{1000.0, -2500.0, 1750.0, 20.0, -30.0, 40.0},
|
||||||
|
}},
|
||||||
|
});
|
||||||
|
|
||||||
|
auto result = std::move(builder).build();
|
||||||
|
if (!result.domain.has_value()) {
|
||||||
|
throw std::runtime_error{"Test Domain failed validation."};
|
||||||
|
}
|
||||||
|
return std::move(*result.domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint64_t> bits(const std::vector<double>& values) {
|
||||||
|
std::vector<std::uint64_t> result;
|
||||||
|
result.reserve(values.size());
|
||||||
|
for (const double value : values) {
|
||||||
|
result.push_back(std::bit_cast<std::uint64_t>(value));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void expect_bitwise_equal(
|
||||||
|
const fesa::EquationSystem& expected,
|
||||||
|
const fesa::EquationSystem& actual) {
|
||||||
|
EXPECT_EQ(actual.stiffness.order, expected.stiffness.order);
|
||||||
|
EXPECT_EQ(actual.stiffness.row_offsets, expected.stiffness.row_offsets);
|
||||||
|
EXPECT_EQ(
|
||||||
|
actual.stiffness.column_indices,
|
||||||
|
expected.stiffness.column_indices);
|
||||||
|
EXPECT_EQ(bits(actual.stiffness.values), bits(expected.stiffness.values));
|
||||||
|
EXPECT_EQ(bits(actual.force), bits(expected.force));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LinearState final {
|
||||||
|
std::vector<double> displacement;
|
||||||
|
std::vector<double> reaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
LinearState solve_linear_system(
|
||||||
|
const fesa::EquationSystem& system,
|
||||||
|
const fesa::DofManager& dofs) {
|
||||||
|
fesa::ConstraintResult constrained =
|
||||||
|
fesa::eliminate_essential_bcs(system, dofs);
|
||||||
|
if (!constrained.reduced_system.has_value()) {
|
||||||
|
throw std::runtime_error{"Constraint elimination failed."};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::PardisoLinearSolver solver;
|
||||||
|
fesa::LinearSolveResult solved = solver.solve(
|
||||||
|
constrained.reduced_system->stiffness,
|
||||||
|
constrained.reduced_system->force);
|
||||||
|
if (!solved.diagnostics.empty()) {
|
||||||
|
throw std::runtime_error{"Linear solve failed."};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> displacement =
|
||||||
|
dofs.reconstruct_full(solved.solution);
|
||||||
|
return {
|
||||||
|
displacement,
|
||||||
|
fesa::recover_reaction(system, displacement),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t available_concurrency() {
|
||||||
|
return std::max(
|
||||||
|
std::size_t{1},
|
||||||
|
static_cast<std::size_t>(std::thread::hardware_concurrency()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThreadCountDeterminism, AssemblyAndLinearStateAreBitwiseStable) {
|
||||||
|
const fesa::Domain domain = build_branched_cantilever();
|
||||||
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
const fesa::EquationSystem serial = fesa::assemble_serial(domain, dofs);
|
||||||
|
const LinearState serial_state = solve_linear_system(serial, dofs);
|
||||||
|
const std::array<std::size_t, 3> thread_counts{
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
available_concurrency(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (std::size_t repeat = 0; repeat < repeat_count; ++repeat) {
|
||||||
|
for (const std::size_t threads : thread_counts) {
|
||||||
|
SCOPED_TRACE(::testing::Message{}
|
||||||
|
<< "repeat=" << repeat << ", threads=" << threads);
|
||||||
|
const fesa::EquationSystem parallel =
|
||||||
|
fesa::assemble_parallel(domain, dofs, {threads, 1});
|
||||||
|
expect_bitwise_equal(serial, parallel);
|
||||||
|
|
||||||
|
const LinearState parallel_state =
|
||||||
|
solve_linear_system(parallel, dofs);
|
||||||
|
EXPECT_EQ(
|
||||||
|
bits(parallel_state.displacement),
|
||||||
|
bits(serial_state.displacement));
|
||||||
|
EXPECT_EQ(
|
||||||
|
bits(parallel_state.reaction),
|
||||||
|
bits(serial_state.reaction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -98,6 +98,63 @@ void replace_step_time_with_vector(const std::filesystem::path& path) {
|
|||||||
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void delete_link(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view link_path) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_link_path{link_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(
|
||||||
|
H5Ldelete(file.get(), owned_link_path.c_str(), H5P_DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void copy_object(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view source_path,
|
||||||
|
const std::string_view target_path) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_source_path{source_path};
|
||||||
|
const std::string owned_target_path{target_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(H5Ocopy(
|
||||||
|
file.get(),
|
||||||
|
owned_source_path.c_str(),
|
||||||
|
file.get(),
|
||||||
|
owned_target_path.c_str(),
|
||||||
|
H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_double_attribute(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view object_path,
|
||||||
|
const std::string_view attribute_name,
|
||||||
|
const double value) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_object_path{object_path};
|
||||||
|
const std::string owned_attribute_name{attribute_name};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle object{
|
||||||
|
H5Oopen(file.get(), owned_object_path.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Oclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle attribute{
|
||||||
|
H5Aopen(object.get(), owned_attribute_name.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Aclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(
|
||||||
|
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value));
|
||||||
|
}
|
||||||
|
|
||||||
void write_int64_dataset(
|
void write_int64_dataset(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const std::string_view dataset_path,
|
const std::string_view dataset_path,
|
||||||
@@ -146,12 +203,69 @@ void write_double_dataset(
|
|||||||
values.data()));
|
values.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void write_uint8_dataset(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view dataset_path,
|
||||||
|
const std::span<const std::uint8_t> values) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_dataset_path{dataset_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle dataset{
|
||||||
|
H5Dopen2(
|
||||||
|
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Dclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(H5Dwrite(
|
||||||
|
dataset.get(),
|
||||||
|
H5T_NATIVE_UINT8,
|
||||||
|
H5S_ALL,
|
||||||
|
H5S_ALL,
|
||||||
|
H5P_DEFAULT,
|
||||||
|
values.data()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_root_string_attribute(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view name,
|
||||||
|
const std::string_view value) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_name{name};
|
||||||
|
const std::string owned_value{value};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle attribute{
|
||||||
|
H5Aopen(file.get(), owned_name.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Aclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle type{H5Aget_type(attribute.get()), &H5Tclose};
|
||||||
|
const char* pointer = owned_value.c_str();
|
||||||
|
require_hdf5_status(H5Awrite(attribute.get(), type.get(), &pointer));
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path round_trip_path() {
|
std::filesystem::path round_trip_path() {
|
||||||
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||||
"Temporary" / "fesa-round-trip.h5";
|
"Temporary" / "fesa-round-trip.h5";
|
||||||
}
|
}
|
||||||
|
|
||||||
fesa::Domain make_domain() {
|
std::filesystem::path self_contained_path() {
|
||||||
|
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||||
|
"Temporary" / "fesa-self-contained.h5";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::Hdf5InputIdentity& test_input_identity() {
|
||||||
|
static const fesa::Hdf5InputIdentity identity{
|
||||||
|
"beam model.inp",
|
||||||
|
"fnv1a64:0123456789abcdef",
|
||||||
|
};
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Domain make_domain(const bool add_unreported_node = false) {
|
||||||
fesa::DomainBuilder builder;
|
fesa::DomainBuilder builder;
|
||||||
builder.add_node({
|
builder.add_node({
|
||||||
fesa::NodeId{42},
|
fesa::NodeId{42},
|
||||||
@@ -161,8 +275,15 @@ fesa::Domain make_domain() {
|
|||||||
builder.add_node({
|
builder.add_node({
|
||||||
fesa::NodeId{7},
|
fesa::NodeId{7},
|
||||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
||||||
fesa::Vec3{4.0, 5.5, -6.25},
|
fesa::Vec3{2.25, -2.5, 3.75},
|
||||||
});
|
});
|
||||||
|
if (add_unreported_node) {
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{99},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1003},
|
||||||
|
fesa::Vec3{8.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
}
|
||||||
builder.add_material({
|
builder.add_material({
|
||||||
fesa::MaterialId{6},
|
fesa::MaterialId{6},
|
||||||
"Steel",
|
"Steel",
|
||||||
@@ -180,7 +301,7 @@ fesa::Domain make_domain() {
|
|||||||
0.032,
|
0.032,
|
||||||
fesa::ShearPropertySource::input,
|
fesa::ShearPropertySource::input,
|
||||||
fesa::Vec3{0.0, 1.0, 0.0},
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
{},
|
{{0.25, -0.5}, {-0.75, 0.125}},
|
||||||
});
|
});
|
||||||
builder.add_section({
|
builder.add_section({
|
||||||
fesa::SectionId{12},
|
fesa::SectionId{12},
|
||||||
@@ -209,7 +330,22 @@ fesa::Domain make_domain() {
|
|||||||
fesa::MaterialId{6},
|
fesa::MaterialId{6},
|
||||||
fesa::SectionId{4},
|
fesa::SectionId{4},
|
||||||
});
|
});
|
||||||
builder.set_step({"Load/Case", {}, {}});
|
builder.add_node_set({"Fixed", {fesa::NodeId{42}}});
|
||||||
|
builder.add_node_set(
|
||||||
|
{"Loaded", {fesa::NodeId{7}, fesa::NodeId{42}}});
|
||||||
|
builder.add_element_set(
|
||||||
|
{"AllBeams", {fesa::ElementId{9}, fesa::ElementId{17}}});
|
||||||
|
builder.set_step({
|
||||||
|
"Load/Case",
|
||||||
|
{
|
||||||
|
{fesa::NodeId{42}, 1, 0.0},
|
||||||
|
{fesa::NodeId{7}, 6, 0.125},
|
||||||
|
},
|
||||||
|
{{
|
||||||
|
fesa::NodeId{7},
|
||||||
|
{100.0, -200.0, 300.0, -400.0, 500.0, -600.0},
|
||||||
|
}},
|
||||||
|
});
|
||||||
|
|
||||||
auto built = std::move(builder).build();
|
auto built = std::move(builder).build();
|
||||||
EXPECT_TRUE(built.domain.has_value());
|
EXPECT_TRUE(built.domain.has_value());
|
||||||
@@ -217,15 +353,26 @@ fesa::Domain make_domain() {
|
|||||||
return std::move(*built.domain);
|
return std::move(*built.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fesa::BeamSectionResult make_end_result(
|
||||||
|
double xi,
|
||||||
|
fesa::NodeId node,
|
||||||
|
double offset);
|
||||||
|
|
||||||
fesa::ResultDatabase make_database() {
|
fesa::ResultDatabase make_database() {
|
||||||
return {
|
fesa::ResultDatabase database{
|
||||||
"1.0.0",
|
"2.0.0",
|
||||||
{{
|
{{
|
||||||
"Load/Case",
|
"Load/Case",
|
||||||
{{
|
{{
|
||||||
1.25,
|
1.25,
|
||||||
{
|
{
|
||||||
{fesa::NodeId{7}, fesa::NodeId{42}},
|
{fesa::NodeId{7}, fesa::NodeId{42}},
|
||||||
|
{
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart", "Beam-1", 1002},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart", "Beam-1", 1001},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
||||||
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
||||||
@@ -236,9 +383,89 @@ fesa::ResultDatabase make_database() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
|
{},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
|
database.steps[0].frames[0].element.beams = {
|
||||||
|
{
|
||||||
|
fesa::ElementId{9},
|
||||||
|
{"BeamPart", "Beam-1", 2001},
|
||||||
|
{
|
||||||
|
{-1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, -1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
make_end_result(-1.0, fesa::NodeId{7}, 0.0),
|
||||||
|
make_end_result(1.0, fesa::NodeId{42}, 100.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::ElementId{17},
|
||||||
|
{"BeamPart", "Beam-1", 2002},
|
||||||
|
{
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
make_end_result(-1.0, fesa::NodeId{42}, 200.0),
|
||||||
|
make_end_result(1.0, fesa::NodeId{7}, 300.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::BeamSectionResult make_end_result(
|
||||||
|
const double xi,
|
||||||
|
const fesa::NodeId node,
|
||||||
|
const double offset) {
|
||||||
|
return {
|
||||||
|
xi,
|
||||||
|
node,
|
||||||
|
{
|
||||||
|
offset + 1.0,
|
||||||
|
offset + 2.0,
|
||||||
|
offset + 3.0,
|
||||||
|
offset + 4.0,
|
||||||
|
offset + 5.0,
|
||||||
|
offset + 6.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset + 10.0,
|
||||||
|
offset + 20.0,
|
||||||
|
offset + 30.0,
|
||||||
|
offset + 40.0,
|
||||||
|
offset + 50.0,
|
||||||
|
offset + 60.0,
|
||||||
|
},
|
||||||
|
offset + 70.0,
|
||||||
|
{offset + 80.0, offset + 90.0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::ResultDatabase make_complete_database() {
|
||||||
|
fesa::ResultDatabase database = make_database();
|
||||||
|
auto& frame = database.steps[0].frames[0];
|
||||||
|
frame.diagnostics = {
|
||||||
|
{
|
||||||
|
fesa::DiagnosticStage::solver,
|
||||||
|
fesa::Severity::warning,
|
||||||
|
"solver.residual",
|
||||||
|
"Residual diagnostic",
|
||||||
|
fesa::SourceLocation{"beam model.inp", 41, 7},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::DiagnosticStage::results,
|
||||||
|
fesa::Severity::error,
|
||||||
|
"results.equilibrium",
|
||||||
|
"Equilibrium diagnostic",
|
||||||
|
std::nullopt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_results_error(
|
bool has_results_error(
|
||||||
@@ -260,8 +487,8 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
const auto domain = make_domain();
|
const auto domain = make_domain();
|
||||||
const auto database = make_database();
|
const auto database = make_database();
|
||||||
|
|
||||||
const auto write_diagnostics =
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, domain, database);
|
path, domain, database, test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
|
|
||||||
const auto read = fesa::read_hdf5_results(path);
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
@@ -269,7 +496,7 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
ASSERT_TRUE(read.database.has_value());
|
ASSERT_TRUE(read.database.has_value());
|
||||||
ASSERT_TRUE(read.model.has_value());
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
|
||||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||||
ASSERT_EQ(read.database->steps.size(), 1U);
|
ASSERT_EQ(read.database->steps.size(), 1U);
|
||||||
const auto& step = read.database->steps[0];
|
const auto& step = read.database->steps[0];
|
||||||
EXPECT_EQ(step.name, "Load/Case");
|
EXPECT_EQ(step.name, "Load/Case");
|
||||||
@@ -279,6 +506,12 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
frame.nodal.node_ids,
|
frame.nodal.node_ids,
|
||||||
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.nodal.origins,
|
||||||
|
(std::vector<fesa::EntityOrigin>{
|
||||||
|
{"BeamPart", "Beam-1", 1002},
|
||||||
|
{"BeamPart", "Beam-1", 1001},
|
||||||
|
}));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
frame.nodal.displacement[0],
|
frame.nodal.displacement[0],
|
||||||
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
||||||
@@ -308,9 +541,9 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
read.model->nodes[1].origin,
|
read.model->nodes[1].origin,
|
||||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 4.0);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 2.25);
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, 5.5);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, -2.5);
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, -6.25);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, 3.75);
|
||||||
|
|
||||||
ASSERT_EQ(read.model->elements.size(), 2U);
|
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||||
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
||||||
@@ -341,6 +574,223 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
fesa::ShearPropertySource::phase1_default);
|
fesa::ShearPropertySource::phase1_default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
||||||
|
const auto path = self_contained_path();
|
||||||
|
std::filesystem::create_directories(path.parent_path());
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path,
|
||||||
|
make_domain(),
|
||||||
|
make_complete_database(),
|
||||||
|
test_input_identity())
|
||||||
|
.empty());
|
||||||
|
|
||||||
|
const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(path);
|
||||||
|
ASSERT_TRUE(read.diagnostics.empty());
|
||||||
|
ASSERT_TRUE(read.metadata.has_value());
|
||||||
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
ASSERT_TRUE(read.analysis.has_value());
|
||||||
|
ASSERT_TRUE(read.database.has_value());
|
||||||
|
|
||||||
|
EXPECT_EQ(read.metadata->schema_version, "2.0.0");
|
||||||
|
EXPECT_EQ(read.metadata->fesa_version, "0.1.0");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->unit_policy,
|
||||||
|
"consistent_input_units_no_conversion");
|
||||||
|
EXPECT_EQ(read.metadata->input_source, "beam model.inp");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_fingerprint,
|
||||||
|
"fnv1a64:0123456789abcdef");
|
||||||
|
|
||||||
|
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||||
|
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->elements[0].origin,
|
||||||
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 2001}));
|
||||||
|
EXPECT_EQ(read.model->elements[0].material, fesa::MaterialId{6});
|
||||||
|
ASSERT_EQ(read.model->materials.size(), 1U);
|
||||||
|
EXPECT_EQ(read.model->materials[0].name, "Steel");
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->materials[0].young, 210.0e9);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->materials[0].poisson, 0.3);
|
||||||
|
ASSERT_EQ(read.model->sections.size(), 2U);
|
||||||
|
EXPECT_EQ(read.model->sections[0].name, "InputShear");
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].area, 0.04);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].iy, 1.2e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].iz, 1.4e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].torsion_j, 2.0e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(read.model->sections[0].orientation.y, 1.0);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->sections[0].recovery_points,
|
||||||
|
(std::vector<std::array<double, 2>>{
|
||||||
|
{0.25, -0.5}, {-0.75, 0.125}}));
|
||||||
|
ASSERT_EQ(read.model->node_sets.size(), 2U);
|
||||||
|
EXPECT_EQ(read.model->node_sets[0].name, "Fixed");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->node_sets[1].members,
|
||||||
|
(std::vector<fesa::NodeId>{
|
||||||
|
fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||||
|
ASSERT_EQ(read.model->element_sets.size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.model->element_sets[0].members,
|
||||||
|
(std::vector<fesa::ElementId>{
|
||||||
|
fesa::ElementId{9}, fesa::ElementId{17}}));
|
||||||
|
|
||||||
|
EXPECT_EQ(read.analysis->step.name, "Load/Case");
|
||||||
|
ASSERT_EQ(read.analysis->step.prescribed_dofs.size(), 2U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.analysis->step.prescribed_dofs[1].node,
|
||||||
|
fesa::NodeId{7});
|
||||||
|
EXPECT_EQ(read.analysis->step.prescribed_dofs[1].dof, 6U);
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
read.analysis->step.prescribed_dofs[1].value, 0.125);
|
||||||
|
ASSERT_EQ(read.analysis->step.nodal_loads.size(), 1U);
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
read.analysis->step.nodal_loads[0].values[5], -600.0);
|
||||||
|
EXPECT_EQ(read.analysis->solver.backend, "mkl_pardiso");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.analysis->solver.constraint_method,
|
||||||
|
"essential_dof_elimination");
|
||||||
|
EXPECT_EQ(read.analysis->solver.assembly, "deterministic_serial");
|
||||||
|
|
||||||
|
const auto& frame = read.database->steps[0].frames[0];
|
||||||
|
ASSERT_EQ(frame.element.beams.size(), 2U);
|
||||||
|
EXPECT_EQ(frame.element.beams[0].element, fesa::ElementId{9});
|
||||||
|
EXPECT_DOUBLE_EQ(frame.element.beams[0].local_frame.ex.x, -1.0);
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.element.beams[0].end_results[0].end_node,
|
||||||
|
fesa::NodeId{7});
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.element.beams[0].end_results[1].section_force,
|
||||||
|
(std::array<double, 6>{
|
||||||
|
110.0, 120.0, 130.0, 140.0, 150.0, 160.0}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.element.beams[1].end_results[0].sigma_xx,
|
||||||
|
(std::vector<double>{280.0, 290.0}));
|
||||||
|
ASSERT_EQ(frame.diagnostics.size(), 2U);
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].stage, fesa::DiagnosticStage::solver);
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].severity, fesa::Severity::warning);
|
||||||
|
ASSERT_TRUE(frame.diagnostics[0].source.has_value());
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].source->file, "beam model.inp");
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].source->line, 41U);
|
||||||
|
EXPECT_EQ(frame.diagnostics[0].source->column, 7U);
|
||||||
|
EXPECT_FALSE(frame.diagnostics[1].source.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsMissingResultFrameBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-result-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames.clear();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsIncompleteNodalCoverageBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-incomplete-nodal-results.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
const auto database = make_complete_database();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(true), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsIncompleteBeamCoverageBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-incomplete-beam-results.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames[0].element.beams.pop_back();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsFiniteLocalFrameThatDisagreesWithModel) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-wrong-local-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames[0].element.beams[0].local_frame = {
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.result_element_mismatch"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" / "fesa-schema-1.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_database();
|
||||||
|
database.schema_version = "1.0.0";
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsMissingInputIdentityBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-input-identity.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), {"", ""});
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.invalid_input_identity"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsUnlistedMinorSchemaVersion) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" / "fesa-schema-2-1.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
write_root_string_attribute(path, "schema_version", "2.1.0");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.unsupported_schema"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" / "fesa-invalid.h5";
|
"Testing" / "Temporary" / "fesa-invalid.h5";
|
||||||
@@ -349,14 +799,15 @@ TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
|||||||
auto database = make_database();
|
auto database = make_database();
|
||||||
database.steps[0].frames[0].nodal.reaction.pop_back();
|
database.steps[0].frames[0].nodal.reaction.pop_back();
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(
|
EXPECT_TRUE(
|
||||||
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
||||||
EXPECT_FALSE(std::filesystem::exists(path));
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-unsupported-diagnostics.h5";
|
"fesa-unsupported-diagnostics.h5";
|
||||||
@@ -371,11 +822,21 @@ TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
|||||||
std::nullopt,
|
std::nullopt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(has_results_error(
|
ASSERT_TRUE(diagnostics.empty());
|
||||||
diagnostics, "hdf5.unsupported_result_diagnostics"));
|
|
||||||
EXPECT_FALSE(std::filesystem::exists(path));
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
ASSERT_TRUE(read.diagnostics.empty());
|
||||||
|
ASSERT_TRUE(read.database.has_value());
|
||||||
|
const auto& stored = read.database->steps[0].frames[0].diagnostics;
|
||||||
|
ASSERT_EQ(stored.size(), 1U);
|
||||||
|
EXPECT_EQ(stored[0].stage, fesa::DiagnosticStage::solver);
|
||||||
|
EXPECT_EQ(stored[0].severity, fesa::Severity::warning);
|
||||||
|
EXPECT_EQ(stored[0].code, "solver.residual");
|
||||||
|
EXPECT_EQ(stored[0].message, "Residual diagnostic");
|
||||||
|
EXPECT_FALSE(stored[0].source.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
||||||
@@ -385,9 +846,14 @@ TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
|||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto domain = make_domain();
|
const auto domain = make_domain();
|
||||||
auto database = make_database();
|
auto database = make_database();
|
||||||
database.steps[0].frames[0].nodal.node_ids[0] = fesa::NodeId{999};
|
auto& nodal = database.steps[0].frames[0].nodal;
|
||||||
|
nodal.node_ids.push_back(fesa::NodeId{999});
|
||||||
|
nodal.origins.push_back({"BeamPart", "Beam-1", 1999});
|
||||||
|
nodal.displacement.push_back({});
|
||||||
|
nodal.reaction.push_back({});
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(has_results_error(
|
EXPECT_TRUE(has_results_error(
|
||||||
diagnostics, "hdf5.result_node_not_in_model"));
|
diagnostics, "hdf5.result_node_not_in_model"));
|
||||||
@@ -411,8 +877,8 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonscalar-step-time.h5";
|
"fesa-nonscalar-step-time.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto write_diagnostics =
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database());
|
path, make_domain(), make_database(), test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
replace_step_time_with_vector(path);
|
replace_step_time_with_vector(path);
|
||||||
|
|
||||||
@@ -423,19 +889,88 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
|||||||
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedResultWithoutRequiredFrame) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-serialized-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
delete_link(path, "/results/steps/0/frames/0");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedResultWithExtraFrame) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-extra-serialized-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
copy_object(
|
||||||
|
path,
|
||||||
|
"/results/steps/0/frames/0",
|
||||||
|
"/results/steps/0/frames/1");
|
||||||
|
write_double_attribute(
|
||||||
|
path, "/results/steps/0/frames/1", "step_time", 2.5);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsInvalidSerializedInputFingerprint) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-invalid-input-fingerprint.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
write_root_string_attribute(
|
||||||
|
path, "input_fingerprint", "sha256:not-the-contract");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.invalid_input_identity"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-invalid-result-node.h5";
|
"fesa-invalid-result-node.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto write_diagnostics =
|
auto database = make_database();
|
||||||
fesa::write_hdf5(path, make_domain(), make_database());
|
auto& nodal = database.steps[0].frames[0].nodal;
|
||||||
|
nodal.node_ids.push_back(fesa::NodeId{99});
|
||||||
|
nodal.origins.push_back({"BeamPart", "Beam-1", 1003});
|
||||||
|
nodal.displacement.push_back({});
|
||||||
|
nodal.reaction.push_back({});
|
||||||
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(true), database, test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
const std::array<std::int64_t, 2> node_ids{999, 42};
|
const std::array<std::int64_t, 3> node_ids{42, 7, 100};
|
||||||
write_int64_dataset(
|
write_int64_dataset(path, "/model/nodes/internal_id", node_ids);
|
||||||
path,
|
|
||||||
"/results/steps/0/frames/0/nodal/node_ids",
|
|
||||||
node_ids);
|
|
||||||
|
|
||||||
const auto read = fesa::read_hdf5_results(path);
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
@@ -450,8 +985,9 @@ TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-node-ids.h5";
|
"fesa-duplicate-node-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{42, 42};
|
const std::array<std::int64_t, 2> ids{42, 42};
|
||||||
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
||||||
|
|
||||||
@@ -468,8 +1004,9 @@ TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-element-ids.h5";
|
"fesa-duplicate-element-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{9, 9};
|
const std::array<std::int64_t, 2> ids{9, 9};
|
||||||
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
||||||
|
|
||||||
@@ -486,8 +1023,9 @@ TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-section-ids.h5";
|
"fesa-duplicate-section-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{4, 4};
|
const std::array<std::int64_t, 2> ids{4, 4};
|
||||||
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
||||||
|
|
||||||
@@ -504,15 +1042,16 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonfinite-coordinates.h5";
|
"fesa-nonfinite-coordinates.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 6> coordinates{
|
const std::array<double, 6> coordinates{
|
||||||
std::numeric_limits<double>::quiet_NaN(),
|
std::numeric_limits<double>::quiet_NaN(),
|
||||||
-2.5,
|
-2.5,
|
||||||
3.75,
|
3.75,
|
||||||
4.0,
|
2.25,
|
||||||
5.5,
|
-2.5,
|
||||||
-6.25,
|
3.75,
|
||||||
};
|
};
|
||||||
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||||
|
|
||||||
@@ -524,13 +1063,128 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
|||||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedZeroLengthElement) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-zero-length-element.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 6> coordinates{
|
||||||
|
1.25, -2.5, 3.75, 1.25, -2.5, 3.75};
|
||||||
|
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedOrientationParallelToElement) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-parallel-orientation.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 6> orientations{
|
||||||
|
1.0, 0.0, 0.0, 0.0, 1.0, 0.0};
|
||||||
|
write_double_dataset(
|
||||||
|
path, "/model/sections/orientation", orientations);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsDuplicateSerializedNodeOrigins) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-duplicate-node-origins.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<std::int64_t, 2> labels{1001, 1001};
|
||||||
|
write_int64_dataset(path, "/model/nodes/local_label", labels);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsDuplicateSerializedBoundaryConditions) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-duplicate-boundary-conditions.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<std::int64_t, 2> node_ids{42, 42};
|
||||||
|
const std::array<std::uint8_t, 2> dofs{1, 1};
|
||||||
|
write_int64_dataset(
|
||||||
|
path,
|
||||||
|
"/analysis/steps/0/boundary_conditions/node_ids",
|
||||||
|
node_ids);
|
||||||
|
write_uint8_dataset(
|
||||||
|
path, "/analysis/steps/0/boundary_conditions/dofs", dofs);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedLocalFrameThatDisagreesWithModel) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-serialized-wrong-local-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 18> local_frames{
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
};
|
||||||
|
write_double_dataset(
|
||||||
|
path,
|
||||||
|
"/results/steps/0/frames/0/element/beam/local_frame",
|
||||||
|
local_frames);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.result_element_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonfinite-shear-area.h5";
|
"fesa-nonfinite-shear-area.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 2> shear_areas{
|
const std::array<double, 2> shear_areas{
|
||||||
std::numeric_limits<double>::infinity(),
|
std::numeric_limits<double>::infinity(),
|
||||||
0.05,
|
0.05,
|
||||||
@@ -550,8 +1204,9 @@ TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonpositive-shear-area.h5";
|
"fesa-nonpositive-shear-area.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
||||||
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
||||||
|
|
||||||
|
|||||||
@@ -702,7 +702,7 @@ TEST(Cload, ReportsInvalidDofAtTheDataRow) {
|
|||||||
TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
|
TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
|
||||||
const std::filesystem::path path =
|
const std::filesystem::path path =
|
||||||
std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
|
std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
|
||||||
"reference" / "cantilever beam" / "cantilever beam.inp";
|
"reference" / "cantilever beam" / "cantilever beam fesa.inp";
|
||||||
|
|
||||||
const auto result = parse_and_map(path);
|
const auto result = parse_and_map(path);
|
||||||
|
|
||||||
@@ -723,7 +723,7 @@ TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
|
|||||||
EXPECT_EQ(domain.step().nodal_loads.front().node, node->id);
|
EXPECT_EQ(domain.step().nodal_loads.front().node, node->id);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
domain.step().nodal_loads.front().values,
|
domain.step().nodal_loads.front().values,
|
||||||
(std::array<double, 6>{0.0, 0.0, -10000.0, 0.0, 0.0, 0.0}));
|
(std::array<double, 6>{0.0, 0.0, -1.0e6, 0.0, 0.0, 0.0}));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ std::string quote(const std::filesystem::path& path) {
|
|||||||
return '"' + path.string() + '"';
|
return '"' + path.string() + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string path_utf8(const std::filesystem::path& path) {
|
||||||
|
const std::u8string value = path.u8string();
|
||||||
|
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||||
|
}
|
||||||
|
|
||||||
std::string read_text(const std::filesystem::path& path) {
|
std::string read_text(const std::filesystem::path& path) {
|
||||||
std::ifstream input{path, std::ios::binary};
|
std::ifstream input{path, std::ios::binary};
|
||||||
return {
|
return {
|
||||||
@@ -93,8 +98,15 @@ TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
|
|||||||
fesa::read_hdf5_results(output.path());
|
fesa::read_hdf5_results(output.path());
|
||||||
ASSERT_TRUE(read.database.has_value());
|
ASSERT_TRUE(read.database.has_value());
|
||||||
ASSERT_TRUE(read.model.has_value());
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
ASSERT_TRUE(read.metadata.has_value());
|
||||||
EXPECT_TRUE(read.diagnostics.empty());
|
EXPECT_TRUE(read.diagnostics.empty());
|
||||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_source,
|
||||||
|
path_utf8(fixture_path("minimal_cantilever.inp")));
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_fingerprint,
|
||||||
|
"fnv1a64:73f31da4615f09b3");
|
||||||
|
|
||||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||||
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#include <fesa/assembly/assembler.hpp>
|
||||||
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
|
|
||||||
|
#include <bit>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::size_t benchmark_element_count = 1000;
|
||||||
|
|
||||||
|
fesa::Domain build_beam_chain() {
|
||||||
|
fesa::DomainBuilder builder;
|
||||||
|
for (std::size_t index = 0; index <= benchmark_element_count; ++index) {
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{static_cast<std::int64_t>(index)},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BenchmarkPart",
|
||||||
|
"Benchmark-1",
|
||||||
|
static_cast<std::int64_t>(index + 1),
|
||||||
|
},
|
||||||
|
fesa::Vec3{static_cast<double>(index), 0.0, 0.0},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
builder.add_material({
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
"Steel",
|
||||||
|
210.0e9,
|
||||||
|
0.3,
|
||||||
|
});
|
||||||
|
builder.add_section({
|
||||||
|
fesa::SectionId{0},
|
||||||
|
"General",
|
||||||
|
0.02,
|
||||||
|
3.0e-5,
|
||||||
|
4.0e-5,
|
||||||
|
2.0e-5,
|
||||||
|
0.015,
|
||||||
|
0.016,
|
||||||
|
fesa::ShearPropertySource::input,
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
{},
|
||||||
|
});
|
||||||
|
for (std::size_t index = 0; index < benchmark_element_count; ++index) {
|
||||||
|
builder.add_beam_element({
|
||||||
|
fesa::ElementId{static_cast<std::int64_t>(index)},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BenchmarkPart",
|
||||||
|
"Benchmark-1",
|
||||||
|
static_cast<std::int64_t>(index + 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::NodeId{static_cast<std::int64_t>(index)},
|
||||||
|
fesa::NodeId{static_cast<std::int64_t>(index + 1)},
|
||||||
|
},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
builder.set_step({"Benchmark", {}, {}});
|
||||||
|
|
||||||
|
auto result = std::move(builder).build();
|
||||||
|
if (!result.domain.has_value()) {
|
||||||
|
throw std::runtime_error{"Benchmark Domain failed validation."};
|
||||||
|
}
|
||||||
|
return std::move(*result.domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Operation>
|
||||||
|
std::pair<fesa::EquationSystem, double> measure(Operation&& operation) {
|
||||||
|
const auto start = std::chrono::steady_clock::now();
|
||||||
|
fesa::EquationSystem result = std::forward<Operation>(operation)();
|
||||||
|
const auto finish = std::chrono::steady_clock::now();
|
||||||
|
const double milliseconds =
|
||||||
|
std::chrono::duration<double, std::milli>(finish - start).count();
|
||||||
|
return {std::move(result), milliseconds};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint64_t> bits(const std::vector<double>& values) {
|
||||||
|
std::vector<std::uint64_t> result;
|
||||||
|
result.reserve(values.size());
|
||||||
|
for (const double value : values) {
|
||||||
|
result.push_back(std::bit_cast<std::uint64_t>(value));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool same_system(
|
||||||
|
const fesa::EquationSystem& left,
|
||||||
|
const fesa::EquationSystem& right) {
|
||||||
|
return left.stiffness.order == right.stiffness.order &&
|
||||||
|
left.stiffness.row_offsets == right.stiffness.row_offsets &&
|
||||||
|
left.stiffness.column_indices == right.stiffness.column_indices &&
|
||||||
|
bits(left.stiffness.values) == bits(right.stiffness.values) &&
|
||||||
|
bits(left.force) == bits(right.force);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t available_concurrency() {
|
||||||
|
const std::size_t available =
|
||||||
|
static_cast<std::size_t>(std::thread::hardware_concurrency());
|
||||||
|
return available == 0 ? 1 : available;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
try {
|
||||||
|
const fesa::Domain domain = build_beam_chain();
|
||||||
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
const std::size_t threads = available_concurrency();
|
||||||
|
|
||||||
|
auto [serial, serial_ms] = measure(
|
||||||
|
[&] { return fesa::assemble_serial(domain, dofs); });
|
||||||
|
auto [parallel, parallel_ms] = measure([&] {
|
||||||
|
return fesa::assemble_parallel(domain, dofs, {threads, 1});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!same_system(serial, parallel)) {
|
||||||
|
std::cerr << "Serial and parallel assembly results differ.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "elements=" << domain.beam_elements().size()
|
||||||
|
<< " serial_ms=" << serial_ms
|
||||||
|
<< " parallel_ms=" << parallel_ms
|
||||||
|
<< " parallel_threads=" << threads << '\n';
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::cerr << "Assembly benchmark failed: " << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
add_executable(fesa_cantilever_reference_tests
|
||||||
|
cantilever_reference_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(
|
||||||
|
fesa_cantilever_reference_tests PRIVATE cxx_std_20
|
||||||
|
)
|
||||||
|
target_compile_options(
|
||||||
|
fesa_cantilever_reference_tests PRIVATE /W4 /permissive- /EHsc
|
||||||
|
)
|
||||||
|
target_compile_definitions(
|
||||||
|
fesa_cantilever_reference_tests
|
||||||
|
PRIVATE
|
||||||
|
FESA_REFERENCE_COMPARE_PATH="$<TARGET_FILE:fesa-reference-compare>"
|
||||||
|
FESA_REPOSITORY_ROOT="${CMAKE_SOURCE_DIR}"
|
||||||
|
FESA_TEST_BINARY_DIR="${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(
|
||||||
|
fesa_cantilever_reference_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
add_dependencies(
|
||||||
|
fesa_cantilever_reference_tests
|
||||||
|
fesa-reference-compare
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CantileverReference
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_cantilever_reference_tests>"
|
||||||
|
)
|
||||||
|
|
||||||
|
set_property(
|
||||||
|
TEST CantileverReference
|
||||||
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
|
)
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
#include <fesa/analysis/run_solver.hpp>
|
||||||
|
#include <fesa/io/hdf5/writer.hpp>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr double kEquilibriumRelativeTolerance = 2.0e-13;
|
||||||
|
constexpr double kEquilibriumAbsoluteTolerance = 1.0e-12;
|
||||||
|
constexpr std::string_view kInstanceName = "PART-1_1-1";
|
||||||
|
|
||||||
|
class TemporaryPath final {
|
||||||
|
public:
|
||||||
|
explicit TemporaryPath(std::filesystem::path path)
|
||||||
|
: path_{std::move(path)} {
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::create_directories(path_.parent_path(), error);
|
||||||
|
if (error) {
|
||||||
|
throw std::runtime_error{
|
||||||
|
"Failed to create reference test directory."};
|
||||||
|
}
|
||||||
|
std::filesystem::remove(path_, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
~TemporaryPath() {
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::remove(path_, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TemporaryPath(const TemporaryPath&) = delete;
|
||||||
|
TemporaryPath& operator=(const TemporaryPath&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||||
|
return path_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path reference_path(const std::string_view name) {
|
||||||
|
return std::filesystem::path{FESA_REPOSITORY_ROOT} / "reference" /
|
||||||
|
"cantilever beam" / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path test_output_path(const std::string_view name) {
|
||||||
|
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "testing" / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string quote(const std::filesystem::path& path) {
|
||||||
|
return '"' + path.string() + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string read_text(const std::filesystem::path& path) {
|
||||||
|
std::ifstream input{path, std::ios::binary};
|
||||||
|
return {
|
||||||
|
std::istreambuf_iterator<char>{input},
|
||||||
|
std::istreambuf_iterator<char>{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::Vec3& coordinates_for(
|
||||||
|
const fesa::Hdf5ModelSnapshot& model,
|
||||||
|
const fesa::NodeId node_id) {
|
||||||
|
const auto found = std::ranges::find_if(
|
||||||
|
model.nodes,
|
||||||
|
[node_id](const fesa::Hdf5NodeSnapshot& node) {
|
||||||
|
return node.id == node_id;
|
||||||
|
});
|
||||||
|
if (found == model.nodes.end()) {
|
||||||
|
throw std::runtime_error{
|
||||||
|
"Result or load references an unknown node ID."};
|
||||||
|
}
|
||||||
|
return found->coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_nodal_resultant(
|
||||||
|
std::array<double, 3>& force,
|
||||||
|
std::array<double, 3>& moment,
|
||||||
|
const fesa::Vec3& position,
|
||||||
|
const std::array<double, 6>& values) {
|
||||||
|
force[0] += values[0];
|
||||||
|
force[1] += values[1];
|
||||||
|
force[2] += values[2];
|
||||||
|
moment[0] += values[3] + position.y * values[2] -
|
||||||
|
position.z * values[1];
|
||||||
|
moment[1] += values[4] + position.z * values[0] -
|
||||||
|
position.x * values[2];
|
||||||
|
moment[2] += values[5] + position.x * values[1] -
|
||||||
|
position.y * values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void expect_finite(const fesa::ResultFrame& frame) {
|
||||||
|
for (const auto& displacement : frame.nodal.displacement) {
|
||||||
|
for (const double value : displacement) {
|
||||||
|
EXPECT_TRUE(std::isfinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& reaction : frame.nodal.reaction) {
|
||||||
|
for (const double value : reaction) {
|
||||||
|
EXPECT_TRUE(std::isfinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fesa::BeamElementFrame& beam : frame.element.beams) {
|
||||||
|
EXPECT_TRUE(fesa::is_finite(beam.local_frame.ex));
|
||||||
|
EXPECT_TRUE(fesa::is_finite(beam.local_frame.ey));
|
||||||
|
EXPECT_TRUE(fesa::is_finite(beam.local_frame.ez));
|
||||||
|
for (const fesa::BeamSectionResult& end : beam.end_results) {
|
||||||
|
EXPECT_TRUE(std::isfinite(end.xi));
|
||||||
|
EXPECT_TRUE(std::isfinite(end.centroid_sigma_xx));
|
||||||
|
for (const double value : end.section_strain) {
|
||||||
|
EXPECT_TRUE(std::isfinite(value));
|
||||||
|
}
|
||||||
|
for (const double value : end.section_force) {
|
||||||
|
EXPECT_TRUE(std::isfinite(value));
|
||||||
|
}
|
||||||
|
for (const double value : end.sigma_xx) {
|
||||||
|
EXPECT_TRUE(std::isfinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CantileverReference, CorrelatesAllAvailableAbaqusResults) {
|
||||||
|
const TemporaryPath results{
|
||||||
|
test_output_path("cantilever-reference.h5")};
|
||||||
|
const TemporaryPath standard_output{
|
||||||
|
test_output_path("cantilever-reference-compare.stdout.txt")};
|
||||||
|
const TemporaryPath error_output{
|
||||||
|
test_output_path("cantilever-reference-compare.stderr.txt")};
|
||||||
|
|
||||||
|
const fesa::AnalysisRunResult run = fesa::run_solver({
|
||||||
|
reference_path("cantilever beam fesa.inp"),
|
||||||
|
results.path(),
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(run.succeeded);
|
||||||
|
ASSERT_TRUE(run.diagnostics.empty());
|
||||||
|
|
||||||
|
const fesa::Hdf5ReadResult read =
|
||||||
|
fesa::read_hdf5_results(results.path());
|
||||||
|
ASSERT_TRUE(read.diagnostics.empty());
|
||||||
|
ASSERT_TRUE(read.database.has_value());
|
||||||
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
ASSERT_TRUE(read.analysis.has_value());
|
||||||
|
ASSERT_EQ(read.database->steps.size(), 1U);
|
||||||
|
ASSERT_EQ(read.database->steps[0].frames.size(), 1U);
|
||||||
|
|
||||||
|
const fesa::ResultFrame& frame =
|
||||||
|
read.database->steps[0].frames[0];
|
||||||
|
expect_finite(frame);
|
||||||
|
|
||||||
|
std::array<double, 3> total_force{};
|
||||||
|
std::array<double, 3> total_moment{};
|
||||||
|
std::array<double, 3> applied_force{};
|
||||||
|
std::array<double, 3> applied_moment{};
|
||||||
|
ASSERT_EQ(frame.nodal.node_ids.size(), frame.nodal.reaction.size());
|
||||||
|
for (std::size_t index = 0; index < frame.nodal.node_ids.size(); ++index) {
|
||||||
|
add_nodal_resultant(
|
||||||
|
total_force,
|
||||||
|
total_moment,
|
||||||
|
coordinates_for(*read.model, frame.nodal.node_ids[index]),
|
||||||
|
frame.nodal.reaction[index]);
|
||||||
|
}
|
||||||
|
for (const fesa::NodalLoad& load : read.analysis->step.nodal_loads) {
|
||||||
|
const fesa::Vec3& position =
|
||||||
|
coordinates_for(*read.model, load.node);
|
||||||
|
add_nodal_resultant(
|
||||||
|
total_force,
|
||||||
|
total_moment,
|
||||||
|
position,
|
||||||
|
load.values);
|
||||||
|
add_nodal_resultant(
|
||||||
|
applied_force,
|
||||||
|
applied_moment,
|
||||||
|
position,
|
||||||
|
load.values);
|
||||||
|
}
|
||||||
|
for (std::size_t component = 0; component < total_force.size(); ++component) {
|
||||||
|
const double tolerance = std::max(
|
||||||
|
kEquilibriumAbsoluteTolerance,
|
||||||
|
kEquilibriumRelativeTolerance *
|
||||||
|
std::abs(applied_force[component]));
|
||||||
|
EXPECT_NEAR(total_force[component], 0.0, tolerance);
|
||||||
|
}
|
||||||
|
for (std::size_t component = 0; component < total_moment.size(); ++component) {
|
||||||
|
const double tolerance = std::max(
|
||||||
|
kEquilibriumAbsoluteTolerance,
|
||||||
|
kEquilibriumRelativeTolerance *
|
||||||
|
std::abs(applied_moment[component]));
|
||||||
|
EXPECT_NEAR(total_moment[component], 0.0, tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string command =
|
||||||
|
'"' + quote(std::filesystem::path{FESA_REFERENCE_COMPARE_PATH}) +
|
||||||
|
" --results " + quote(results.path()) +
|
||||||
|
" --instance " + std::string{kInstanceName} +
|
||||||
|
" --displacements " +
|
||||||
|
quote(reference_path("cantilever beam displacements.csv")) +
|
||||||
|
" --reactions " +
|
||||||
|
quote(reference_path("cantilever beam reactions.csv")) +
|
||||||
|
" --internal-forces " +
|
||||||
|
quote(reference_path("cantilever beam elemental forces.csv")) +
|
||||||
|
std::string{" --displacement-absolute-scale 1e-10"} +
|
||||||
|
" --reaction-absolute-scale 1e-8" +
|
||||||
|
" --internal-force-absolute-scale 1e-8" +
|
||||||
|
" 1>" + quote(standard_output.path()) +
|
||||||
|
" 2>" + quote(error_output.path()) + '"';
|
||||||
|
const int exit_code = std::system(command.c_str());
|
||||||
|
|
||||||
|
const std::string standard_text = read_text(standard_output.path());
|
||||||
|
const std::string error_text = read_text(error_output.path());
|
||||||
|
|
||||||
|
EXPECT_EQ(exit_code, 0)
|
||||||
|
<< standard_text << error_text;
|
||||||
|
EXPECT_NE(
|
||||||
|
standard_text.find("quantity=displacement"),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
standard_text.find("quantity=reaction"),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
standard_text.find("quantity=internal_force"),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(standard_text.find("rmse="), std::string::npos);
|
||||||
|
EXPECT_NE(standard_text.find("relative_l2="), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -59,7 +59,7 @@ fesa::Domain build_axial_domain(const bool all_constrained) {
|
|||||||
2.0,
|
2.0,
|
||||||
fesa::ShearPropertySource::input,
|
fesa::ShearPropertySource::input,
|
||||||
fesa::Vec3{0.0, 1.0, 0.0},
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
{},
|
{{1.0, 2.0}, {-1.0, -2.0}},
|
||||||
});
|
});
|
||||||
builder.add_beam_element({
|
builder.add_beam_element({
|
||||||
fesa::ElementId{0},
|
fesa::ElementId{0},
|
||||||
@@ -149,7 +149,7 @@ TEST(LinearStaticAnalysis, SolvesHandCalculatedAxialBeamInNodeIdOrder) {
|
|||||||
ASSERT_TRUE(run.results.has_value());
|
ASSERT_TRUE(run.results.has_value());
|
||||||
EXPECT_TRUE(run.diagnostics.empty());
|
EXPECT_TRUE(run.diagnostics.empty());
|
||||||
EXPECT_TRUE(fesa::validate_result_database(*run.results).succeeded);
|
EXPECT_TRUE(fesa::validate_result_database(*run.results).succeeded);
|
||||||
EXPECT_EQ(run.results->schema_version, "1.0.0");
|
EXPECT_EQ(run.results->schema_version, "2.0.0");
|
||||||
ASSERT_EQ(run.results->steps.size(), 1);
|
ASSERT_EQ(run.results->steps.size(), 1);
|
||||||
EXPECT_EQ(run.results->steps[0].name, "Load");
|
EXPECT_EQ(run.results->steps[0].name, "Load");
|
||||||
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
|
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
|
||||||
@@ -214,4 +214,49 @@ TEST(LinearStaticAnalysis, SolvesAllConstrainedSystemWithoutPardiso) {
|
|||||||
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
|
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, PreservesOriginsConnectivityAndBeamRecovery) {
|
||||||
|
const fesa::Domain domain = build_axial_domain(false);
|
||||||
|
|
||||||
|
const fesa::AnalysisRunResult run =
|
||||||
|
fesa::LinearStaticAnalysis{}.run(domain);
|
||||||
|
|
||||||
|
ASSERT_TRUE(run.succeeded);
|
||||||
|
ASSERT_TRUE(run.results.has_value());
|
||||||
|
const fesa::ResultFrame& frame = run.results->steps[0].frames[0];
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.nodal.origins,
|
||||||
|
(std::vector<fesa::EntityOrigin>{
|
||||||
|
{"BeamPart", "Beam-1", 2},
|
||||||
|
{"BeamPart", "Beam-1", 1},
|
||||||
|
}));
|
||||||
|
|
||||||
|
ASSERT_EQ(frame.element.beams.size(), 1U);
|
||||||
|
const fesa::BeamElementFrame& beam = frame.element.beams[0];
|
||||||
|
EXPECT_EQ(beam.element, fesa::ElementId{0});
|
||||||
|
EXPECT_EQ(
|
||||||
|
beam.origin,
|
||||||
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1}));
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.x, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.y, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.z, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.x, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.y, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.z, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.x, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.y, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.z, 1.0);
|
||||||
|
EXPECT_EQ(beam.end_results[0].end_node, fesa::NodeId{20});
|
||||||
|
EXPECT_EQ(beam.end_results[1].end_node, fesa::NodeId{4});
|
||||||
|
EXPECT_DOUBLE_EQ(beam.end_results[0].xi, -1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.end_results[1].xi, 1.0);
|
||||||
|
for (const fesa::BeamSectionResult& end : beam.end_results) {
|
||||||
|
EXPECT_NEAR(end.section_strain[0], 0.05, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.section_force[0], 10.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.centroid_sigma_xx, 5.0, 1.0e-12);
|
||||||
|
ASSERT_EQ(end.sigma_xx.size(), 2U);
|
||||||
|
EXPECT_NEAR(end.sigma_xx[0], 5.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.sigma_xx[1], 5.0, 1.0e-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
#include <fesa/assembly/assembler.hpp>
|
||||||
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <bit>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::Domain finish_domain(
|
||||||
|
fesa::DomainBuilder builder,
|
||||||
|
fesa::StepDefinition step) {
|
||||||
|
builder.set_step(std::move(step));
|
||||||
|
auto result = std::move(builder).build();
|
||||||
|
if (!result.domain.has_value()) {
|
||||||
|
throw std::runtime_error{"Test Domain failed validation."};
|
||||||
|
}
|
||||||
|
return std::move(*result.domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Domain build_beam_domain(const bool branched) {
|
||||||
|
fesa::DomainBuilder builder;
|
||||||
|
const std::array<fesa::Vec3, 6> positions{{
|
||||||
|
{0.0, 0.0, 0.0},
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{2.0, 0.0, 0.0},
|
||||||
|
{3.0, 0.0, 0.0},
|
||||||
|
{1.0, 0.0, 1.0},
|
||||||
|
{1.0, 1.0, 1.0},
|
||||||
|
}};
|
||||||
|
const std::size_t node_count = branched ? positions.size() : 4;
|
||||||
|
for (std::size_t index = 0; index < node_count; ++index) {
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{static_cast<std::int64_t>(index)},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart",
|
||||||
|
"Beam-1",
|
||||||
|
static_cast<std::int64_t>(index + 1),
|
||||||
|
},
|
||||||
|
positions[index],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
builder.add_material({
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
"Steel",
|
||||||
|
210.0e9,
|
||||||
|
0.3,
|
||||||
|
});
|
||||||
|
builder.add_section({
|
||||||
|
fesa::SectionId{0},
|
||||||
|
"General",
|
||||||
|
0.02,
|
||||||
|
3.0e-5,
|
||||||
|
4.0e-5,
|
||||||
|
2.0e-5,
|
||||||
|
0.015,
|
||||||
|
0.016,
|
||||||
|
fesa::ShearPropertySource::input,
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
{},
|
||||||
|
});
|
||||||
|
|
||||||
|
const std::array<fesa::BeamElement, 5> elements{{
|
||||||
|
{
|
||||||
|
fesa::ElementId{40},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 300},
|
||||||
|
{fesa::NodeId{2}, fesa::NodeId{3}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::ElementId{10},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 100},
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::ElementId{30},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 200},
|
||||||
|
{fesa::NodeId{1}, fesa::NodeId{2}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::ElementId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 500},
|
||||||
|
{fesa::NodeId{1}, fesa::NodeId{5}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::ElementId{20},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 400},
|
||||||
|
{fesa::NodeId{1}, fesa::NodeId{4}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
},
|
||||||
|
}};
|
||||||
|
const std::size_t element_count = branched ? elements.size() : 3;
|
||||||
|
for (std::size_t index = 0; index < element_count; ++index) {
|
||||||
|
builder.add_beam_element(elements[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return finish_domain(
|
||||||
|
std::move(builder),
|
||||||
|
{
|
||||||
|
"Load",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
{fesa::NodeId{3}, {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}},
|
||||||
|
{fesa::NodeId{1}, {1.0e16, 1.0, 0.0, 0.0, 0.0, 0.0}},
|
||||||
|
{fesa::NodeId{1}, {-1.0e16, 2.0, 0.0, 0.0, 0.0, 0.0}},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Domain build_multiple_kernel_failure_domain() {
|
||||||
|
fesa::DomainBuilder builder;
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
|
||||||
|
fesa::Vec3{0.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
|
||||||
|
fesa::Vec3{1.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
|
||||||
|
auto material = fesa::IsotropicElastic{
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
"Invalid",
|
||||||
|
std::numeric_limits<double>::max(),
|
||||||
|
0.3,
|
||||||
|
};
|
||||||
|
builder.add_material(std::move(material));
|
||||||
|
auto section = fesa::BeamSection{
|
||||||
|
fesa::SectionId{0},
|
||||||
|
"Invalid",
|
||||||
|
std::numeric_limits<double>::max(),
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
fesa::ShearPropertySource::input,
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
builder.add_section(std::move(section));
|
||||||
|
|
||||||
|
builder.add_beam_element({
|
||||||
|
fesa::ElementId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 20},
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
});
|
||||||
|
builder.add_beam_element({
|
||||||
|
fesa::ElementId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 10},
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
});
|
||||||
|
|
||||||
|
return finish_domain(std::move(builder), {"Load", {}, {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint64_t> bits(const std::vector<double>& values) {
|
||||||
|
std::vector<std::uint64_t> result;
|
||||||
|
result.reserve(values.size());
|
||||||
|
for (const double value : values) {
|
||||||
|
result.push_back(std::bit_cast<std::uint64_t>(value));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void expect_bitwise_equal(
|
||||||
|
const fesa::EquationSystem& expected,
|
||||||
|
const fesa::EquationSystem& actual) {
|
||||||
|
EXPECT_EQ(actual.stiffness.order, expected.stiffness.order);
|
||||||
|
EXPECT_EQ(
|
||||||
|
actual.stiffness.row_offsets,
|
||||||
|
expected.stiffness.row_offsets);
|
||||||
|
EXPECT_EQ(
|
||||||
|
actual.stiffness.column_indices,
|
||||||
|
expected.stiffness.column_indices);
|
||||||
|
EXPECT_EQ(bits(actual.stiffness.values), bits(expected.stiffness.values));
|
||||||
|
EXPECT_EQ(bits(actual.force), bits(expected.force));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ParallelAssembly, MatchesSerialForFixedBeamChain) {
|
||||||
|
const fesa::Domain domain = build_beam_domain(false);
|
||||||
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
const fesa::EquationSystem serial = fesa::assemble_serial(domain, dofs);
|
||||||
|
|
||||||
|
expect_bitwise_equal(
|
||||||
|
serial,
|
||||||
|
fesa::assemble_parallel(domain, dofs, {1, 1}));
|
||||||
|
expect_bitwise_equal(
|
||||||
|
serial,
|
||||||
|
fesa::assemble_parallel(domain, dofs, {4, 2}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ParallelAssembly, MatchesSerialForFixedBranchedDomain) {
|
||||||
|
const fesa::Domain domain = build_beam_domain(true);
|
||||||
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
const fesa::EquationSystem serial = fesa::assemble_serial(domain, dofs);
|
||||||
|
|
||||||
|
expect_bitwise_equal(
|
||||||
|
serial,
|
||||||
|
fesa::assemble_parallel(domain, dofs, {1, 2}));
|
||||||
|
expect_bitwise_equal(
|
||||||
|
serial,
|
||||||
|
fesa::assemble_parallel(domain, dofs, {3, 1}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TbbElementEvaluation, RejectsZeroExecutionLimits) {
|
||||||
|
const fesa::Domain domain = build_beam_domain(false);
|
||||||
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
|
||||||
|
EXPECT_THROW(
|
||||||
|
static_cast<void>(
|
||||||
|
fesa::assemble_parallel(domain, dofs, {0, 1})),
|
||||||
|
std::invalid_argument);
|
||||||
|
EXPECT_THROW(
|
||||||
|
static_cast<void>(
|
||||||
|
fesa::assemble_parallel(domain, dofs, {1, 0})),
|
||||||
|
std::invalid_argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TbbElementEvaluation, ReportsFirstFailureByCanonicalElementOrigin) {
|
||||||
|
const fesa::Domain domain = build_multiple_kernel_failure_domain();
|
||||||
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
|
||||||
|
for (const std::size_t threads : {std::size_t{1}, std::size_t{4}}) {
|
||||||
|
SCOPED_TRACE(::testing::Message{} << "threads=" << threads);
|
||||||
|
try {
|
||||||
|
static_cast<void>(
|
||||||
|
fesa::assemble_parallel(domain, dofs, {threads, 1}));
|
||||||
|
FAIL() << "Expected a Beam kernel failure.";
|
||||||
|
} catch (const std::runtime_error& error) {
|
||||||
|
const std::string message{error.what()};
|
||||||
|
EXPECT_NE(
|
||||||
|
message.find("Beam element 10 kernel failed"),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_EQ(
|
||||||
|
message.find("Beam element 20 kernel failed"),
|
||||||
|
std::string::npos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <fesa/assembly/contribution.hpp>
|
||||||
#include <fesa/assembly/serial_assembler.hpp>
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -187,8 +188,9 @@ fesa::Domain build_rounding_domain(const bool large_element_first) {
|
|||||||
storage_order = {2, 0, 1};
|
storage_order = {2, 0, 1};
|
||||||
}
|
}
|
||||||
for (const std::size_t index : storage_order) {
|
for (const std::size_t index : storage_order) {
|
||||||
|
constexpr std::array<std::int64_t, 3> element_ids{1, 2, 0};
|
||||||
builder.add_beam_element({
|
builder.add_beam_element({
|
||||||
fesa::ElementId{static_cast<std::int64_t>(index)},
|
fesa::ElementId{element_ids[index]},
|
||||||
fesa::EntityOrigin{
|
fesa::EntityOrigin{
|
||||||
"BeamPart",
|
"BeamPart",
|
||||||
"Beam-1",
|
"Beam-1",
|
||||||
@@ -250,6 +252,80 @@ double csr_value(
|
|||||||
std::distance(matrix.column_indices.begin(), found))];
|
std::distance(matrix.column_indices.begin(), found))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint64_t> value_bits(
|
||||||
|
const fesa::SymmetricCsr& matrix) {
|
||||||
|
std::vector<std::uint64_t> bits;
|
||||||
|
bits.reserve(matrix.values.size());
|
||||||
|
for (const double value : matrix.values) {
|
||||||
|
bits.push_back(std::bit_cast<std::uint64_t>(value));
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CanonicalContribution, OrdersByCoordinateElementAndLocalOrder) {
|
||||||
|
const std::vector<fesa::MatrixContribution> contributions{
|
||||||
|
{1, 2, fesa::ElementId{9}, 4, 9.0},
|
||||||
|
{0, 2, fesa::ElementId{3}, 8, 3.8},
|
||||||
|
{0, 2, fesa::ElementId{3}, 2, 3.2},
|
||||||
|
{0, 2, fesa::ElementId{1}, 7, 1.7},
|
||||||
|
{0, 0, fesa::ElementId{5}, 0, 5.0},
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<fesa::MatrixContribution> canonical =
|
||||||
|
fesa::canonicalize_contributions(contributions);
|
||||||
|
|
||||||
|
ASSERT_EQ(canonical.size(), contributions.size());
|
||||||
|
EXPECT_EQ(canonical[0].row, 0);
|
||||||
|
EXPECT_EQ(canonical[0].column, 0);
|
||||||
|
EXPECT_EQ(canonical[0].element, fesa::ElementId{5});
|
||||||
|
EXPECT_EQ(canonical[1].element, fesa::ElementId{1});
|
||||||
|
EXPECT_EQ(canonical[2].element, fesa::ElementId{3});
|
||||||
|
EXPECT_EQ(canonical[2].local_order, 2);
|
||||||
|
EXPECT_EQ(canonical[3].element, fesa::ElementId{3});
|
||||||
|
EXPECT_EQ(canonical[3].local_order, 8);
|
||||||
|
EXPECT_EQ(canonical[4].row, 1);
|
||||||
|
EXPECT_EQ(canonical[4].column, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DeterministicMerge, IsBitwiseStableAcrossInputPermutations) {
|
||||||
|
const std::vector<fesa::MatrixContribution> contributions{
|
||||||
|
{0, 1, fesa::ElementId{2}, 0, 1.0},
|
||||||
|
{0, 1, fesa::ElementId{0}, 0, 1.0e16},
|
||||||
|
{0, 1, fesa::ElementId{1}, 0, -1.0e16},
|
||||||
|
{1, 1, fesa::ElementId{1}, 1, -0.0},
|
||||||
|
{1, 1, fesa::ElementId{0}, 1, +0.0},
|
||||||
|
};
|
||||||
|
const std::vector<fesa::MatrixContribution> canonical =
|
||||||
|
fesa::canonicalize_contributions(contributions);
|
||||||
|
const fesa::SymmetricCsr expected =
|
||||||
|
fesa::merge_contributions(2, canonical);
|
||||||
|
|
||||||
|
std::array<std::size_t, 5> permutation{0, 1, 2, 3, 4};
|
||||||
|
do {
|
||||||
|
std::vector<fesa::MatrixContribution> shuffled;
|
||||||
|
shuffled.reserve(contributions.size());
|
||||||
|
for (const std::size_t index : permutation) {
|
||||||
|
shuffled.push_back(contributions[index]);
|
||||||
|
}
|
||||||
|
const std::vector<fesa::MatrixContribution> shuffled_canonical =
|
||||||
|
fesa::canonicalize_contributions(shuffled);
|
||||||
|
const fesa::SymmetricCsr actual =
|
||||||
|
fesa::merge_contributions(2, shuffled_canonical);
|
||||||
|
|
||||||
|
EXPECT_EQ(actual.order, expected.order);
|
||||||
|
EXPECT_EQ(actual.row_offsets, expected.row_offsets);
|
||||||
|
EXPECT_EQ(actual.column_indices, expected.column_indices);
|
||||||
|
EXPECT_EQ(value_bits(actual), value_bits(expected));
|
||||||
|
} while (std::ranges::next_permutation(permutation).found);
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::bit_cast<std::uint64_t>(csr_value(expected, 0, 1)),
|
||||||
|
std::bit_cast<std::uint64_t>(1.0));
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::bit_cast<std::uint64_t>(csr_value(expected, 1, 1)),
|
||||||
|
std::bit_cast<std::uint64_t>(+0.0));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(SparsePattern, BuildsExpectedTwoElementChainStructure) {
|
TEST(SparsePattern, BuildsExpectedTwoElementChainStructure) {
|
||||||
const fesa::Domain domain = build_chain_domain(false);
|
const fesa::Domain domain = build_chain_domain(false);
|
||||||
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
const fesa::DofManager dofs = fesa::DofManager::build(domain);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ constexpr double kShearAreaZ = 0.2;
|
|||||||
fesa::Beam3D2Input make_x_axis_input(const double length) {
|
fesa::Beam3D2Input make_x_axis_input(const double length) {
|
||||||
return {
|
return {
|
||||||
{{{0.0, 0.0, 0.0}, {length, 0.0, 0.0}}},
|
{{{0.0, 0.0, 0.0}, {length, 0.0, 0.0}}},
|
||||||
|
{fesa::NodeId{1}, fesa::NodeId{2}},
|
||||||
{fesa::MaterialId{1}, "elastic", kYoung, kPoisson},
|
{fesa::MaterialId{1}, "elastic", kYoung, kPoisson},
|
||||||
{
|
{
|
||||||
fesa::SectionId{1},
|
fesa::SectionId{1},
|
||||||
@@ -369,4 +370,191 @@ TEST(Beam3D2, PreservesGlobalEnergyUnderRigidCoordinateRotation) {
|
|||||||
4096.0 * std::numeric_limits<double>::epsilon());
|
4096.0 * std::numeric_limits<double>::epsilon());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, RecoversPureAxialStrainAndForceAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[6] = 0.4;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
EXPECT_DOUBLE_EQ(results[0].xi, -1.0);
|
||||||
|
EXPECT_EQ(results[0].end_node, fesa::NodeId{101});
|
||||||
|
EXPECT_DOUBLE_EQ(results[1].xi, 1.0);
|
||||||
|
EXPECT_EQ(results[1].end_node, fesa::NodeId{202});
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[0], 0.2);
|
||||||
|
expect_relative_near(result.section_force[0], 16.8);
|
||||||
|
expect_relative_near(result.centroid_sigma_xx, 42.0);
|
||||||
|
for (std::size_t component = 1; component < 6; ++component) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(result.sigma_xx.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, RecoversPureTorsionAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[9] = 0.6;
|
||||||
|
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[3], 0.3);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[3],
|
||||||
|
shear_modulus * kTorsionJ * 0.3);
|
||||||
|
for (const std::size_t component :
|
||||||
|
std::array<std::size_t, 5>{0, 1, 2, 4, 5}) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, UsesReducedIntegrationPointForShearAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[7] = 1.2;
|
||||||
|
displacement[5] = 0.2;
|
||||||
|
displacement[11] = 0.6;
|
||||||
|
displacement[8] = -0.4;
|
||||||
|
displacement[4] = 0.1;
|
||||||
|
displacement[10] = 0.5;
|
||||||
|
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[1], 0.2);
|
||||||
|
expect_relative_near(result.section_strain[2], 0.1);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[1],
|
||||||
|
shear_modulus * kShearAreaY * 0.2);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[2],
|
||||||
|
shear_modulus * kShearAreaZ * 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BeamRecovery, UsesBeamFrameForGlobalDisplacements) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.coordinates = {{{0.0, 0.0, 0.0}, {0.0, 2.0, 0.0}}};
|
||||||
|
input.section.orientation = {0.0, 0.0, 1.0};
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[7] = 0.4;
|
||||||
|
displacement[10] = 0.6;
|
||||||
|
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[0], 0.2);
|
||||||
|
expect_relative_near(result.section_force[0], 16.8);
|
||||||
|
expect_relative_near(result.section_strain[3], 0.3);
|
||||||
|
expect_relative_near(
|
||||||
|
result.section_force[3],
|
||||||
|
shear_modulus * kTorsionJ * 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SectionForce, RecoversPureBendingAboutLocalYAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[4] = -0.4;
|
||||||
|
displacement[10] = 0.4;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[4], 0.4);
|
||||||
|
expect_relative_near(result.section_force[4], 2.52);
|
||||||
|
for (const std::size_t component :
|
||||||
|
std::array<std::size_t, 5>{0, 1, 2, 3, 5}) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SectionForce, RecoversPureBendingAboutLocalZAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[5] = 0.25;
|
||||||
|
displacement[11] = -0.25;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_strain[5], -0.25);
|
||||||
|
expect_relative_near(result.section_force[5], -2.625);
|
||||||
|
for (const std::size_t component :
|
||||||
|
std::array<std::size_t, 5>{0, 1, 2, 3, 4}) {
|
||||||
|
expect_relative_near(result.section_strain[component], 0.0);
|
||||||
|
expect_relative_near(result.section_force[component], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SectionForce, PreservesBiaxialBendingSignsAtBothEnds) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[4] = -0.4;
|
||||||
|
displacement[10] = 0.4;
|
||||||
|
displacement[5] = 0.25;
|
||||||
|
displacement[11] = -0.25;
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.section_force[4], 2.52);
|
||||||
|
expect_relative_near(result.section_force[5], -2.625);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CentroidStress, UsesAxialStressAndPreservesRecoveryPointOrder) {
|
||||||
|
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||||
|
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||||
|
std::array<double, 12> displacement{};
|
||||||
|
displacement[6] = 0.2;
|
||||||
|
displacement[4] = -0.2;
|
||||||
|
displacement[10] = 0.2;
|
||||||
|
displacement[5] = 0.3;
|
||||||
|
displacement[11] = -0.3;
|
||||||
|
const std::array<std::array<double, 2>, 3> recovery_points{{
|
||||||
|
{2.0, 3.0},
|
||||||
|
{-1.0, 4.0},
|
||||||
|
{5.0, -2.0},
|
||||||
|
}};
|
||||||
|
|
||||||
|
const auto results = fesa::recover_beam3d2(
|
||||||
|
input,
|
||||||
|
displacement,
|
||||||
|
recovery_points);
|
||||||
|
|
||||||
|
ASSERT_EQ(results.size(), 2U);
|
||||||
|
for (const auto& result : results) {
|
||||||
|
expect_relative_near(result.centroid_sigma_xx, 21.0);
|
||||||
|
ASSERT_EQ(result.sigma_xx.size(), recovery_points.size());
|
||||||
|
expect_relative_near(result.sigma_xx[0], 273.0);
|
||||||
|
expect_relative_near(result.sigma_xx[1], 126.0);
|
||||||
|
expect_relative_near(result.sigma_xx[2], 252.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
|||||||
|
|
||||||
ASSERT_TRUE(result.deck.has_value());
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
EXPECT_TRUE(result.diagnostics.empty());
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.input_fingerprint,
|
||||||
|
"fnv1a64:73f31da4615f09b3");
|
||||||
EXPECT_TRUE(result.deck->parts.empty());
|
EXPECT_TRUE(result.deck->parts.empty());
|
||||||
EXPECT_FALSE(result.deck->assembly.has_value());
|
EXPECT_FALSE(result.deck->assembly.has_value());
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ std::array<double, 6> zeros() {
|
|||||||
fesa::ResultDatabase valid_database() {
|
fesa::ResultDatabase valid_database() {
|
||||||
fesa::NodalFrame nodal{
|
fesa::NodalFrame nodal{
|
||||||
{fesa::NodeId{0}, fesa::NodeId{1}},
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
{
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 10},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 20},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
zeros(),
|
zeros(),
|
||||||
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
|
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
|
||||||
@@ -26,9 +30,41 @@ fesa::ResultDatabase valid_database() {
|
|||||||
zeros(),
|
zeros(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
fesa::ResultFrame frame{1.0, std::move(nodal), {}};
|
fesa::BeamElementFrame beam{
|
||||||
|
fesa::ElementId{3},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 30},
|
||||||
|
{
|
||||||
|
fesa::Vec3{1.0, 0.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 0.0, 1.0},
|
||||||
|
},
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
-1.0,
|
||||||
|
fesa::NodeId{0},
|
||||||
|
zeros(),
|
||||||
|
zeros(),
|
||||||
|
0.0,
|
||||||
|
{1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
1.0,
|
||||||
|
fesa::NodeId{1},
|
||||||
|
zeros(),
|
||||||
|
zeros(),
|
||||||
|
0.0,
|
||||||
|
{2.0},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
fesa::ResultFrame frame{
|
||||||
|
1.0,
|
||||||
|
std::move(nodal),
|
||||||
|
{{std::move(beam)}},
|
||||||
|
{},
|
||||||
|
};
|
||||||
fesa::ResultStep step{"Load", {std::move(frame)}};
|
fesa::ResultStep step{"Load", {std::move(frame)}};
|
||||||
return {"1.0.0", {std::move(step)}};
|
return {"2.0.0", {std::move(step)}};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_diagnostic(
|
bool has_diagnostic(
|
||||||
@@ -72,6 +108,17 @@ TEST(NodalFrame, RejectsReactionSizeMismatch) {
|
|||||||
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
|
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, RejectsNodeOriginSizeMismatch) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0].nodal.origins.pop_back();
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
status, "results.nodal_origin_size_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(NodalFrame, RejectsDuplicateNodeId) {
|
TEST(NodalFrame, RejectsDuplicateNodeId) {
|
||||||
auto database = valid_database();
|
auto database = valid_database();
|
||||||
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
|
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
|
||||||
@@ -135,4 +182,110 @@ TEST(ResultDatabase, RejectsNonfiniteFrameTime) {
|
|||||||
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, DeclaresCoordinatesAndComponentOrdering) {
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::coordinate_system,
|
||||||
|
fesa::FieldCoordinateSystem::global);
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::displacement_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::reaction_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::coordinate_system,
|
||||||
|
fesa::FieldCoordinateSystem::element_local);
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::section_strain_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||||
|
"kappa_z"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::section_force_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"N", "Vy", "Vz", "T", "My", "Mz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::axial_stress_component,
|
||||||
|
std::string_view{"sigma_xx"});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, AcceptsFiniteConnectedBeamResults) {
|
||||||
|
const auto status = fesa::validate_result_database(valid_database());
|
||||||
|
|
||||||
|
EXPECT_TRUE(status.succeeded);
|
||||||
|
EXPECT_TRUE(status.diagnostics.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsDuplicateElementId) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& beams = database.steps[0].frames[0].element.beams;
|
||||||
|
beams.push_back(beams[0]);
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_element_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsDuplicateEndNode) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& ends = database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results;
|
||||||
|
ends[1].end_node = ends[0].end_node;
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_beam_end_node"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsWrongEndConnectivity) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[0].xi = 1.0;
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsEndNodeAbsentFromNodalFrame) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[1].end_node = fesa::NodeId{99};
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsNonfiniteLocalFrameAndSectionValue) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& beam = database.steps[0].frames[0].element.beams[0];
|
||||||
|
beam.local_frame.ey.y = std::numeric_limits<double>::infinity();
|
||||||
|
beam.end_results[1].section_force[4] =
|
||||||
|
std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsMismatchedRecoveryPointCount) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[1].sigma_xx.push_back(3.0);
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
status, "results.recovery_point_count_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/validation/comparison.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::array<double, 6> values(
|
||||||
|
const double first,
|
||||||
|
const double second,
|
||||||
|
const double third,
|
||||||
|
const double fourth,
|
||||||
|
const double fifth,
|
||||||
|
const double sixth) {
|
||||||
|
return {first, second, third, fourth, fifth, sixth};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::ResultFrame result_frame() {
|
||||||
|
fesa::NodalFrame nodal{
|
||||||
|
{fesa::NodeId{10}, fesa::NodeId{11}, fesa::NodeId{12}},
|
||||||
|
{
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Part-1-1", 101},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Part-1-1", 102},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Part-1-1", 103},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
values(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
|
||||||
|
values(7.0, 8.0, 9.0, 10.0, 11.0, 12.0),
|
||||||
|
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
values(13.0, 14.0, 15.0, 16.0, 17.0, 18.0),
|
||||||
|
values(19.0, 20.0, 21.0, 22.0, 23.0, 24.0),
|
||||||
|
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fesa::BeamElementFrame beam{
|
||||||
|
fesa::ElementId{20},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Part-1-1", 501},
|
||||||
|
{
|
||||||
|
fesa::Vec3{1.0, 0.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 0.0, 1.0},
|
||||||
|
},
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
-1.0,
|
||||||
|
fesa::NodeId{10},
|
||||||
|
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||||
|
values(25.0, 26.0, 27.0, 28.0, 29.0, 30.0),
|
||||||
|
31.0,
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
1.0,
|
||||||
|
fesa::NodeId{11},
|
||||||
|
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||||
|
values(32.0, 33.0, 34.0, 35.0, 36.0, 37.0),
|
||||||
|
38.0,
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
return {1.0, std::move(nodal), {{std::move(beam)}}, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::ComparisonSample sample(
|
||||||
|
const fesa::ReferenceQuantity quantity,
|
||||||
|
fesa::ResultPosition position,
|
||||||
|
std::vector<double> reference,
|
||||||
|
std::vector<double> actual,
|
||||||
|
const fesa::Tolerance tolerance) {
|
||||||
|
return {
|
||||||
|
quantity,
|
||||||
|
std::move(position),
|
||||||
|
std::move(reference),
|
||||||
|
std::move(actual),
|
||||||
|
tolerance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_failure(
|
||||||
|
const std::vector<fesa::Diagnostic>& failures,
|
||||||
|
const std::string_view code) {
|
||||||
|
return std::ranges::any_of(
|
||||||
|
failures,
|
||||||
|
[code](const fesa::Diagnostic& failure) {
|
||||||
|
return failure.stage == fesa::DiagnosticStage::validation &&
|
||||||
|
failure.severity == fesa::Severity::error &&
|
||||||
|
failure.code == code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ComparisonMetric, RejectsNearZeroErrorBeyondAbsoluteScale) {
|
||||||
|
const auto input = sample(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{2.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
const auto report = fesa::compare_samples(
|
||||||
|
std::span<const fesa::ComparisonSample>{&input, 1});
|
||||||
|
|
||||||
|
ASSERT_FALSE(report.passed);
|
||||||
|
EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 2.0);
|
||||||
|
ASSERT_EQ(report.failures.size(), 1U);
|
||||||
|
EXPECT_EQ(report.failures[0].code, "validation.tolerance_exceeded");
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("quantity=displacement"),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("entity=101"),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("component=Ux"),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("reference="),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("actual="),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("normalized_error="),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("relative_tolerance="),
|
||||||
|
std::string::npos);
|
||||||
|
EXPECT_NE(
|
||||||
|
report.failures[0].message.find("absolute_scale="),
|
||||||
|
std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ComparisonMetric, RejectsRepresentativeLargeRelativeError) {
|
||||||
|
const auto input = sample(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
|
||||||
|
{1.0e6 + 20.0, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
|
||||||
|
{1.0e-5, 0.0});
|
||||||
|
|
||||||
|
const auto report = fesa::compare_samples(
|
||||||
|
std::span<const fesa::ComparisonSample>{&input, 1});
|
||||||
|
|
||||||
|
EXPECT_FALSE(report.passed);
|
||||||
|
EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 2.0);
|
||||||
|
EXPECT_TRUE(has_failure(
|
||||||
|
report.failures, "validation.tolerance_exceeded"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ComparisonMetric, AcceptsErrorsAtTheExplicitToleranceBoundary) {
|
||||||
|
const std::vector<fesa::ComparisonSample> inputs{
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{1.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{1.0e-5, 1.0e-9}),
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
|
||||||
|
{1.0e6 + 10.0, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
|
||||||
|
{1.0e-5, 0.0}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto report = fesa::compare_samples(inputs);
|
||||||
|
|
||||||
|
EXPECT_TRUE(report.passed);
|
||||||
|
EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 1.0);
|
||||||
|
EXPECT_TRUE(report.failures.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ComparisonMetric, RejectsNonfiniteValues) {
|
||||||
|
const auto input = sample(
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
{"Part-1-1", 501, 101},
|
||||||
|
{10.0},
|
||||||
|
{std::numeric_limits<double>::quiet_NaN()},
|
||||||
|
{1.0e-5, 1.0e-6});
|
||||||
|
|
||||||
|
const auto report = fesa::compare_samples(
|
||||||
|
std::span<const fesa::ComparisonSample>{&input, 1});
|
||||||
|
|
||||||
|
EXPECT_FALSE(report.passed);
|
||||||
|
EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
|
||||||
|
EXPECT_TRUE(has_failure(
|
||||||
|
report.failures, "validation.nonfinite_comparison_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ComparisonMetric, RejectsDuplicateQuantityAndPosition) {
|
||||||
|
const std::vector<fesa::ComparisonSample> inputs{
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{1.0e-5, 1.0e-9}),
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{1.0, 1.0, 1.0, 1.0, 1.0, 1.0},
|
||||||
|
{1.0, 1.0, 1.0, 1.0, 1.0, 1.0},
|
||||||
|
{1.0e-5, 1.0e-9}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto report = fesa::compare_samples(inputs);
|
||||||
|
|
||||||
|
EXPECT_FALSE(report.passed);
|
||||||
|
EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
|
||||||
|
EXPECT_TRUE(has_failure(
|
||||||
|
report.failures, "validation.duplicate_result_position"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ComparisonMetric, RejectsComponentCountMismatch) {
|
||||||
|
const auto input = sample(
|
||||||
|
fesa::ReferenceQuantity::internal_force,
|
||||||
|
{"Part-1-1", 501, 101},
|
||||||
|
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
||||||
|
{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
const auto report = fesa::compare_samples(
|
||||||
|
std::span<const fesa::ComparisonSample>{&input, 1});
|
||||||
|
|
||||||
|
EXPECT_FALSE(report.passed);
|
||||||
|
EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
|
||||||
|
EXPECT_TRUE(has_failure(
|
||||||
|
report.failures, "validation.component_count_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CorrelationMetric, ComputesComponentWiseRmseAndRelativeL2) {
|
||||||
|
const std::vector<fesa::ComparisonSample> inputs{
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{3.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0e-9}),
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
{"Part-1-1", 102, std::nullopt},
|
||||||
|
{4.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0e-9}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto report = fesa::correlate_samples(inputs);
|
||||||
|
|
||||||
|
ASSERT_TRUE(report.evaluable);
|
||||||
|
ASSERT_TRUE(report.failures.empty());
|
||||||
|
ASSERT_EQ(report.metrics.size(), 6U);
|
||||||
|
EXPECT_EQ(report.metrics[0].quantity, fesa::ReferenceQuantity::displacement);
|
||||||
|
EXPECT_EQ(report.metrics[0].component_index, 0U);
|
||||||
|
EXPECT_EQ(report.metrics[0].value_count, 2U);
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
report.metrics[0].root_mean_square_error,
|
||||||
|
std::sqrt(12.5));
|
||||||
|
EXPECT_DOUBLE_EQ(report.metrics[0].relative_l2_error, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(report.metrics[1].root_mean_square_error, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(report.metrics[1].relative_l2_error, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CorrelationMetric, UsesAbsoluteScaleNormForNearZeroReference) {
|
||||||
|
const std::vector<fesa::ComparisonSample> inputs{
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{3.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0e-9}),
|
||||||
|
sample(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
{"Part-1-1", 102, std::nullopt},
|
||||||
|
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{4.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0e-9}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto report = fesa::correlate_samples(inputs);
|
||||||
|
|
||||||
|
ASSERT_TRUE(report.evaluable);
|
||||||
|
ASSERT_EQ(report.metrics.size(), 6U);
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
report.metrics[0].root_mean_square_error,
|
||||||
|
std::sqrt(12.5) * 1.0e-9);
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
report.metrics[0].relative_l2_error,
|
||||||
|
5.0 / std::sqrt(2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EntityMatching, MatchesNodalResultByInstanceAndExternalLabel) {
|
||||||
|
const auto frame = result_frame();
|
||||||
|
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
||||||
|
|
||||||
|
const auto match = fesa::make_comparison_sample(
|
||||||
|
frame,
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
{"Part-1-1", 102, std::nullopt},
|
||||||
|
reference,
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
ASSERT_TRUE(match.sample.has_value());
|
||||||
|
EXPECT_TRUE(match.failures.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
match.sample->actual,
|
||||||
|
(std::vector<double>{7.0, 8.0, 9.0, 10.0, 11.0, 12.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EntityMatching, MatchesReactionComponentsWithoutUsingDisplacement) {
|
||||||
|
const auto frame = result_frame();
|
||||||
|
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
||||||
|
|
||||||
|
const auto match = fesa::make_comparison_sample(
|
||||||
|
frame,
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
{"Part-1-1", 101, std::nullopt},
|
||||||
|
reference,
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
ASSERT_TRUE(match.sample.has_value());
|
||||||
|
EXPECT_TRUE(match.failures.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
match.sample->actual,
|
||||||
|
(std::vector<double>{13.0, 14.0, 15.0, 16.0, 17.0, 18.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EntityMatching, MatchesElementResultByElementAndEndNodeLabels) {
|
||||||
|
const auto frame = result_frame();
|
||||||
|
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
||||||
|
|
||||||
|
const auto match = fesa::make_comparison_sample(
|
||||||
|
frame,
|
||||||
|
fesa::ReferenceQuantity::internal_force,
|
||||||
|
{"Part-1-1", 501, 102},
|
||||||
|
reference,
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
ASSERT_TRUE(match.sample.has_value());
|
||||||
|
EXPECT_TRUE(match.failures.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
match.sample->actual,
|
||||||
|
(std::vector<double>{32.0, 33.0, 34.0, 35.0, 36.0, 37.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EntityMatching, MatchesCentroidStressAtTheRequestedElementEnd) {
|
||||||
|
const auto frame = result_frame();
|
||||||
|
const std::array reference{0.0};
|
||||||
|
|
||||||
|
const auto match = fesa::make_comparison_sample(
|
||||||
|
frame,
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
{"Part-1-1", 501, 101},
|
||||||
|
reference,
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
ASSERT_TRUE(match.sample.has_value());
|
||||||
|
EXPECT_TRUE(match.failures.empty());
|
||||||
|
EXPECT_EQ(match.sample->actual, (std::vector<double>{31.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EntityMatching, RejectsUnknownResultOrigin) {
|
||||||
|
const auto frame = result_frame();
|
||||||
|
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
||||||
|
|
||||||
|
const auto match = fesa::make_comparison_sample(
|
||||||
|
frame,
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
{"Part-1-1", 999, std::nullopt},
|
||||||
|
reference,
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
EXPECT_FALSE(match.sample.has_value());
|
||||||
|
EXPECT_TRUE(has_failure(
|
||||||
|
match.failures, "validation.unknown_result_origin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EntityMatching, RejectsNodeThatIsNotAnEndOfTheElement) {
|
||||||
|
const auto frame = result_frame();
|
||||||
|
const std::array reference{0.0};
|
||||||
|
|
||||||
|
const auto match = fesa::make_comparison_sample(
|
||||||
|
frame,
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
{"Part-1-1", 501, 103},
|
||||||
|
reference,
|
||||||
|
{1.0e-5, 1.0e-9});
|
||||||
|
|
||||||
|
EXPECT_FALSE(match.sample.has_value());
|
||||||
|
EXPECT_TRUE(has_failure(
|
||||||
|
match.failures, "validation.invalid_element_node_pair"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/validation/reference_csv.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TemporaryCsv final {
|
||||||
|
public:
|
||||||
|
TemporaryCsv(std::string_view name, std::string_view contents)
|
||||||
|
: path_{std::filesystem::path{testing::TempDir()} / name} {
|
||||||
|
std::ofstream output{path_, std::ios::binary};
|
||||||
|
output.write(
|
||||||
|
contents.data(), static_cast<std::streamsize>(contents.size()));
|
||||||
|
if (!output) {
|
||||||
|
throw std::runtime_error{"Failed to write temporary reference CSV."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~TemporaryCsv() {
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::remove(path_, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TemporaryCsv(const TemporaryCsv&) = delete;
|
||||||
|
TemporaryCsv& operator=(const TemporaryCsv&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||||
|
return path_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path fixture_path(const std::string_view name) {
|
||||||
|
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
|
||||||
|
"reference" / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path supplied_reference_path(const std::string_view name) {
|
||||||
|
return std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
|
||||||
|
"reference" / "cantilever beam" / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_diagnostic(
|
||||||
|
const std::vector<fesa::Diagnostic>& diagnostics,
|
||||||
|
const std::string_view code) {
|
||||||
|
return std::ranges::any_of(
|
||||||
|
diagnostics,
|
||||||
|
[code](const fesa::Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.code == code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, ReadsSuppliedDisplacementsWithWhitespaceHeader) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
supplied_reference_path("cantilever beam displacements.csv"),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 11U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().quantity,
|
||||||
|
fesa::ReferenceQuantity::displacement);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "PART-1_1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 1);
|
||||||
|
EXPECT_FALSE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().values,
|
||||||
|
(std::vector<double>{0.0, 0.0, -1.0e-30, 0.0, 1.0e-29, 0.0}));
|
||||||
|
EXPECT_EQ(result.rows.back().position.entity_label, 11);
|
||||||
|
EXPECT_EQ(result.rows.back().values[2], -1.91857e-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, ReadsSuppliedReactionsWithWhitespaceHeader) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
supplied_reference_path("cantilever beam reactions.csv"),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 11U);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "PART-1_1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 1);
|
||||||
|
EXPECT_FALSE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().values,
|
||||||
|
(std::vector<double>{0.0, 0.0, 1.0e6, 0.0, -1.0e7, 0.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InternalForceCsv, MapsAllSixComponentsAndElementEndPosition) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::internal_force,
|
||||||
|
fixture_path("internalforces.csv"),
|
||||||
|
"unused-request-name");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 2U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().quantity,
|
||||||
|
fesa::ReferenceQuantity::internal_force);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 501);
|
||||||
|
ASSERT_TRUE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(*result.rows.front().position.end_node_label, 101);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().values,
|
||||||
|
(std::vector<double>{1.25, 3.75, -2.5, -6.25, -4.0, 5.5}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InternalForceCsv, AcceptsTrailingEmptyAbaqusExportColumns) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-trailing-empty-columns.csv",
|
||||||
|
"Part Instance Name,Element Label,Node Label,SF-SF1,SF-SF2,"
|
||||||
|
"SF-SF3,SM-SM1,SM-SM2,SM-SM3,,,\n"
|
||||||
|
"PART-1_1-1,1,1,1,2,3,4,5,6,,,\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::internal_force,
|
||||||
|
input.path(),
|
||||||
|
"unused-request-name");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().values,
|
||||||
|
(std::vector<double>{1.0, 3.0, 2.0, 6.0, 4.0, 5.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StressCsv, FillsOmittedInstanceAndReadsCentroidStress) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
fixture_path("stresses.csv"),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 2U);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 501);
|
||||||
|
ASSERT_TRUE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(*result.rows.front().position.end_node_label, 101);
|
||||||
|
EXPECT_EQ(result.rows.front().values, (std::vector<double>{42.5}));
|
||||||
|
EXPECT_EQ(result.rows.back().values, (std::vector<double>{-17.25}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, AcceptsUtf8BomOnlyAtTheStartAndTrimsValues) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-bom.csv",
|
||||||
|
"\xEF\xBB\xBF Part Instance Name , Node Label , U-U1 , U-U2 , "
|
||||||
|
"U-U3 , UR-UR1 , UR-UR2 , UR-UR3\n"
|
||||||
|
" Beam-1 , 7 , 1 , 2 , 3 , 4 , 5 , 6 \n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement, input.path(), "unused");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 1U);
|
||||||
|
EXPECT_EQ(result.rows[0].position.instance_name, "Beam-1");
|
||||||
|
EXPECT_EQ(result.rows[0].position.entity_label, 7);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows[0].values,
|
||||||
|
(std::vector<double>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, DiagnosesMissingRequiredColumn) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-missing-column.csv",
|
||||||
|
"Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,Unexpected\n"
|
||||||
|
"1,0,0,0,0,0,0\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.rows.empty());
|
||||||
|
ASSERT_TRUE(has_diagnostic(
|
||||||
|
result.diagnostics, "validation.reference_csv_missing_column"));
|
||||||
|
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics.front().source->line, 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, DiagnosesDuplicateResultPosition) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-duplicate.csv",
|
||||||
|
"Element Label,Node Label,Sxx\n"
|
||||||
|
"8,2,10\n"
|
||||||
|
"8,2,11\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
input.path(),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.rows.empty());
|
||||||
|
ASSERT_TRUE(has_diagnostic(
|
||||||
|
result.diagnostics, "validation.reference_csv_duplicate_row"));
|
||||||
|
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics.front().source->line, 3U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, DiagnosesInvalidAndNonfiniteNumbers) {
|
||||||
|
const TemporaryCsv invalid{
|
||||||
|
"fesa-reference-invalid-number.csv",
|
||||||
|
"Node Label,RF-RF1,RF-RF2,RF-RF3,RM-RM1,RM-RM2,RM-RM3\n"
|
||||||
|
"1,0,0,not-a-number,0,0,0\n"};
|
||||||
|
const TemporaryCsv nonfinite{
|
||||||
|
"fesa-reference-nonfinite-number.csv",
|
||||||
|
"Element Label,Node Label,Sxx\n"
|
||||||
|
"8,2,nan\n"};
|
||||||
|
|
||||||
|
const auto invalid_result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
invalid.path(),
|
||||||
|
"Part-1-1");
|
||||||
|
const auto nonfinite_result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
nonfinite.path(),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(invalid_result.rows.empty());
|
||||||
|
EXPECT_TRUE(nonfinite_result.rows.empty());
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
invalid_result.diagnostics,
|
||||||
|
"validation.reference_csv_invalid_number"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
nonfinite_result.diagnostics,
|
||||||
|
"validation.reference_csv_invalid_number"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, RejectsUtf8BomOutsideTheFileStart) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-misplaced-bom.csv",
|
||||||
|
"Node Label,\xEF\xBB\xBF U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n"
|
||||||
|
"1,0,0,0,0,0,0\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.rows.empty());
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
result.diagnostics,
|
||||||
|
"validation.reference_csv_invalid_encoding"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user