feat(results-and-pipeline): step 1 — minimal-hdf5-schema
This commit is contained in:
+2
-1
@@ -36,6 +36,7 @@ add_library(fesa_core STATIC
|
||||
src/fesa/fem/line2_shape.cpp
|
||||
src/fesa/io/abaqus/parser.cpp
|
||||
src/fesa/io/abaqus/semantic_mapper.cpp
|
||||
src/fesa/io/hdf5/writer.cpp
|
||||
src/fesa/model/domain.cpp
|
||||
src/fesa/model/domain_builder.cpp
|
||||
src/fesa/results/result_database.cpp
|
||||
@@ -49,7 +50,7 @@ target_include_directories(fesa_core
|
||||
|
||||
target_compile_features(fesa_core PUBLIC cxx_std_20)
|
||||
target_compile_options(fesa_core PRIVATE /W4 /permissive- /EHsc)
|
||||
target_link_libraries(fesa_core PRIVATE MKL::MKL)
|
||||
target_link_libraries(fesa_core PRIVATE MKL::MKL HDF5::HDF5)
|
||||
|
||||
add_executable(fesa
|
||||
src/fesa/cli/main.cpp
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# FESA HDF5 Schema 1.0.0
|
||||
|
||||
## 1. 범위
|
||||
|
||||
Schema `1.0.0`은 `results-and-pipeline` Phase의 최소 수직 슬라이스를 정의한다.
|
||||
파일은 활성 `Domain`의 절점, Beam 연결성, 적용된 전단면적과 그 출처, 그리고 전역
|
||||
좌표계 절점 변위·회전 및 반력·반력모멘트를 저장한다. 단위 변환은 수행하지 않는다.
|
||||
|
||||
이 버전에는 재료 전체 속성, 집합, 하중·경계조건, solver 설정, 요소 결과, history,
|
||||
reference CSV 및 진단 dataset을 저장하지 않는다. 이후 같은 major version에서
|
||||
dataset을 추가할 수 있지만 아래 required object의 의미, 형상 또는 datatype을
|
||||
변경해서는 안 된다.
|
||||
|
||||
## 2. 공통 규칙
|
||||
|
||||
- root attribute `schema_version`은 UTF-8 문자열 `1.0.0`이다.
|
||||
- 정수 dataset은 명시한 little-endian 고정폭 타입을 사용한다.
|
||||
- 실수 dataset은 IEEE 754 little-endian 64-bit 타입을 사용한다.
|
||||
- 문자열 dataset과 attribute는 UTF-8 variable-length string을 사용한다.
|
||||
- `dense_index`는 해당 dataset 행의 0-based index이며 연속적이다.
|
||||
- `internal_id`와 결과의 `node_ids`는 FESA semantic model의 nonnegative ID다.
|
||||
- flat/orphan mesh의 `part_name`과 `instance_name`은 빈 문자열이다.
|
||||
- 결과의 6개 component 순서는
|
||||
`(Ux, Uy, Uz, Rx, Ry, Rz)` 및 `(RFx, RFy, RFz, RMx, RMy, RMz)`다.
|
||||
- Step과 frame group 이름은 각각 0부터 연속된 decimal index다. 원래 Step 이름은
|
||||
Step group의 `name` attribute에 저장한다.
|
||||
|
||||
## 3. Required objects
|
||||
|
||||
```text
|
||||
/
|
||||
├── @schema_version UTF-8 = "1.0.0"
|
||||
├── model
|
||||
│ ├── nodes
|
||||
│ │ ├── dense_index uint64 [node_count]
|
||||
│ │ ├── internal_id int64 [node_count]
|
||||
│ │ ├── part_name UTF-8 [node_count]
|
||||
│ │ ├── instance_name UTF-8 [node_count]
|
||||
│ │ ├── local_label int64 [node_count]
|
||||
│ │ └── coordinates float64[node_count, 3]
|
||||
│ ├── elements
|
||||
│ │ ├── dense_index uint64 [element_count]
|
||||
│ │ ├── internal_id int64 [element_count]
|
||||
│ │ ├── connectivity uint64 [element_count, 2]
|
||||
│ │ └── section_id int64 [element_count]
|
||||
│ └── sections
|
||||
│ ├── internal_id int64 [section_count]
|
||||
│ ├── shear_area_y float64[section_count]
|
||||
│ ├── shear_area_z float64[section_count]
|
||||
│ └── shear_source uint8 [section_count]
|
||||
└── results
|
||||
└── steps
|
||||
└── <step_index>
|
||||
├── @name UTF-8
|
||||
└── frames
|
||||
└── <frame_index>
|
||||
├── @step_time float64
|
||||
└── nodal
|
||||
├── node_ids int64 [result_node_count]
|
||||
├── displacement float64[result_node_count, 6]
|
||||
└── reaction float64[result_node_count, 6]
|
||||
```
|
||||
|
||||
`model/elements/connectivity`는 `model/nodes/dense_index`를 참조한다. 따라서
|
||||
`internal_id`가 연속적이거나 Domain 저장 순서와 같다고 가정하지 않는다.
|
||||
`section_id`는 `model/sections/internal_id`를 참조한다.
|
||||
|
||||
`shear_source` 값은 다음과 같다.
|
||||
|
||||
| 값 | 의미 |
|
||||
|---:|---|
|
||||
| `0` | 입력에서 명시된 전단강성으로부터 구성한 값 (`input`) |
|
||||
| `1` | Phase 1 기본값 `Asy=Asz=5A/6`, `SCF=0` (`phase1_default`) |
|
||||
|
||||
## 4. Writer와 reader 계약
|
||||
|
||||
- writer는 쓰기 전에 `ResultDatabase` 유효성과 schema version을 검사한다.
|
||||
- schema `1.0.0`이 표현하지 않는 non-empty frame diagnostics는 파일을 만들기 전에
|
||||
`hdf5.unsupported_result_diagnostics`로 거부한다.
|
||||
- required object 생성·쓰기·flush·close 중 HDF5 오류가 발생하면 성공으로 반환하지
|
||||
않고 `DiagnosticStage::results` 오류로 변환한다.
|
||||
- reader는 schema version, required object, datatype, rank와 shape를 검사한다.
|
||||
- reader는 model internal ID의 uniqueness, finite coordinates 및 finite positive
|
||||
shear area를 검사한다.
|
||||
- writer와 reader는 모든 nodal result ID가 `model/nodes/internal_id`에 존재하는지
|
||||
검사하며, 없는 ID를 성공 결과로 반환하지 않는다.
|
||||
- reader는 nodal result를 `ResultDatabase`로, model dataset을 HDF5 adapter 전용
|
||||
read-only inspection model로 반환한다. `Domain`과 `ResultDatabase`에는 HDF5
|
||||
저장 계약을 추가하지 않는다.
|
||||
- malformed 또는 지원하지 않는 파일은 부분 database를 반환하지 않는다.
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/core/vec3.hpp>
|
||||
#include <fesa/model/beam_section.hpp>
|
||||
#include <fesa/model/domain.hpp>
|
||||
#include <fesa/model/entity_origin.hpp>
|
||||
#include <fesa/model/ids.hpp>
|
||||
#include <fesa/results/result_database.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct Hdf5NodeSnapshot final {
|
||||
std::uint64_t dense_index;
|
||||
NodeId id;
|
||||
EntityOrigin origin;
|
||||
Vec3 coordinates;
|
||||
};
|
||||
|
||||
struct Hdf5ElementSnapshot final {
|
||||
std::uint64_t dense_index;
|
||||
ElementId id;
|
||||
std::array<std::uint64_t, 2> connectivity;
|
||||
SectionId section;
|
||||
};
|
||||
|
||||
struct Hdf5SectionSnapshot final {
|
||||
SectionId id;
|
||||
double shear_area_y;
|
||||
double shear_area_z;
|
||||
ShearPropertySource shear_source;
|
||||
};
|
||||
|
||||
struct Hdf5ModelSnapshot final {
|
||||
std::vector<Hdf5NodeSnapshot> nodes;
|
||||
std::vector<Hdf5ElementSnapshot> elements;
|
||||
std::vector<Hdf5SectionSnapshot> sections;
|
||||
};
|
||||
|
||||
struct Hdf5ReadResult final {
|
||||
std::optional<ResultDatabase> database;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
std::optional<Hdf5ModelSnapshot> model;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
||||
const std::filesystem::path& path,
|
||||
const Domain& domain,
|
||||
const ResultDatabase& database);
|
||||
|
||||
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
||||
const std::filesystem::path& path);
|
||||
|
||||
} // namespace fesa
|
||||
File diff suppressed because it is too large
Load Diff
@@ -406,3 +406,46 @@ add_test(
|
||||
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||
--gtest_filter=ResultDatabase.*
|
||||
)
|
||||
|
||||
add_executable(fesa_hdf5_results_tests
|
||||
integration/io/hdf5_results_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(fesa_hdf5_results_tests PRIVATE cxx_std_20)
|
||||
target_compile_options(
|
||||
fesa_hdf5_results_tests
|
||||
PRIVATE
|
||||
/W4
|
||||
/permissive-
|
||||
/EHsc
|
||||
)
|
||||
target_compile_definitions(
|
||||
fesa_hdf5_results_tests
|
||||
PRIVATE
|
||||
FESA_TEST_BINARY_DIR="${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
|
||||
target_link_libraries(fesa_hdf5_results_tests
|
||||
PRIVATE
|
||||
fesa_core
|
||||
HDF5::HDF5
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME Hdf5
|
||||
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
|
||||
--gtest_filter=Hdf5.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ResultRoundTrip
|
||||
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
|
||||
--gtest_filter=ResultRoundTrip.*
|
||||
)
|
||||
|
||||
set_property(
|
||||
TEST Hdf5 ResultRoundTrip
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
#include <fesa/io/hdf5/writer.hpp>
|
||||
#include <fesa/model/domain_builder.hpp>
|
||||
|
||||
#include <hdf5.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
class TestHdf5Handle final {
|
||||
public:
|
||||
using CloseFunction = herr_t (*)(hid_t);
|
||||
|
||||
TestHdf5Handle(hid_t id, CloseFunction close)
|
||||
: id_{id}, close_{close} {
|
||||
if (id_ < 0) {
|
||||
throw std::runtime_error{"Failed to open test HDF5 resource."};
|
||||
}
|
||||
}
|
||||
|
||||
~TestHdf5Handle() {
|
||||
if (id_ >= 0) {
|
||||
close_(id_);
|
||||
}
|
||||
}
|
||||
|
||||
TestHdf5Handle(const TestHdf5Handle&) = delete;
|
||||
TestHdf5Handle& operator=(const TestHdf5Handle&) = delete;
|
||||
|
||||
TestHdf5Handle(TestHdf5Handle&& other) noexcept
|
||||
: id_{std::exchange(other.id_, H5I_INVALID_HID)},
|
||||
close_{other.close_} {}
|
||||
|
||||
TestHdf5Handle& operator=(TestHdf5Handle&&) = delete;
|
||||
|
||||
[[nodiscard]] hid_t get() const noexcept {
|
||||
return id_;
|
||||
}
|
||||
|
||||
private:
|
||||
hid_t id_;
|
||||
CloseFunction close_;
|
||||
};
|
||||
|
||||
std::string hdf5_path(const std::filesystem::path& path) {
|
||||
const std::u8string value = path.u8string();
|
||||
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||
}
|
||||
|
||||
void require_hdf5_status(const herr_t status) {
|
||||
if (status < 0) {
|
||||
throw std::runtime_error{"Test HDF5 mutation failed."};
|
||||
}
|
||||
}
|
||||
|
||||
void replace_step_time_with_vector(const std::filesystem::path& path) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
TestHdf5Handle frame{
|
||||
H5Gopen2(
|
||||
file.get(), "/results/steps/0/frames/0", H5P_DEFAULT),
|
||||
&H5Gclose,
|
||||
};
|
||||
require_hdf5_status(H5Adelete(frame.get(), "step_time"));
|
||||
const std::array<hsize_t, 1> dimensions{2U};
|
||||
TestHdf5Handle space{
|
||||
H5Screate_simple(1, dimensions.data(), nullptr),
|
||||
&H5Sclose,
|
||||
};
|
||||
TestHdf5Handle attribute{
|
||||
H5Acreate2(
|
||||
frame.get(),
|
||||
"step_time",
|
||||
H5T_IEEE_F64LE,
|
||||
space.get(),
|
||||
H5P_DEFAULT,
|
||||
H5P_DEFAULT),
|
||||
&H5Aclose,
|
||||
};
|
||||
const std::array<double, 2> values{1.25, 2.5};
|
||||
require_hdf5_status(
|
||||
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
||||
}
|
||||
|
||||
void write_int64_dataset(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view dataset_path,
|
||||
const std::span<const std::int64_t> values) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
const std::string owned_dataset_path{dataset_path};
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
TestHdf5Handle dataset{
|
||||
H5Dopen2(
|
||||
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
|
||||
&H5Dclose,
|
||||
};
|
||||
require_hdf5_status(H5Dwrite(
|
||||
dataset.get(),
|
||||
H5T_NATIVE_INT64,
|
||||
H5S_ALL,
|
||||
H5S_ALL,
|
||||
H5P_DEFAULT,
|
||||
values.data()));
|
||||
}
|
||||
|
||||
void write_double_dataset(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view dataset_path,
|
||||
const std::span<const double> values) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
const std::string owned_dataset_path{dataset_path};
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
TestHdf5Handle dataset{
|
||||
H5Dopen2(
|
||||
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
|
||||
&H5Dclose,
|
||||
};
|
||||
require_hdf5_status(H5Dwrite(
|
||||
dataset.get(),
|
||||
H5T_NATIVE_DOUBLE,
|
||||
H5S_ALL,
|
||||
H5S_ALL,
|
||||
H5P_DEFAULT,
|
||||
values.data()));
|
||||
}
|
||||
|
||||
std::filesystem::path round_trip_path() {
|
||||
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||
"Temporary" / "fesa-round-trip.h5";
|
||||
}
|
||||
|
||||
fesa::Domain make_domain() {
|
||||
fesa::DomainBuilder builder;
|
||||
builder.add_node({
|
||||
fesa::NodeId{42},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1001},
|
||||
fesa::Vec3{1.25, -2.5, 3.75},
|
||||
});
|
||||
builder.add_node({
|
||||
fesa::NodeId{7},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
||||
fesa::Vec3{4.0, 5.5, -6.25},
|
||||
});
|
||||
builder.add_material({
|
||||
fesa::MaterialId{6},
|
||||
"Steel",
|
||||
210.0e9,
|
||||
0.3,
|
||||
});
|
||||
builder.add_section({
|
||||
fesa::SectionId{4},
|
||||
"InputShear",
|
||||
0.04,
|
||||
1.2e-4,
|
||||
1.4e-4,
|
||||
2.0e-4,
|
||||
0.031,
|
||||
0.032,
|
||||
fesa::ShearPropertySource::input,
|
||||
fesa::Vec3{0.0, 1.0, 0.0},
|
||||
{},
|
||||
});
|
||||
builder.add_section({
|
||||
fesa::SectionId{12},
|
||||
"DefaultShear",
|
||||
0.06,
|
||||
1.5e-4,
|
||||
1.7e-4,
|
||||
2.2e-4,
|
||||
0.05,
|
||||
0.05,
|
||||
fesa::ShearPropertySource::phase1_default,
|
||||
fesa::Vec3{0.0, 1.0, 0.0},
|
||||
{},
|
||||
});
|
||||
builder.add_beam_element({
|
||||
fesa::ElementId{9},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 2001},
|
||||
{fesa::NodeId{7}, fesa::NodeId{42}},
|
||||
fesa::MaterialId{6},
|
||||
fesa::SectionId{4},
|
||||
});
|
||||
builder.add_beam_element({
|
||||
fesa::ElementId{17},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 2002},
|
||||
{fesa::NodeId{42}, fesa::NodeId{7}},
|
||||
fesa::MaterialId{6},
|
||||
fesa::SectionId{4},
|
||||
});
|
||||
builder.set_step({"Load/Case", {}, {}});
|
||||
|
||||
auto built = std::move(builder).build();
|
||||
EXPECT_TRUE(built.domain.has_value());
|
||||
EXPECT_TRUE(built.diagnostics.empty());
|
||||
return std::move(*built.domain);
|
||||
}
|
||||
|
||||
fesa::ResultDatabase make_database() {
|
||||
return {
|
||||
"1.0.0",
|
||||
{{
|
||||
"Load/Case",
|
||||
{{
|
||||
1.25,
|
||||
{
|
||||
{fesa::NodeId{7}, fesa::NodeId{42}},
|
||||
{
|
||||
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
||||
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
||||
},
|
||||
{
|
||||
{10.0, 20.0, 30.0, 40.0, 50.0, 60.0},
|
||||
{-10.0, -20.0, -30.0, -40.0, -50.0, -60.0},
|
||||
},
|
||||
},
|
||||
{},
|
||||
}},
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
bool has_results_error(
|
||||
const std::vector<fesa::Diagnostic>& diagnostics,
|
||||
const std::string_view code) {
|
||||
return std::ranges::any_of(
|
||||
diagnostics,
|
||||
[code](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.stage == fesa::DiagnosticStage::results &&
|
||||
diagnostic.severity == fesa::Severity::error &&
|
||||
diagnostic.code == code;
|
||||
});
|
||||
}
|
||||
|
||||
TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
const auto path = round_trip_path();
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::filesystem::remove(path);
|
||||
const auto domain = make_domain();
|
||||
const auto database = make_database();
|
||||
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, domain, database);
|
||||
ASSERT_TRUE(write_diagnostics.empty());
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
ASSERT_TRUE(read.diagnostics.empty());
|
||||
ASSERT_TRUE(read.database.has_value());
|
||||
ASSERT_TRUE(read.model.has_value());
|
||||
|
||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
||||
ASSERT_EQ(read.database->steps.size(), 1U);
|
||||
const auto& step = read.database->steps[0];
|
||||
EXPECT_EQ(step.name, "Load/Case");
|
||||
ASSERT_EQ(step.frames.size(), 1U);
|
||||
const auto& frame = step.frames[0];
|
||||
EXPECT_DOUBLE_EQ(frame.step_time, 1.25);
|
||||
EXPECT_EQ(
|
||||
frame.nodal.node_ids,
|
||||
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||
EXPECT_EQ(
|
||||
frame.nodal.displacement[0],
|
||||
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
||||
EXPECT_EQ(
|
||||
frame.nodal.displacement[1],
|
||||
(std::array<double, 6>{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0}));
|
||||
EXPECT_EQ(
|
||||
frame.nodal.reaction[0],
|
||||
(std::array<double, 6>{10.0, 20.0, 30.0, 40.0, 50.0, 60.0}));
|
||||
EXPECT_EQ(
|
||||
frame.nodal.reaction[1],
|
||||
(std::array<double, 6>{
|
||||
-10.0, -20.0, -30.0, -40.0, -50.0, -60.0}));
|
||||
EXPECT_TRUE(frame.diagnostics.empty());
|
||||
|
||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||
EXPECT_EQ(read.model->nodes[0].dense_index, 0U);
|
||||
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{42});
|
||||
EXPECT_EQ(
|
||||
read.model->nodes[0].origin,
|
||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1001}));
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.x, 1.25);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.y, -2.5);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.z, 3.75);
|
||||
EXPECT_EQ(read.model->nodes[1].dense_index, 1U);
|
||||
EXPECT_EQ(read.model->nodes[1].id, fesa::NodeId{7});
|
||||
EXPECT_EQ(
|
||||
read.model->nodes[1].origin,
|
||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 4.0);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, 5.5);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, -6.25);
|
||||
|
||||
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
||||
EXPECT_EQ(read.model->elements[0].id, fesa::ElementId{9});
|
||||
EXPECT_EQ(
|
||||
read.model->elements[0].connectivity,
|
||||
(std::array<std::uint64_t, 2>{1U, 0U}));
|
||||
EXPECT_EQ(read.model->elements[0].section, fesa::SectionId{4});
|
||||
EXPECT_EQ(read.model->elements[1].dense_index, 1U);
|
||||
EXPECT_EQ(read.model->elements[1].id, fesa::ElementId{17});
|
||||
EXPECT_EQ(
|
||||
read.model->elements[1].connectivity,
|
||||
(std::array<std::uint64_t, 2>{0U, 1U}));
|
||||
EXPECT_EQ(read.model->elements[1].section, fesa::SectionId{4});
|
||||
|
||||
ASSERT_EQ(read.model->sections.size(), 2U);
|
||||
EXPECT_EQ(read.model->sections[0].id, fesa::SectionId{4});
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_y, 0.031);
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_z, 0.032);
|
||||
EXPECT_EQ(
|
||||
read.model->sections[0].shear_source,
|
||||
fesa::ShearPropertySource::input);
|
||||
EXPECT_EQ(read.model->sections[1].id, fesa::SectionId{12});
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_y, 0.05);
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_z, 0.05);
|
||||
EXPECT_EQ(
|
||||
read.model->sections[1].shear_source,
|
||||
fesa::ShearPropertySource::phase1_default);
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" / "fesa-invalid.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto domain = make_domain();
|
||||
auto database = make_database();
|
||||
database.steps[0].frames[0].nodal.reaction.pop_back();
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
|
||||
EXPECT_TRUE(
|
||||
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-unsupported-diagnostics.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto domain = make_domain();
|
||||
auto database = make_database();
|
||||
database.steps[0].frames[0].diagnostics.push_back({
|
||||
fesa::DiagnosticStage::solver,
|
||||
fesa::Severity::warning,
|
||||
"solver.residual",
|
||||
"Residual diagnostic",
|
||||
std::nullopt,
|
||||
});
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.unsupported_result_diagnostics"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-missing-result-node.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto domain = make_domain();
|
||||
auto database = make_database();
|
||||
database.steps[0].frames[0].nodal.node_ids[0] = fesa::NodeId{999};
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.result_node_not_in_model"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, ReportsMissingFileAtResultsStage) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" / "fesa-missing.h5";
|
||||
std::filesystem::remove(path);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.open_failed"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonscalar-step-time.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, make_domain(), make_database());
|
||||
ASSERT_TRUE(write_diagnostics.empty());
|
||||
replace_step_time_with_vector(path);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-invalid-result-node.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, make_domain(), make_database());
|
||||
ASSERT_TRUE(write_diagnostics.empty());
|
||||
const std::array<std::int64_t, 2> node_ids{999, 42};
|
||||
write_int64_dataset(
|
||||
path,
|
||||
"/results/steps/0/frames/0/nodal/node_ids",
|
||||
node_ids);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(has_results_error(
|
||||
read.diagnostics, "hdf5.result_node_not_in_model"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-node-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
const std::array<std::int64_t, 2> ids{42, 42};
|
||||
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-element-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
const std::array<std::int64_t, 2> ids{9, 9};
|
||||
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-section-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
const std::array<std::int64_t, 2> ids{4, 4};
|
||||
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonfinite-coordinates.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
const std::array<double, 6> coordinates{
|
||||
std::numeric_limits<double>::quiet_NaN(),
|
||||
-2.5,
|
||||
3.75,
|
||||
4.0,
|
||||
5.5,
|
||||
-6.25,
|
||||
};
|
||||
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonfinite-shear-area.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
const std::array<double, 2> shear_areas{
|
||||
std::numeric_limits<double>::infinity(),
|
||||
0.05,
|
||||
};
|
||||
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonpositive-shear-area.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
||||
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user