feat(beam-reference-qualification): correlate Abaqus beam results
This commit is contained in:
@@ -60,7 +60,7 @@
|
||||
- 전단강성이 생략되면 \(A_{sy}=A_{sz}=5A/6\)과 `SCF=0`을 Phase 1 기본값으로
|
||||
적용한다.
|
||||
- reference 비교는 metadata 없이 요청된 물리량과 CSV 경로를 명시한다. 현재
|
||||
캔틸레버 샘플은 변위와 반력을 비교하며 요소 내력과 도심 응력 비교 루틴은
|
||||
캔틸레버 샘플은 변위, 반력 및 요소 단면력을 비교하며 도심 응력 비교 루틴은
|
||||
synthetic CSV로 검증한다.
|
||||
- 새 MSVC 빌드 경고를 추가하지 않는다.
|
||||
- 변경은 요청 범위에 한정하고 Conventional Commits 형식의 메시지를 사용한다.
|
||||
|
||||
@@ -68,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
|
||||
|
||||
+3
-1
@@ -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: 일관 단위계와 결과 좌표계
|
||||
|
||||
|
||||
@@ -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를 직접 주입해 전체 파이프라인 결함을 숨기지
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
@@ -43,6 +44,20 @@ struct ComparisonReport final {
|
||||
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;
|
||||
@@ -58,4 +73,7 @@ struct ComparisonSampleMatch final {
|
||||
[[nodiscard]] ComparisonReport compare_samples(
|
||||
std::span<const ComparisonSample> samples);
|
||||
|
||||
[[nodiscard]] CorrelationReport correlate_samples(
|
||||
std::span<const ComparisonSample> samples);
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -52,7 +52,7 @@ FESA 정식화 적합성과 Abaqus 결과 상관성을 별도 gate로 검증한
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R "ValidationComparison|ReferenceCsv" --output-on-failure
|
||||
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 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
|
||||
|
||||
@@ -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를 약화한다.
|
||||
|
||||
@@ -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
|
||||
**
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cstddef>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
@@ -439,4 +440,152 @@ ComparisonReport compare_samples(
|
||||
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;
|
||||
}
|
||||
@@ -64,6 +64,12 @@ std::vector<std::string> split_fields(const std::string_view line) {
|
||||
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,
|
||||
@@ -196,7 +202,7 @@ std::vector<std::string_view> value_columns(
|
||||
case ReferenceQuantity::reaction:
|
||||
return {"RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"};
|
||||
case ReferenceQuantity::internal_force:
|
||||
return {"SF-SF1", "SF-SF2", "SF-SF3", "SM-SM1", "SM-SM2", "SM-SM3"};
|
||||
return {"SF-SF1", "SF-SF3", "SF-SF2", "SM-SM3", "SM-SM1", "SM-SM2"};
|
||||
case ReferenceQuantity::centroid_stress:
|
||||
return {"Sxx"};
|
||||
}
|
||||
@@ -283,7 +289,8 @@ ReferenceCsvReadResult read_reference_csv(
|
||||
1U);
|
||||
}
|
||||
|
||||
const std::vector<std::string> headers = split_fields(line);
|
||||
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() ||
|
||||
@@ -340,7 +347,8 @@ ReferenceCsvReadResult read_reference_csv(
|
||||
if (trim(line).empty()) {
|
||||
continue;
|
||||
}
|
||||
const std::vector<std::string> fields = split_fields(line);
|
||||
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",
|
||||
|
||||
@@ -773,6 +773,12 @@ add_test(
|
||||
--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>"
|
||||
@@ -796,3 +802,5 @@ add_test(
|
||||
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||
--gtest_filter=StressCsv.*
|
||||
)
|
||||
|
||||
add_subdirectory(reference)
|
||||
|
||||
@@ -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
|
||||
@@ -240,6 +240,66 @@ TEST(ComparisonMetric, RejectsComponentCountMismatch) {
|
||||
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};
|
||||
|
||||
@@ -72,14 +72,14 @@ TEST(ReferenceCsv, ReadsSuppliedDisplacementsWithWhitespaceHeader) {
|
||||
EXPECT_EQ(
|
||||
result.rows.front().quantity,
|
||||
fesa::ReferenceQuantity::displacement);
|
||||
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||
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-32, 0.0, 1.0e-31, 0.0}));
|
||||
(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.92e-4);
|
||||
EXPECT_EQ(result.rows.back().values[2], -1.91857e-2);
|
||||
}
|
||||
|
||||
TEST(ReferenceCsv, ReadsSuppliedReactionsWithWhitespaceHeader) {
|
||||
@@ -90,12 +90,12 @@ TEST(ReferenceCsv, ReadsSuppliedReactionsWithWhitespaceHeader) {
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
ASSERT_EQ(result.rows.size(), 11U);
|
||||
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||
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.0e4, 0.0, -1.0e5, 0.0}));
|
||||
(std::vector<double>{0.0, 0.0, 1.0e6, 0.0, -1.0e7, 0.0}));
|
||||
}
|
||||
|
||||
TEST(InternalForceCsv, MapsAllSixComponentsAndElementEndPosition) {
|
||||
@@ -115,7 +115,26 @@ TEST(InternalForceCsv, MapsAllSixComponentsAndElementEndPosition) {
|
||||
EXPECT_EQ(*result.rows.front().position.end_node_label, 101);
|
||||
EXPECT_EQ(
|
||||
result.rows.front().values,
|
||||
(std::vector<double>{1.25, -2.5, 3.75, -4.0, 5.5, -6.25}));
|
||||
(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) {
|
||||
|
||||
Reference in New Issue
Block a user