Compare commits
10 Commits
51939fba5b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 5855604318 | |||
| 6bc7cc3ada | |||
| 8c448ffe45 | |||
| ca0268ea5b | |||
| 675379779f | |||
| b68f6ee143 | |||
| 02e2994c5b | |||
| 4f96719663 | |||
| 2df90b95f8 | |||
| 98c13c2ec2 |
@@ -60,7 +60,7 @@
|
||||
- 전단강성이 생략되면 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 Phase 1 기본값으로
|
||||
적용한다.
|
||||
- reference 비교는 metadata 없이 요청된 물리량과 CSV 경로를 명시한다. 현재
|
||||
캔틸레버 샘플은 변위와 반력을 비교하며 요소 내력과 도심 응력 비교 루틴은
|
||||
캔틸레버 샘플은 변위, 반력 및 요소 단면력을 비교하며 도심 응력 비교 루틴은
|
||||
synthetic CSV로 검증한다.
|
||||
- 새 MSVC 빌드 경고를 추가하지 않는다.
|
||||
- 변경은 요청 범위에 한정하고 Conventional Commits 형식의 메시지를 사용한다.
|
||||
|
||||
@@ -47,6 +47,8 @@ add_library(fesa_core STATIC
|
||||
src/fesa/model/domain_builder.cpp
|
||||
src/fesa/results/result_database.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
|
||||
@@ -66,6 +68,16 @@ target_link_libraries(fesa PRIVATE fesa_core)
|
||||
target_compile_features(fesa PRIVATE cxx_std_20)
|
||||
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)
|
||||
add_executable(fesa_assembly_benchmark
|
||||
tests/performance/assembly_benchmark.cpp
|
||||
|
||||
+43
-2
@@ -164,7 +164,7 @@ HDF5 파일에 저장하고 root에 schema version을 기록한다.
|
||||
|
||||
## ADR-010: 검증 계층별 허용오차
|
||||
|
||||
**상태:** Accepted
|
||||
**상태:** Accepted for threshold tests; cross-formulation correlation superseded by ADR-017
|
||||
|
||||
**상황:** 모든 물리량에 하나의 상대오차를 적용하면 영에 가까운 값이나 서로 다른
|
||||
규모의 결과를 올바르게 판정할 수 없다.
|
||||
@@ -177,6 +177,8 @@ HDF5 파일에 저장하고 root에 schema version을 기록한다.
|
||||
- 영에 가까운 값과 큰 값 모두 의미 있게 비교할 수 있다.
|
||||
- 모델별 예외 tolerance에는 문서화된 수치 근거가 필요하다.
|
||||
- 단일 tolerance보다 comparison request와 helper가 복잡해진다.
|
||||
- Abaqus와 FESA의 정식화가 다른 상관성 비교에는 ADR-017의 component별 RMSE와
|
||||
Relative L2 계약을 적용한다.
|
||||
|
||||
## ADR-011: 일관 단위계와 결과 좌표계
|
||||
|
||||
@@ -257,7 +259,7 @@ flat `Domain`으로 정규화한다. 외부 entity는 `(instance name, part-loca
|
||||
|
||||
## ADR-015: 명시적 물리량 선택 기반 CSV 검증
|
||||
|
||||
**상태:** Accepted
|
||||
**상태:** Superseded by ADR-017
|
||||
|
||||
**상황:** 현재 캔틸레버 reference에는 변위와 반력만 있고 per-model metadata는
|
||||
요구하지 않는다. 요소 내력과 응력 비교 기능은 해당 CSV가 추가되기 전에 구현해야
|
||||
@@ -297,3 +299,42 @@ toolset을 명시한다. CMake, CMake Presets, CTest 및 GoogleTest/GoogleMock
|
||||
보장 대상이 아니다.
|
||||
- 컴파일러 갱신에 따른 경고와 표준 라이브러리 동작은 전체 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 결과 비교
|
||||
- `reference/<model-id>`: Abaqus 입력과 현재 사용할 수 있는 결과 CSV
|
||||
|
||||
reference comparison request가 비교할 물리량과 CSV 경로, 상대 tolerance 및
|
||||
물리량별 절대 scale을 명시한다. 요청한 CSV가 없으면 실패하며 요청하지 않은 결과를
|
||||
통과로 표시하지 않는다. 현재 캔틸레버는 변위와 반력만 요청하고, 요소 내력과
|
||||
단면 도심 응력 adapter는 synthetic CSV로 검증한다.
|
||||
reference comparison request가 비교할 물리량과 CSV 경로 및 물리량별 절대 scale을
|
||||
명시한다. 요청한 CSV가 없으면 실패하며 요청하지 않은 결과를 통과로 표시하지
|
||||
않는다. 현재 캔틸레버는 변위, 반력 및 요소 단면력을 요청하고, 단면 도심 응력
|
||||
adapter는 synthetic CSV로 검증한다.
|
||||
|
||||
요소 내력 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에서는
|
||||
Instance 열 생략을 허용하되 comparison request가 제공한 Instance 이름으로
|
||||
보완한다.
|
||||
보완한다. Abaqus와 FESA의 정식화가 다른 상관성 비교는 component별 RMSE와
|
||||
Relative L2를 보고하며 관측값으로 만든 pass/fail tolerance를 적용하지 않는다.
|
||||
|
||||
reference helper는 반드시 public parser와 analysis 경로로 FESA 결과를 생성한다.
|
||||
테스트 전용 경로로 Domain이나 matrix를 직접 주입해 전체 파이프라인 결함을 숨기지
|
||||
|
||||
+224
-356
@@ -1,50 +1,46 @@
|
||||
# FESA Session Handoff
|
||||
|
||||
## 1. 문서 목적과 기준
|
||||
## 1. 목적과 기준 문서
|
||||
|
||||
이 문서는 `deterministic-parallel-assembly` 완료 후 새 세션에서
|
||||
`result-contract-completion` Phase를 바로 시작하기 위한 인수인계 기록이다.
|
||||
요구사항과 설계의 최종 기준은 다음 파일이다.
|
||||
이 문서는 `beam-reference-qualification` 완료 후 새 세션에서 마지막 Phase 1 단계인
|
||||
`internal-release`를 시작하기 위한 인수인계 기록이다. 과거 phase의 구현 역사를
|
||||
반복하기보다 현재 기준선, 검증 결과, 남은 작업과 실행 순서를 제공한다.
|
||||
|
||||
다음 문서를 우선 기준으로 사용한다.
|
||||
|
||||
- `AGENTS.md`
|
||||
- `docs/PRD.md`
|
||||
- `docs/ARCHITECTURE.md`
|
||||
- `docs/ADR.md`
|
||||
- `docs/HARNESS.md`
|
||||
- `docs/formulation/timoshenko-beam-3d.md`
|
||||
- `docs/HDF5_SCHEMA.md`
|
||||
- `phases/result-contract-completion/index.json`
|
||||
- `phases/result-contract-completion/step0.md`부터 `step2.md`
|
||||
- `docs/ABAQUS_INPUT_SUBSET.md`
|
||||
- `docs/formulation/timoshenko-beam-3d.md`
|
||||
- `docs/VALIDATION.md`
|
||||
- `phases/internal-release/index.json`
|
||||
- `phases/internal-release/step0.md`부터 `step4.md`
|
||||
|
||||
내용이 충돌하면 위 기준 문서와 `phases/`의 현재 metadata를 우선한다. 이 문서는
|
||||
현재 구현, 검증 baseline, 다음 Phase에서 먼저 정렬해야 할 계약과 실행환경을
|
||||
보충한다.
|
||||
내용이 충돌하면 `AGENTS.md`, PRD/ADR/아키텍처, 완료된 phase metadata와 실제
|
||||
테스트를 우선한다. 특히 `internal-release`의 기존 Step 0과 Step 4에 남아 있는
|
||||
reference 범위 설명은 4절의 최신 계약으로 바로잡은 뒤 release evidence에 사용한다.
|
||||
|
||||
## 2. 현재 저장소와 Phase 상태
|
||||
## 2. Git과 Phase 상태
|
||||
|
||||
2026-08-02 확인 기준:
|
||||
2026-08-03 기준 구현 상태는 다음과 같다.
|
||||
|
||||
- 기준 브랜치: `dev`
|
||||
- 이 HANDOFF 갱신 직전 구현 HEAD:
|
||||
`7d52247d6c6180ac4f4dab2e6ecb0ee1be2608d0`
|
||||
- 갱신 직전 `dev`는 `origin/dev`보다 9개 commit 앞서 있었고 작업 트리는 clean이었다.
|
||||
- `feat-deterministic-parallel-assembly`는 `dev`에 fast-forward 병합한 뒤 삭제했다.
|
||||
- 완료 Phase:
|
||||
- `solver-bootstrap`
|
||||
- `domain-and-input-skeleton`
|
||||
- `fem-and-beam-kernel`
|
||||
- `equation-and-linear-solve`
|
||||
- `results-and-pipeline`
|
||||
- `abaqus-subset-completion`
|
||||
- `deterministic-parallel-assembly`
|
||||
- 다음 Phase: `result-contract-completion`
|
||||
- 다음 Step: Step 0 `beam-element-end-recovery`
|
||||
- `result-contract-completion`의 Step 0~2는 모두 pending이다.
|
||||
- 후속 Phase `beam-reference-qualification`과 `internal-release`도 pending이다.
|
||||
- 이 문서 갱신 직전 구현 HEAD: `6bc7cc3adadded6f1a2a0ed45449b2264c0f7a4e`
|
||||
- `feat-beam-reference-qualification`은 `dev`에 fast-forward 병합된 뒤 로컬에서
|
||||
삭제됐다.
|
||||
- `internal-release`를 제외한 `phases/index.json`의 모든 phase가 `completed`다.
|
||||
- 다음 Phase: `internal-release`
|
||||
- 다음 Step: Step 0 `release-checklist`
|
||||
- Step 0부터 Step 4까지 모두 `pending`이다.
|
||||
- 이 문서의 갱신은 구현 기준선 다음의 docs-only commit이다.
|
||||
|
||||
이 문서는 위 9개 구현/phase commit 다음 commit으로 `dev`에 기록하고 모두
|
||||
`origin/dev`에 push한다. 새 세션에서는 reset, rebase 또는 force push로 상태를
|
||||
맞추지 말고 먼저 실제 동기화 상태를 확인한다.
|
||||
새 세션에서는 기록된 hash를 강제로 맞추지 말고 로컬과 원격의 실제 상태를 먼저
|
||||
확인한다. 이 HANDOFF commit이 push된 뒤에는 `dev`와 `origin/dev`가 같은 commit을
|
||||
가리켜야 한다.
|
||||
|
||||
```powershell
|
||||
git switch dev
|
||||
@@ -54,248 +50,185 @@ git rev-parse origin/dev
|
||||
git rev-list --left-right --count origin/dev...dev
|
||||
```
|
||||
|
||||
이번에 함께 push할 선행 commit은 다음과 같다.
|
||||
reset, rebase 또는 force push로 차이를 숨기지 않는다. 예상하지 못한 변경이 있으면
|
||||
소유자를 확인하고 보존한다.
|
||||
|
||||
```text
|
||||
40a7e6c feat(deterministic-parallel-assembly): step 0 — canonical-contribution-order
|
||||
b9bb439 chore(deterministic-parallel-assembly): step 0 output
|
||||
6c2e1f3 feat(deterministic-parallel-assembly): step 1 — tbb-element-evaluation
|
||||
dc6baed chore(deterministic-parallel-assembly): step 1 output
|
||||
127286c feat(deterministic-parallel-assembly): step 2 — thread-count-determinism
|
||||
6ee51a8 chore(deterministic-parallel-assembly): step 2 output
|
||||
6ad9f3c chore(deterministic-parallel-assembly): mark phase completed
|
||||
ac8d24a test(assembly): cover deterministic kernel failures
|
||||
7d52247 fix(assembly): stage benchmark TBB runtime
|
||||
```
|
||||
## 3. 완료된 Phase 1 기준선
|
||||
|
||||
## 3. 완료된 deterministic parallel assembly
|
||||
현재 production 경로는 다음 기능을 제공한다.
|
||||
|
||||
Phase metadata는 `phases/deterministic-parallel-assembly/index.json`에 기록되어
|
||||
있으며 Step 0~2가 모두 completed다.
|
||||
- Abaqus `.inp` 제한 부분집합의 flat/orphan mesh 또는 좌표변환이 없는 단일
|
||||
Part/Assembly/Instance 모델 파싱과 semantic validation
|
||||
- 2절점 3D Isoparametric Timoshenko Beam, 등방성 선형 탄성, 일반 단면,
|
||||
`*BOUNDARY`, `*CLOAD`, 단일 선형 정적 Step
|
||||
- MSVC v145, Intel oneAPI MKL PARDISO와 TBB를 사용하는 Windows x64 해석 경로
|
||||
- canonical contribution ordering에 기반한 결정론적 병렬 요소 평가와 조립
|
||||
- 원래 full equation에서 변위, 반력과 평형을 복구하는 선형 정적 해석
|
||||
- 두 요소 끝의 section strain, section force, centroid/recovery-point `Sxx` 회복
|
||||
- node/element provenance, component와 좌표계 metadata를 포함한 완전한
|
||||
`ResultDatabase`
|
||||
- 입력 원본 없이 model, analysis와 결과를 재구성할 수 있는 HDF5 schema `2.0.0`
|
||||
- 변위, 반력, 요소 단면력용 Abaqus CSV adapter와 component별 상관성 보고 CLI
|
||||
|
||||
주요 결과:
|
||||
결과 계약과 Beam reference phase의 세부 완료 기록은 다음 파일에 있다.
|
||||
|
||||
- `MatrixContribution`의 canonical key를
|
||||
`(row, column, element, local_order)`로 고정했다.
|
||||
- upper-triangle CSR pattern과 값을 한 번의 deterministic serial merge로 만든다.
|
||||
- oneTBB는 Beam 요소별 `compute_beam3d2` 평가에만 사용한다.
|
||||
- worker는 요소별 로컬 contribution을 만들고 shared CSR values에 쓰지 않는다.
|
||||
- `AssemblyOptions.max_threads`와 `grain_size`는 0을 거부한다.
|
||||
- element storage order와 external label 순열에 관계없이 기존 serial oracle의
|
||||
`EntityOrigin` 순서를 보존하도록 canonical rank를 contribution의 `ElementId`에
|
||||
사용한다.
|
||||
- 여러 Beam kernel failure가 동시에 발생해도 canonical element origin상 첫 오류를
|
||||
반환한다. 해당 독립 review 보완은 mutation test로 RED를 확인한 뒤 추가했다.
|
||||
- thread count 1, 2, 16에서 10회 반복하며 CSR 구조/값, RHS, displacement와
|
||||
reaction의 bit pattern이 동일함을 검증한다.
|
||||
- `fesa_assembly_benchmark`는 production serial/parallel API를 사용하고 시간과
|
||||
element count만 출력한다. speedup은 correctness 조건이 아니다.
|
||||
- benchmark target의 output directory에 imported `TBB::tbb` runtime DLL을
|
||||
post-build로 복사하므로 새 셸의 PATH에 TBB가 없어도 정확한 exe 경로로 실행된다.
|
||||
|
||||
관련 파일:
|
||||
|
||||
- `include/fesa/assembly/contribution.hpp`
|
||||
- `include/fesa/assembly/assembler.hpp`
|
||||
- `src/fesa/assembly/contribution.cpp`
|
||||
- `src/fesa/assembly/parallel_assembler.cpp`
|
||||
- `src/fesa/assembly/serial_assembler.cpp`
|
||||
- `tests/unit/assembly/serial_assembler_test.cpp`
|
||||
- `tests/unit/assembly/parallel_assembler_test.cpp`
|
||||
- `tests/integration/assembly/thread_count_determinism_test.cpp`
|
||||
- `tests/performance/assembly_benchmark.cpp`
|
||||
|
||||
유지할 핵심 경계:
|
||||
|
||||
- `assemble_serial`은 계속 bitwise oracle이다.
|
||||
- TBB task가 끝난 후에만 solver를 호출한다.
|
||||
- TBB type은 `core`, `model`, `fem`, `elements` public contract에 노출하지 않는다.
|
||||
- scheduling 순서가 contribution 합산 순서나 오류 선택을 결정하면 안 된다.
|
||||
- `LinearStaticAnalysis`는 현재도 `assemble_serial`을 호출한다. 다음 Phase 요구사항에
|
||||
없는 parallel backend 선택 기능을 끼워 넣지 않는다.
|
||||
|
||||
## 4. 현재 결과·HDF5 baseline과 남은 간극
|
||||
|
||||
`results-and-pipeline`에서 최소 수직 파이프라인은 완성되어 있다.
|
||||
|
||||
- `ResultDatabase -> ResultStep -> ResultFrame -> NodalFrame` semantic model
|
||||
- serial assembly, essential BC, PARDISO, full displacement reconstruction과
|
||||
`r=Ku-f` 반력 복구를 조율하는 `LinearStaticAnalysis`
|
||||
- all-constrained case의 order-0 PARDISO 우회
|
||||
- schema `1.0.0` HDF5 writer와 public reader
|
||||
- parser부터 HDF5 output까지 연결하는 `run_solver`와 `fesa solve`
|
||||
|
||||
현재 구현의 정확한 한계:
|
||||
|
||||
- `ResultFrame`은 `step_time`, nodal displacement/reaction, diagnostics만 가진다.
|
||||
- node/element `EntityOrigin`, field 좌표계, component label/order가 semantic result에
|
||||
명시되어 있지 않다.
|
||||
- Beam section strain/resultant, local frame, centroid/recovery-point stress가 없다.
|
||||
- `LinearStaticAnalysis`는 nodal result만 채우며 element recovery를 호출하지 않는다.
|
||||
- HDF5 schema `1.0.0`은 node 좌표·origin, Beam connectivity/section ID, 적용된
|
||||
shear area/source와 nodal result만 저장한다.
|
||||
- HDF5 reader의 model은 adapter 전용 `Hdf5ModelSnapshot`이며 완전한 model/analysis
|
||||
재구성 계약이 아니다.
|
||||
- material 전체 속성, sets, section 전체 속성/orientation/recovery points,
|
||||
step BC/load/solver settings, element result와 diagnostics는 파일에 없다.
|
||||
- writer는 non-empty frame diagnostics를
|
||||
`hdf5.unsupported_result_diagnostics`로 거부한다.
|
||||
|
||||
관련 파일:
|
||||
|
||||
- `include/fesa/results/result_database.hpp`
|
||||
- `src/fesa/results/result_database.cpp`
|
||||
- `include/fesa/analysis/linear_static_analysis.hpp`
|
||||
- `src/fesa/analysis/linear_static_analysis.cpp`
|
||||
- `include/fesa/io/hdf5/writer.hpp`
|
||||
- `src/fesa/io/hdf5/writer.cpp`
|
||||
- `phases/result-contract-completion/index.json`
|
||||
- `phases/beam-reference-qualification/index.json`
|
||||
- `docs/HDF5_SCHEMA.md`
|
||||
- `tests/unit/results/result_database_test.cpp`
|
||||
- `tests/unit/analysis/linear_static_analysis_test.cpp`
|
||||
- `tests/integration/io/hdf5_results_test.cpp`
|
||||
- `tests/integration/pipeline/minimal_cantilever_test.cpp`
|
||||
- `docs/VALIDATION.md`
|
||||
|
||||
이 간극을 `result-contract-completion`에서만 필요한 만큼 채운다.
|
||||
`internal-release`에서는 위 solver 계약을 다시 설계하지 않는다. 설치, clean consumer,
|
||||
scale measurement와 release evidence에 필요한 최소 변경만 수행한다.
|
||||
|
||||
## 5. 다음 Phase 목표와 Step 순서
|
||||
## 4. 검증 상태와 남은 제한
|
||||
|
||||
### Step 0 - beam-element-end-recovery
|
||||
### 4.1 이중 검증 gate
|
||||
|
||||
- 두 요소 끝의 section strain과 section resultant를 계산한다.
|
||||
- strain 순서는
|
||||
`(epsilon, gamma_y, gamma_z, kappa_x, kappa_y, kappa_z)`, force 순서는
|
||||
`(N, Vy, Vz, T, My, Mz)`로 고정한다.
|
||||
- centroid stress는 `N/A`, recovery point `(y,z)`의 stress는 axial+bending
|
||||
`sigma_xx`만 계산한다.
|
||||
- 순수 축, 비틀림, 각 축 굽힘, 이축 굽힘, 양 끝 부호와 입력 recovery-point 순서를
|
||||
hand calculation으로 먼저 고정한다.
|
||||
- stiffness와 동일한 `BeamFrame`, DOF, 회전 및 부호 convention을 재사용한다.
|
||||
`docs/VALIDATION.md`가 검증 결과의 단일 상세 보고서다.
|
||||
|
||||
Focused acceptance:
|
||||
- Gate A — FESA 정식화 적합성: **PASS**
|
||||
- Gate B — Abaqus 결과 상관성: **EVALUABLE**
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R "BeamRecovery|CentroidStress|SectionForce" --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
Gate A는 해석해, strain energy, 강체 mode, 회전 불변성, 평형과 결정성을 엄격한
|
||||
tolerance로 검증한다. Gate B는 Abaqus B31과 FESA 정식화의 차이를 인정하고
|
||||
component별 RMSE와 Relative L2를 보고한다. Gate B의 `EVALUABLE`은 모든 요청
|
||||
entity/component가 유일하게 매칭되고 metric이 유한하다는 뜻이며, 임의의 상관성
|
||||
pass/fail threshold를 통과했다는 뜻은 아니다.
|
||||
|
||||
### Step 1 - complete-result-contract
|
||||
현재 cantilever reference의 주요 Relative L2는 다음과 같다.
|
||||
|
||||
- `BeamElementFrame`에 element ID/origin, local frame과 두 끝
|
||||
`BeamSectionResult`를 저장한다.
|
||||
- `ResultFrame`에 `ElementFrame`을 추가한다.
|
||||
- node/element provenance, field 좌표계와 component label/order를 semantic contract에
|
||||
명시한다.
|
||||
- duplicate element/end node, wrong connectivity, nonfinite value와 mismatched
|
||||
recovery-point count를 validation에서 거부한다.
|
||||
- `LinearStaticAnalysis`가 production Beam recovery API를 호출해 element result를
|
||||
채운다. recovery 수식을 analysis에 복제하지 않는다.
|
||||
| 결과 | Component | Relative L2 |
|
||||
|---|---|---:|
|
||||
| displacement | `Uz` | `0.0022349969291714038` |
|
||||
| displacement | `Ry` | `0.0000010416581667076092` |
|
||||
| reaction | `RFz` | `4.4393520322089023e-13` |
|
||||
| reaction | `RMy` | `2.591919334788851e-13` |
|
||||
| internal force | `Vz` | `2.7107472798172426e-13` |
|
||||
| internal force | `My` | `0.082541022764834646` |
|
||||
|
||||
Focused acceptance:
|
||||
18개 전체 component의 count, RMSE와 Relative L2는 `docs/VALIDATION.md`를 사용한다.
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R "CompleteResultContract|ElementFrame" --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
### 4.2 Reference 모델과 축 매핑
|
||||
|
||||
### Step 2 - self-contained-hdf5
|
||||
- Abaqus provenance는 `reference/cantilever beam/cantilever beam.inp`와 세 CSV다.
|
||||
- Abaqus 원본 모델의 `SCF=0.25`는 보존한다.
|
||||
- FESA projection은 `reference/cantilever beam/cantilever beam fesa.inp`이며 다른
|
||||
의미 입력을 유지하고 `SCF=0`을 사용한다.
|
||||
- FESA에 Abaqus SCF 보정이나 결과 맞춤 계수를 추가하지 않는다.
|
||||
|
||||
- 구현보다 먼저 `docs/HDF5_SCHEMA.md`에 새 version과 모든 dataset의 type, rank,
|
||||
shape, component/coordinate attributes를 확정한다.
|
||||
- 파일 하나로 model, single-step analysis settings와 모든 Phase 1 result를
|
||||
재구성한다.
|
||||
- model에는 coordinates/connectivity/origin, sets, materials, section 전체 속성,
|
||||
orientation, recovery points와 적용 shear 값/source를 포함한다.
|
||||
- analysis에는 step, BC, load와 solver settings를 포함한다.
|
||||
- results에는 nodal displacement/reaction, Beam local frame, 두 끝 strain/force,
|
||||
centroid/recovery-point `Sxx`와 diagnostics를 포함한다.
|
||||
- public reader round trip과 `h5ls`로 구조를 독립 검증한다.
|
||||
요소력 CSV는 다음 순서로 매핑한다.
|
||||
|
||||
Focused acceptance:
|
||||
| FESA | Abaqus CSV |
|
||||
|---|---|
|
||||
| `N` | `SF1` |
|
||||
| `Vy` | `SF3` |
|
||||
| `Vz` | `SF2` |
|
||||
| `T` | `SM3` |
|
||||
| `My` | `SM1` |
|
||||
| `Mz` | `SM2` |
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R SelfContainedHdf5 --output-on-failure
|
||||
h5ls -r .\out\build\windows-debug\Testing\Temporary\fesa-self-contained.h5
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
### 4.3 아직 완료되지 않은 검증
|
||||
|
||||
## 6. 구현 전에 정렬할 설계점
|
||||
- 실제 Abaqus 결과로 검증된 물리량은 변위, 반력과 요소 단면력이다.
|
||||
- Abaqus 도심 응력 CSV는 제공되지 않았다. `StressCsv`는 synthetic fixture로 schema와
|
||||
entity mapping만 검증했으며 응력은 **not yet Abaqus-qualified**다.
|
||||
- synthetic internal-force fixture도 adapter 단위 검증에 계속 사용하지만, 요소
|
||||
단면력 자체는 별도의 실제 Abaqus golden CSV와 상관성 비교가 완료됐다.
|
||||
- 내부 배포용 Release build, install tree, clean consumer smoke test와 약 100,000 DOF
|
||||
측정은 아직 실행되지 않았다. 증거 없이 완료 표시하지 않는다.
|
||||
|
||||
아래는 범위 확장이 아니라 Step 문서와 현재 타입 사이에서 테스트 전에 명시적으로
|
||||
결정해야 할 최소 계약이다.
|
||||
### 4.4 `internal-release` Step 문서의 계약 불일치
|
||||
|
||||
1. `end_node`의 소유권
|
||||
- Step 0 초안의 `BeamSectionResult`에는 `NodeId end_node`가 있지만 제시된
|
||||
`recover_beam3d2(const Beam3D2Input&, ...)` 입력에는 Node ID가 없다.
|
||||
- 가짜 ID를 만들지 않는다. recovery API가 node IDs를 받게 할지, kernel은
|
||||
end ordinal/`xi`만 반환하고 orchestration이 실제 NodeId를 붙일지 최소 설계를
|
||||
정한 뒤 테스트로 고정한다.
|
||||
`phases/internal-release/step0.md`와 `step4.md`에는 beam reference phase 이전의 문구가
|
||||
남아 있다.
|
||||
|
||||
2. 끝점 strain/resultant와 부호
|
||||
- `docs/formulation/timoshenko-beam-3d.md`의 국부 DOF, shear strain, curvature와
|
||||
`sigma_xx=E(epsilon+z*kappa_y-y*kappa_z)`를 기준으로 한다.
|
||||
- section resultant의 양의 방향과 element nodal resisting-force 방향을 혼동하지
|
||||
않는다. 끝값에 임의 절댓값이나 후처리 sign flip을 적용하지 않는다.
|
||||
- “현재 Abaqus displacement/reaction comparison”은 변위·반력·요소 단면력
|
||||
correlation으로 갱신해야 한다.
|
||||
- “내력·응력은 synthetic coverage”라는 묶음 표현은 요소 단면력과 응력을 분리해야
|
||||
한다. 요소 단면력은 real golden correlation과 synthetic adapter coverage가 모두
|
||||
있고, 응력만 synthetic adapter coverage다.
|
||||
|
||||
3. Recovery point 계약
|
||||
- `BeamSection.recovery_points`의 `(y,z)` 순서와 입력 순서를 보존한다.
|
||||
- 빈 목록의 유효성, 두 끝의 expected count와 nonfinite coordinate/result 처리를
|
||||
result validation 및 round-trip test에서 명시한다.
|
||||
- 단면 형상 정보가 없으므로 point shear/torsional stress를 추정하지 않는다.
|
||||
새 세션은 Harness 실행 전에 이 두 Step 문서와 관련 checklist 문구를 현재
|
||||
`docs/PRD.md` 8절 및 `docs/VALIDATION.md`와 일치시켜야 한다. 이 정렬은 검증 범위의
|
||||
확장이 아니라 이미 완료된 증거를 정확히 기술하는 작업이다.
|
||||
|
||||
4. Result provenance와 component metadata
|
||||
- semantic result는 Abaqus/CSV/HDF5 명칭이나 handle에 의존하지 않는다.
|
||||
- node와 element origin, local/global coordinate system, component ordering을
|
||||
중복된 문자열 상수로 흩뜨리지 않을 최소 표현을 선택한다.
|
||||
## 5. 다음 Phase: `internal-release`
|
||||
|
||||
5. HDF5 version과 reader 반환 계약
|
||||
- 현재 `1.0.0`은 required object의 의미/형상/type을 같은 major에서 바꾸지 않는
|
||||
계약이다. 완전한 schema를 무버전으로 덮어쓰지 말고 version 정책을 문서와
|
||||
reader/writer test에 함께 반영한다.
|
||||
- public reader가 `Domain` 자체를 반환할지 완전한 serialization snapshot을
|
||||
반환할지는 아키텍처 경계를 확인해 결정하되, 원본 `.inp` 경로에 기대지 않고
|
||||
모든 요구 항목을 재구성할 수 있어야 한다.
|
||||
Phase metadata는 `phases/internal-release/index.json`에 있다. Step은 순서대로 실행한다.
|
||||
|
||||
6. Diagnostics와 analysis settings
|
||||
- 현재 diagnostics는 semantic frame에 있지만 HDF5 writer가 non-empty 값을
|
||||
거부한다. stage/severity/code/message/source의 손실 없는 저장 계약을 먼저
|
||||
정한다.
|
||||
- solver settings는 실제 Phase 1 실행 설정만 저장한다. 사용하지 않는 미래
|
||||
backend/history/dynamic 설정을 빈 구조로 추가하지 않는다.
|
||||
### Step 0 — `release-checklist`
|
||||
|
||||
## 7. 아키텍처와 범위 경계
|
||||
- `docs/BUILDING.md`, `docs/INPUT_FORMAT.md`, `docs/RELEASE_CHECKLIST.md`를 작성한다.
|
||||
- PRD 8절의 각 내부 배포 기준에 고유 checklist ID를 부여한다.
|
||||
- 실제 target, preset, example과 evidence command를 CMake에서 재확인한다.
|
||||
- 미실행 Release/install/benchmark 항목을 완료 표시하지 않는다.
|
||||
- 4.4절의 stale reference 문구를 먼저 바로잡는다.
|
||||
|
||||
- `core`, `model`, `fem`, `elements`는 Abaqus, HDF5, MKL 및 TBB API에 의존하지 않는다.
|
||||
- Beam kernel에는 Abaqus output column 이름이나 CSV-specific field를 넣지 않는다.
|
||||
- `ResultDatabase`에는 HDF5 object/handle 또는 serialization 전용 type을 노출하지
|
||||
### Step 1 — `cmake-install-package`
|
||||
|
||||
- `cmake --install`로 내부 배포용 install tree를 만든다.
|
||||
- CLI, `fesa_core`, public headers, CMake package config, example, schema/input/validation
|
||||
문서와 runtime DLL inventory를 포함한다.
|
||||
- install config에 absolute build path가 남는 실패 검사를 먼저 작성한다.
|
||||
- installer, registry write, 외부 dependency 다운로드와 public ABI 약속은 범위 밖이다.
|
||||
|
||||
### Step 2 — `install-tree-smoke-test`
|
||||
|
||||
- source/build tree를 참조하지 않는 consumer configure/link test를 만든다.
|
||||
- 설치된 CLI의 `--version`, example solve와 생성 HDF5 open을 검증한다.
|
||||
- 개발 PATH의 `fesa.exe`나 source include fallback으로 결함을 숨기지 않는다.
|
||||
|
||||
### Step 3 — `phase1-scale-benchmark`
|
||||
|
||||
- 약 100,000 DOF Beam chain에서 generation/parsing, assembly, PARDISO solve,
|
||||
recovery와 HDF5 write 시간을 분리해 측정한다.
|
||||
- 재현 가능한 Windows memory metric, thread/solver 설정과 실제 DOF 수를 기록한다.
|
||||
- finite result, 평형과 정상 종료는 검사하되 임의 시간·speedup 기준은 만들지 않는다.
|
||||
|
||||
### Step 4 — `release-evidence-gate`
|
||||
|
||||
- Debug와 Release configure/build/test를 새로 실행한다.
|
||||
- test count가 0이 아닌지 확인한다.
|
||||
- Harness pytest, reference, determinism, HDF5 inspection, install-tree smoke test와 scale
|
||||
benchmark 증거를 checklist ID에 연결한다.
|
||||
- 모든 수용 조건에 현재 실행 증거가 있을 때만 Step과 Phase를 `completed`로 바꾼다.
|
||||
- 실패나 미실행 항목이 있으면 정확한 blocker를 남기고 release 완료를 선언하지 않는다.
|
||||
|
||||
## 6. 유지해야 할 경계
|
||||
|
||||
- C++20, Visual Studio 2026 MSVC v145와 Windows x64 기준을 유지한다.
|
||||
- `core`, `model`, `fem`, `elements`에 Abaqus, MKL, TBB 또는 HDF5 API를 노출하지
|
||||
않는다.
|
||||
- HDF5 resource는 adapter의 move-only RAII wrapper 내부에 둔다.
|
||||
- DofManager가 DOF와 equation mapping을 계속 단독 소유한다.
|
||||
- analysis는 production recovery API를 조율하고 수식을 복제하지 않는다.
|
||||
- 기존 parser/model validation/solver 경로를 test helper로 우회하지 않는다.
|
||||
- 실제 두 번째 구현이 생기기 전에는 generic registry나 backend hierarchy를 만들지
|
||||
- solver semantic model과 result contract에 installer 또는 serialization 전용 타입을
|
||||
추가하지 않는다.
|
||||
- dependency는 개발 환경에 사전 설치된 버전을 사용하며 package 중 다운로드하지
|
||||
않는다.
|
||||
- 이번 Phase에서 Abaqus CSV mapping, golden comparison와 물리량별 tolerance를
|
||||
선행하지 않는다. 이는 `beam-reference-qualification` 범위다.
|
||||
- installer, Release package와 validation report를 선행하지 않는다. 이는
|
||||
`internal-release` 범위다.
|
||||
- install tree는 source/build tree의 절대경로에 의존하지 않아야 한다.
|
||||
- Debug와 Release artifact/runtime을 혼합하지 않는다.
|
||||
- FESA는 단위 변환을 수행하지 않는다.
|
||||
- performance 수치를 correctness gate로 바꾸지 않는다.
|
||||
- stress를 Abaqus-qualified로 표현하지 않는다.
|
||||
- 테스트를 disable하거나 제외해 release evidence를 만들지 않는다.
|
||||
- 사용자가 명시적으로 요청하지 않은 phase 실행에서는 `--push`를 사용하지 않는다.
|
||||
|
||||
## 8. 검증된 baseline과 개발환경
|
||||
## 7. 검증 기준선과 개발환경
|
||||
|
||||
2026-08-02 기준 도구와 dependency:
|
||||
### 7.1 마지막 확인 결과
|
||||
|
||||
- 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
|
||||
2026-08-03의 beam reference qualification 완료 및 `dev` 병합 후 다음을 확인했다.
|
||||
|
||||
새 PowerShell 세션에서 configure 또는 Harness 실행 전에 설정한다. 절대경로를
|
||||
tracked CMake/Preset에 넣지 않는다.
|
||||
- `cmake --build --preset windows-debug`: 성공, 새 MSVC warning 없음
|
||||
- `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 결과로
|
||||
확대 해석하지 않는다.
|
||||
|
||||
### 7.2 Package 설정
|
||||
|
||||
새 PowerShell 세션에서 configure 전에 현재 설치 위치를 확인한다. tracked preset이나
|
||||
CMake 파일에 사용자별 절대경로를 넣지 않는다.
|
||||
|
||||
```powershell
|
||||
$env:MKL_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\mkl"
|
||||
@@ -309,148 +242,83 @@ Test-Path "$env:HDF5_DIR\hdf5-config.cmake"
|
||||
Test-Path "$env:GTest_DIR\GTestConfig.cmake"
|
||||
```
|
||||
|
||||
기본 검증 명령:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
uv run --with pytest python -m pytest -v -rs
|
||||
.\out\build\windows-debug\Debug\fesa_assembly_benchmark.exe
|
||||
```
|
||||
|
||||
HANDOFF 갱신 직전 확인한 baseline:
|
||||
|
||||
- MSVC Debug build 성공, 새 warning 없음
|
||||
- CTest 54개 중 54개 성공
|
||||
- Harness pytest 20개 중 20개 성공; 0-test 성공이 아님
|
||||
- phase focused CTest 5개 모두 성공
|
||||
- benchmark 예시:
|
||||
`elements=1000 serial_ms=488.602 parallel_ms=287.849 parallel_threads=16`
|
||||
(시간과 speedup은 환경 의존적이며 pass 조건이 아니다.)
|
||||
|
||||
CMake cache가 없거나 package 경로가 바뀐 경우에만 같은 환경 변수 세션에서 먼저
|
||||
다음을 실행한다.
|
||||
네 경로가 모두 유효한 같은 셸에서 preset을 실행한다. cache가 없거나 package 위치가
|
||||
변경됐을 때만 `--fresh` configure를 사용한다.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
`h5ls` 직접 실행에는 HDF5 DLL과 Intel `libmmd.dll`이 모두 필요하다. 시스템 PATH는
|
||||
영구 변경하지 말고 현재 셸에만 추가한다.
|
||||
Release evidence는 `internal-release` Step에서 새로 생성한다.
|
||||
|
||||
```powershell
|
||||
$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
|
||||
cmake --preset windows-release
|
||||
cmake --build --preset windows-release
|
||||
ctest --preset windows-release --output-on-failure
|
||||
```
|
||||
|
||||
## 9. Harness child 환경 주의사항
|
||||
## 8. Harness 주의사항
|
||||
|
||||
이전 Phase의 최초 Harness 실행에서 child가 WindowsApps PowerShell을 시작할 때
|
||||
access denied가 발생했고, 일부 재시도 build process가 겹치며 timeout과 stale
|
||||
blocked metadata가 잠시 남았다. 최종적으로 모든 Step output의 `exitCode`는 0이고
|
||||
phase metadata도 completed로 정리되었다.
|
||||
|
||||
현재 `C:\Users\baram\.codex\config.toml`은 원래 값인
|
||||
`[windows] sandbox = "elevated"`로 복원되어 있음을 2026-08-02에 재확인했다.
|
||||
다음 Phase에서는 먼저 표준 실행을 시도하고 같은 문제가 재현될 때만 아래 임시
|
||||
workaround를 적용한다.
|
||||
|
||||
- 기존 `scripts/execute.py`, child Codex, CMake/MSBuild process가 완전히 종료됐는지
|
||||
확인해 중복 executor를 만들지 않는다.
|
||||
- global config의 원래 값을 기록한 뒤 Harness 실행 동안만 sandbox를
|
||||
`unelevated`로 바꾼다.
|
||||
- PATH 앞에는 standalone Codex와 Windows PowerShell 5.1을 두고 WindowsApps 및
|
||||
OpenAI Codex app-bin entry를 제거한다.
|
||||
- 성공/실패와 관계없이 종료 즉시 config를 `elevated`로 복원하고 다시 읽어 확인한다.
|
||||
표준 실행 명령은 다음과 같다.
|
||||
|
||||
```powershell
|
||||
$codexReleaseBin = "C:\Users\baram\.codex\packages\standalone\releases\0.146.0-x86_64-pc-windows-msvc\bin"
|
||||
$windowsPowerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0"
|
||||
$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
|
||||
(Get-Command powershell).Source
|
||||
python scripts/execute.py internal-release
|
||||
```
|
||||
|
||||
설치 버전이나 경로가 바뀌었다면 위 절대경로를 그대로 사용하지 말고 실제 standalone
|
||||
release와 `codex-resources` 존재를 먼저 확인한다. 사용자 profile 전체나 drive root를
|
||||
`--codex-add-dir`로 허용하지 않는다.
|
||||
이전 beam reference phase에서 Harness child가 WindowsApps PowerShell을 시작하지 못해
|
||||
`CreateProcessAsUserW` 오류가 반복됐다. 동일 저장소와 preset에서 직접 실행한 수용
|
||||
명령은 성공했고, `stepN-output.json`에는 child 환경 blocker와 직접 실행 증거를
|
||||
구분해 기록했다.
|
||||
|
||||
## 10. 새 세션 시작 절차
|
||||
새 세션에서는 다음 원칙을 지킨다.
|
||||
|
||||
먼저 이 문서와 다음 파일을 읽는다.
|
||||
1. 먼저 표준 Harness 실행을 시도한다.
|
||||
2. 같은 child shell 오류가 발생하면 중복 executor를 시작하지 말고 남은 process를
|
||||
확인한다.
|
||||
3. 직접 실행한 명령과 Harness 실행 결과를 혼동하지 않고 metadata에 사실대로 적는다.
|
||||
4. 사용자 profile이나 drive root를 `--codex-add-dir`로 허용하지 않는다.
|
||||
5. global Codex 설정을 임시 변경했다면 원래 값을 기록하고 종료 즉시 복원·재확인한다.
|
||||
6. 사용자가 push를 명시하지 않은 phase 실행에는 `--push`를 추가하지 않는다.
|
||||
|
||||
```text
|
||||
phases/result-contract-completion/index.json
|
||||
phases/result-contract-completion/step0.md
|
||||
phases/result-contract-completion/step1.md
|
||||
phases/result-contract-completion/step2.md
|
||||
docs/formulation/timoshenko-beam-3d.md
|
||||
docs/HDF5_SCHEMA.md
|
||||
include/fesa/elements/beam/beam3d2.hpp
|
||||
include/fesa/model/beam_section.hpp
|
||||
include/fesa/results/result_database.hpp
|
||||
include/fesa/io/hdf5/writer.hpp
|
||||
src/fesa/elements/beam/beam3d2.cpp
|
||||
src/fesa/results/result_database.cpp
|
||||
src/fesa/analysis/linear_static_analysis.cpp
|
||||
src/fesa/io/hdf5/writer.cpp
|
||||
```
|
||||
## 9. 새 세션 시작 순서
|
||||
|
||||
그 다음 Git/package 상태와 8절 baseline을 재검증하고 다음을 실행한다.
|
||||
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를 실행한다.
|
||||
|
||||
```powershell
|
||||
python scripts/execute.py result-contract-completion
|
||||
python scripts/execute.py internal-release
|
||||
```
|
||||
|
||||
executor는 feature branch를 생성하거나 checkout하고 Step 상태와
|
||||
`stepN-output.json`을 기록한다. 사용자가 명시적으로 요청하지 않은 한 `--push`를
|
||||
사용하지 않는다.
|
||||
각 Step에서는 실패하는 검사 또는 미충족 evidence를 먼저 확인하고, 최소 변경으로
|
||||
수용 조건을 만족시킨 뒤 focused test와 전체 test를 실행한다. Step output과 phase
|
||||
metadata는 실제 명령 결과를 그대로 반영한다.
|
||||
|
||||
각 Step은 다음 순서를 지킨다.
|
||||
## 10. `internal-release` 완료 조건
|
||||
|
||||
1. Step 파일의 필수 문서와 현재 구현을 모두 읽는다.
|
||||
2. 6절의 모호한 계약을 구현 전에 명시하고 focused test로 고정한다.
|
||||
3. hand-calculated/unit/integration test가 예상한 이유로 실패함을 먼저 확인한다.
|
||||
4. 테스트를 통과시키는 최소 production code만 구현한다.
|
||||
5. focused CTest, 전체 CTest와 Harness pytest를 실행한다.
|
||||
6. Step 2에서는 public reader와 정확한 `h5ls -r` 명령으로 파일 구조를 확인한다.
|
||||
7. Step summary/output metadata와 실제 결과를 대조한다.
|
||||
8. Phase 종료 전 결과 부호, provenance, schema completeness, adapter 경계와
|
||||
round-trip 손실 여부를 독립 review한다.
|
||||
- `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 문서가
|
||||
정확히 유지함
|
||||
- 실패하거나 미실행인 증거가 없는 경우에만 내부 배포 완료 선언
|
||||
|
||||
## 11. 다음 Phase 완료 조건
|
||||
새 세션의 권장 첫 요청은 다음과 같다.
|
||||
|
||||
- `phases/result-contract-completion/index.json`의 Step 0~2가 모두 completed
|
||||
- `phases/index.json`에서 `result-contract-completion`이 completed
|
||||
- pure axial/torsion/bending/biaxial recovery의 값·끝 부호·point 순서 검증
|
||||
- section strain/resultant와 centroid/recovery-point `Sxx`의 명시적 component 및
|
||||
coordinate contract
|
||||
- Beam element result의 ID/origin/connectivity/local frame 보존과 validation
|
||||
- `LinearStaticAnalysis`가 production recovery로 complete `ResultDatabase` 생성
|
||||
- 새 version의 HDF5 하나만으로 model, analysis settings, nodal/element results와
|
||||
diagnostics 재구성
|
||||
- public reader round trip과 `h5ls -r` 구조 검증
|
||||
- focused test와 전체 CTest 통과
|
||||
- Harness pytest가 0개가 아닌 상태로 전체 통과
|
||||
- 새 MSVC warning 없음
|
||||
- 독립 review의 Critical/Important finding 해결
|
||||
- Abaqus golden/tolerance나 internal release 범위를 선행하지 않음
|
||||
- 사용자 요청 없이 원격 push 또는 `dev` 병합을 수행하지 않음
|
||||
|
||||
새 세션의 권장 첫 요청:
|
||||
|
||||
> `docs/HANDOFF.md`와 `result-contract-completion`의 index/step0~2를 읽고 현재
|
||||
> `dev` baseline, Beam recovery의 미결 계약과 Harness child 실행 조건을 확인한 뒤
|
||||
> `result-contract-completion` Phase를 시작해주세요.
|
||||
> `docs/HANDOFF.md`와 `internal-release`의 index/step0~4를 읽고 현재 `dev`
|
||||
> baseline과 beam reference 검증 범위를 확인해주세요. Step 0과 Step 4의 stale
|
||||
> reference 문구를 PRD/VALIDATION에 맞춘 뒤 `internal-release` Phase를 시작해주세요.
|
||||
|
||||
+40
-13
@@ -163,8 +163,8 @@ HDF5 결과는 다음 정보를 함께 갖는 자기완결형 파일이어야
|
||||
- 여러 재료·단면과 중첩 집합
|
||||
4. Reference 테스트
|
||||
- Abaqus/Standard 2024 B31 결과
|
||||
- 현재 캔틸레버의 변위와 반력
|
||||
- 요소 내력 및 요소 절점 단면 도심 응력 비교 계약의 synthetic CSV 검증
|
||||
- 현재 캔틸레버의 변위, 반력 및 요소 단면력
|
||||
- 요소 절점 단면 도심 응력 비교 계약의 synthetic CSV 검증
|
||||
|
||||
### 5.2 골든 데이터
|
||||
|
||||
@@ -174,8 +174,15 @@ Abaqus는 CI나 Harness에서 자동 실행하지 않는다. 별도 Abaqus 2024
|
||||
|
||||
비교 실행은 물리량과 해당 CSV 경로를 명시한다. 요청한 파일이 없으면 실패하고,
|
||||
요청하지 않은 물리량은 통과로 보고하지 않는다. 현재 `reference/cantilever beam`
|
||||
샘플은 변위와 반력만 비교한다. 요소 내력과 응력 CSV가 추가되기 전까지 해당
|
||||
reader와 비교 kernel은 synthetic CSV로 검증한다.
|
||||
샘플은 변위, 반력 및 요소 단면력을 비교한다. 요소 응력 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 식별 및 값 열:
|
||||
|
||||
@@ -185,17 +192,35 @@ CSV 식별 및 값 열:
|
||||
`SF-SF1..SF-SF3`, `SM-SM1..SM-SM3`
|
||||
- 요소 응력: `Part Instance Name`, `Element Label`, `Node Label`, `Sxx`
|
||||
|
||||
단일 Instance에서는 `Part Instance Name` 열을 생략할 수 있다. 내력은
|
||||
`SF1,SF2,SF3,SM1,SM2,SM3`을 각각 \(N,V_y,V_z,T,M_y,M_z\)로 비교한다.
|
||||
응력은 요소 절점의 단면 도심값 \(\sigma_{xx}=N/A\)를 비교한다.
|
||||
단일 Instance에서는 `Part Instance Name` 열을 생략할 수 있다. Abaqus Beam의
|
||||
단면축 \((\mathbf n_1,\mathbf n_2)\)를 FESA의 \((\mathbf e_y,\mathbf e_z)\)와
|
||||
일치시킨 입력에서 요소 내력은 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 허용오차
|
||||
|
||||
- 단위·정식화 테스트는 정규화된 엄격한 tolerance를 사용한다.
|
||||
- Abaqus 비교 기본 상대오차는 \(10^{-5}\)로 한다.
|
||||
- 영에 가까운 결과는 특성 길이, 하중 및 응력에 기반한 절대오차를 함께 사용한다.
|
||||
- formulation 또는 output 위치 차이로 별도 tolerance가 필요하면 comparison
|
||||
test 설정과 `docs/VALIDATION.md`에 근거를 기록한다.
|
||||
- FESA 단위·정식화 적합성 gate는 정규화된 엄격한 tolerance를 사용한다.
|
||||
- 정식화가 일치하는 reference 비교의 기본 상대오차는 \(10^{-5}\)로 한다.
|
||||
- Abaqus B31과 FESA Beam의 정식화가 다른 상관성 gate는 component별 RMSE와
|
||||
Relative L2를 보고한다. 물리량과 component가 다른 값을 하나의 norm으로
|
||||
혼합하지 않는다.
|
||||
- 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. 개발 워크플로우
|
||||
|
||||
@@ -235,6 +260,8 @@ CSV 식별 및 값 열:
|
||||
- 테스트 0개 수집이 아님을 확인
|
||||
- 전체 입력-해석-출력 통합 테스트 통과
|
||||
- physics sanity와 평형 잔차 기준 통과
|
||||
- 현재 Abaqus 2024 변위·반력 골든 결과의 tolerance 통과
|
||||
- FESA Beam 정식화 적합성 gate의 엄격한 tolerance 통과
|
||||
- 현재 Abaqus 2024 변위·반력·요소 단면력과의 component별 RMSE 및 Relative L2
|
||||
상관성 보고서 생성
|
||||
- 요소 내력·도심 응력 CSV adapter와 비교 kernel의 synthetic 검증 통과
|
||||
- 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,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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"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/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- `/docs/formulation/timoshenko-beam-3d.md`
|
||||
- `/reference/cantilever beam/cantilever beam.inp`
|
||||
- `/reference/cantilever beam/cantilever beam displacements.csv`
|
||||
- `/reference/cantilever beam/cantilever beam reactions.csv`
|
||||
- `/reference/cantilever beam/cantilever beam elemental forces.csv`
|
||||
- `/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`
|
||||
|
||||
## 작업
|
||||
|
||||
제공된 계층형 캔틸레버를 production pipeline으로 해석하고 현재 존재하는 변위와
|
||||
반력만 Abaqus 2024 결과와 비교한다.
|
||||
FESA 정식화 적합성과 Abaqus 결과 상관성을 별도 gate로 검증한다.
|
||||
|
||||
- `tests/reference/cantilever_reference_test.cpp`와 reference compare CLI를 먼저
|
||||
작성한다.
|
||||
- comparison request는 Instance `Part-1-1`, relative tolerance `1e-5`,
|
||||
displacement absolute scale `1e-10`, reaction absolute scale `1e-8`을 명시한다.
|
||||
- HDF5 결과와 CSV를 public adapter로 읽어 `(Instance,Node Label)`로 join한다.
|
||||
- 요청하지 않은 internal force/stress 파일을 검색하거나 pass로 보고하지 않는다.
|
||||
- equilibrium과 finite result도 함께 assertion한다.
|
||||
- Gate A는 기존 해석해, energy, rigid mode 및 equilibrium 테스트를 그대로 엄격히
|
||||
통과시킨다. `SCF=0.25`를 kernel에 추가하거나 production parser가 무시하게 하지
|
||||
않는다.
|
||||
- 원본 `cantilever beam.inp`와 세 CSV는 Abaqus provenance로 보존한다. 동일한
|
||||
기하·재료·하중과 전단강성을 사용하되 `SCF=0`인
|
||||
`reference/cantilever beam/cantilever beam fesa.inp`를 추가해 production
|
||||
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
|
||||
|
||||
```powershell
|
||||
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
|
||||
.\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-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.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-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
|
||||
```
|
||||
|
||||
## 검증 절차
|
||||
|
||||
1. reference test가 실제 오차를 보고하며 실패하는 것을 확인한다.
|
||||
2. discrepancy마다 가장 작은 analytical test를 추가한 뒤 근거 있는 kernel만 수정한다.
|
||||
3. tolerance를 넓혀 결함을 숨기지 않는다.
|
||||
4. 전체 테스트와 최대 정규화 오차를 index summary에 기록한다.
|
||||
1. RMSE, Relative L2 및 요소력 축 재배열 테스트가 각각 의도한 이유로 실패하는
|
||||
RED를 확인한다.
|
||||
2. 최소 comparison metric과 CSV adapter 변경으로 GREEN을 만든다.
|
||||
3. reference test가 production solve/HDF5/CSV/correlation 경로를 실행하고 세
|
||||
물리량의 component metric을 출력하는지 확인한다.
|
||||
4. 전체 테스트와 component별 metric 요약을 index summary에 기록한다.
|
||||
|
||||
## 금지사항
|
||||
|
||||
- reference `.inp` 또는 CSV를 수정하지 마라. 이유: 원본 golden을 보존해야 한다.
|
||||
- 미제공 내력/응력 Abaqus 검증을 통과했다고 주장하지 마라. 이유: 증거가 없다.
|
||||
- 원본 Abaqus `.inp` 또는 CSV를 수정하지 마라. 이유: 원본 golden과 formulation
|
||||
provenance를 보존해야 한다.
|
||||
- Abaqus `SCF=0.25`를 FESA가 구현하거나 무시하지 마라. 이유: 승인된 FESA
|
||||
정식화와 입력 계약을 바꾼다.
|
||||
- 미제공 응력 Abaqus 검증을 통과했다고 주장하지 마라. 이유: 증거가 없다.
|
||||
- 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
|
||||
- physics: equilibrium, symmetry, reaction, nonzero prescribed DOF
|
||||
- determinism: tested thread counts와 repeated runs
|
||||
- Abaqus: 현재 cantilever displacement/reaction의 tolerance와 maximum error
|
||||
- contract-only: synthetic internal-force/stress CSV schema와 component mapping
|
||||
- 미제공 Abaqus internal-force/stress는 `not yet Abaqus-qualified`라고 명시한다.
|
||||
- Abaqus correlation: 현재 cantilever displacement/reaction/internal-force의
|
||||
component별 RMSE와 Relative L2
|
||||
- contract-only: synthetic stress CSV schema와 component mapping
|
||||
- 미제공 Abaqus stress는 `not yet Abaqus-qualified`라고 명시한다.
|
||||
|
||||
보고서 수치를 새 test output에서 수집하며 수동 추정값을 쓰지 않는다.
|
||||
|
||||
@@ -34,18 +35,18 @@ ctest --preset windows-debug --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를 새로 실행한다.
|
||||
2. 결과를 benchmark/quantity별 표에 기록한다.
|
||||
3. synthetic coverage와 Abaqus-backed qualification을 명확히 분리한다.
|
||||
3. FESA 정식화 gate, Abaqus correlation 및 synthetic coverage를 명확히 분리한다.
|
||||
4. index summary에 보고서 경로와 test counts를 기록한다.
|
||||
|
||||
## 금지사항
|
||||
|
||||
- 실행하지 않은 결과를 보고서에 쓰지 마라. 이유: 검증 증거가 아니다.
|
||||
- Abaqus 내력/응력 qualification을 추론하지 마라. 이유: CSV가 아직 없다.
|
||||
- Abaqus 응력 qualification을 추론하지 마라. 이유: CSV가 아직 없다.
|
||||
- 실패 테스트를 제외하거나 disable하지 마라. 이유: release gate를 약화한다.
|
||||
|
||||
+2
-1
@@ -42,7 +42,8 @@
|
||||
},
|
||||
{
|
||||
"dir": "beam-reference-qualification",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"completed_at": "2026-08-03T01:48:16+0900"
|
||||
},
|
||||
{
|
||||
"dir": "internal-release",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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
|
||||
2,0,0,-2.87E-06,0,5.43E-06,0
|
||||
3,0,0,-1.09E-05,0,1.03E-05,0
|
||||
4,0,0,-2.35E-05,0,1.46E-05,0
|
||||
5,0,0,-4.00E-05,0,1.83E-05,0
|
||||
6,0,0,-6.01E-05,0,2.14E-05,0
|
||||
7,0,0,-8.29E-05,0,2.40E-05,0
|
||||
8,0,0,-1.08E-04,0,2.60E-05,0
|
||||
9,0,0,-1.35E-04,0,2.74E-05,0
|
||||
10,0,0,-1.63E-04,0,2.83E-05,0
|
||||
11,0,0,-1.92E-04,0,2.86E-05,0
|
||||
Part Instance Name, Node Label, U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3
|
||||
PART-1_1-1,1,0.000000E+00,0.000000E+00,-1.000000E-30,0.000000E+00,1.000000E-29,0.000000E+00
|
||||
PART-1_1-1,2,0.000000E+00,0.000000E+00,-2.900000E-04,0.000000E+00,5.428570E-04,0.000000E+00
|
||||
PART-1_1-1,3,0.000000E+00,0.000000E+00,-1.094290E-03,0.000000E+00,1.028570E-03,0.000000E+00
|
||||
PART-1_1-1,4,0.000000E+00,0.000000E+00,-2.355720E-03,0.000000E+00,1.457140E-03,0.000000E+00
|
||||
PART-1_1-1,5,0.000000E+00,0.000000E+00,-4.017140E-03,0.000000E+00,1.828570E-03,0.000000E+00
|
||||
PART-1_1-1,6,0.000000E+00,0.000000E+00,-6.021430E-03,0.000000E+00,2.142860E-03,0.000000E+00
|
||||
PART-1_1-1,7,0.000000E+00,0.000000E+00,-8.311430E-03,0.000000E+00,2.400000E-03,0.000000E+00
|
||||
PART-1_1-1,8,0.000000E+00,0.000000E+00,-1.083000E-02,0.000000E+00,2.600000E-03,0.000000E+00
|
||||
PART-1_1-1,9,0.000000E+00,0.000000E+00,-1.352000E-02,0.000000E+00,2.742860E-03,0.000000E+00
|
||||
PART-1_1-1,10,0.000000E+00,0.000000E+00,-1.632430E-02,0.000000E+00,2.828570E-03,0.000000E+00
|
||||
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
|
||||
** Job name: Job-1 Model name: Model-1
|
||||
** Generated by: Abaqus/CAE Learning Edition 2024
|
||||
** FESA projection of the Abaqus 2024 cantilever reference model.
|
||||
** 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
|
||||
**
|
||||
** PARTS
|
||||
**
|
||||
*Part, name=PART-1_1
|
||||
*Node
|
||||
1, 0., 0., 0.
|
||||
@@ -31,37 +29,23 @@
|
||||
10, 10, 11
|
||||
*Elset, elset=Set-1, generate
|
||||
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
|
||||
1., 0.0833333, 0., 0.0833333, 0.140833
|
||||
0.,1.,0.
|
||||
*Transverse Shear Stiffness
|
||||
6.73077e+10, 6.73077e+10, 0.
|
||||
*End Part
|
||||
**
|
||||
**
|
||||
** ASSEMBLY
|
||||
**
|
||||
*Assembly, name=Assembly
|
||||
**
|
||||
*Instance, name=PART-1_1-1, part=PART-1_1
|
||||
*End Instance
|
||||
**
|
||||
*Nset, nset=Set-3, instance=PART-1_1-1
|
||||
1,
|
||||
*Nset, nset=Set-4, instance=PART-1_1-1
|
||||
11,
|
||||
*End Assembly
|
||||
**
|
||||
** MATERIALS
|
||||
**
|
||||
*Material, name=Material-1
|
||||
*Elastic
|
||||
2.1e+11, 0.3
|
||||
**
|
||||
** BOUNDARY CONDITIONS
|
||||
**
|
||||
** Name: BC-1 Type: Displacement/Rotation
|
||||
*Boundary
|
||||
Set-3, 1, 1
|
||||
Set-3, 2, 2
|
||||
@@ -69,34 +53,9 @@ Set-3, 3, 3
|
||||
Set-3, 4, 4
|
||||
Set-3, 5, 5
|
||||
Set-3, 6, 6
|
||||
** ----------------------------------------------------------------
|
||||
**
|
||||
** STEP: Step-1
|
||||
**
|
||||
*Step, name=Step-1, nlgeom=NO
|
||||
*Static
|
||||
1., 1., 1e-05, 1.
|
||||
**
|
||||
** LOADS
|
||||
**
|
||||
** Name: Load-1 Type: Concentrated force
|
||||
*Cload
|
||||
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
|
||||
@@ -1,12 +1,12 @@
|
||||
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
|
||||
2,0,0,0,0,0,0
|
||||
3,0,0,0,0,0,0
|
||||
4,0,0,0,0,0,0
|
||||
5,0,0,0,0,0,0
|
||||
6,0,0,0,0,0,0
|
||||
7,0,0,0,0,0,0
|
||||
8,0,0,0,0,0,0
|
||||
9,0,0,0,0,0,0
|
||||
10,0,0,0,0,0,0
|
||||
11,0,0,0,0,0,0
|
||||
Part Instance Name, Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3
|
||||
PART-1_1-1,1,0.000000E+00,0.000000E+00,1.000000E+06,0.000000E+00,-1.000000E+07,0.000000E+00
|
||||
PART-1_1-1,2,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,3,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,4,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,5,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,6,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,7,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,8,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,9,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
PART-1_1-1,10,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
|
||||
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
|
||||
**
|
||||
*Part, name=Part-1
|
||||
*Part, name=PART-1_1
|
||||
*Node
|
||||
1, 0., 0., 0.
|
||||
2, 1., 0., 0.
|
||||
@@ -29,18 +29,16 @@
|
||||
8, 8, 9
|
||||
9, 9, 10
|
||||
10, 10, 11
|
||||
*Nset, nset=Set-1, generate
|
||||
1, 11, 1
|
||||
*Elset, elset=Set-1, generate
|
||||
1, 10, 1
|
||||
*Nset, nset=Set-2, generate
|
||||
1, 11, 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
|
||||
1., 0.0833333, 0., 0.0833333, 0.140833
|
||||
0.,1.,0.
|
||||
*Transverse Shear Stiffness
|
||||
6.73077e+10, 6.73077e+10, 0.25
|
||||
*End Part
|
||||
**
|
||||
**
|
||||
@@ -48,13 +46,13 @@
|
||||
**
|
||||
*Assembly, name=Assembly
|
||||
**
|
||||
*Instance, name=Part-1-1, part=Part-1
|
||||
*Instance, name=PART-1_1-1, part=PART-1_1
|
||||
*End Instance
|
||||
**
|
||||
*Nset, nset=Set-1, instance=Part-1-1
|
||||
11,
|
||||
*Nset, nset=Set-2, instance=Part-1-1
|
||||
*Nset, nset=Set-3, instance=PART-1_1-1
|
||||
1,
|
||||
*Nset, nset=Set-4, instance=PART-1_1-1
|
||||
11,
|
||||
*End Assembly
|
||||
**
|
||||
** MATERIALS
|
||||
@@ -62,6 +60,17 @@
|
||||
*Material, name=Material-1
|
||||
*Elastic
|
||||
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
|
||||
@@ -70,22 +79,11 @@
|
||||
*Static
|
||||
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
|
||||
**
|
||||
** Name: Load-1 Type: Concentrated force
|
||||
*Cload
|
||||
Set-1, 3, -10000.
|
||||
Set-4, 3, -1e+06
|
||||
**
|
||||
** OUTPUT REQUESTS
|
||||
**
|
||||
@@ -93,7 +91,12 @@ Set-1, 3, -10000.
|
||||
**
|
||||
** 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
|
||||
**
|
||||
|
||||
@@ -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
|
||||
@@ -743,3 +743,64 @@ set_property(
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${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
|
||||
|
@@ -702,7 +702,7 @@ TEST(Cload, ReportsInvalidDofAtTheDataRow) {
|
||||
TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
|
||||
const std::filesystem::path 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);
|
||||
|
||||
@@ -723,7 +723,7 @@ TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
|
||||
EXPECT_EQ(domain.step().nodal_loads.front().node, node->id);
|
||||
EXPECT_EQ(
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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