feat(beam-reference-qualification): step 1 — reference-csv-adapters

This commit is contained in:
KOKO\Mimi
2026-08-02 03:33:06 +09:00
parent 2df90b95f8
commit 4f96719663
7 changed files with 740 additions and 0 deletions
+24
View File
@@ -746,6 +746,7 @@ set_property(
add_executable(fesa_validation_comparison_tests
unit/validation/comparison_test.cpp
unit/validation/reference_csv_test.cpp
)
target_compile_features(
@@ -754,6 +755,11 @@ target_compile_features(
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
@@ -772,3 +778,21 @@ add_test(
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.*
)
+3
View File
@@ -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
1 Part Instance Name Element Label Node Label SF-SF1 SF-SF2 SF-SF3 SM-SM1 SM-SM2 SM-SM3
2 Part-1-1 501 101 1.25 -2.5 3.75 -4.0 5.5 -6.25
3 Part-1-1 501 102 7.0 8.0 9.0 10.0 11.0 12.0
+3
View File
@@ -0,0 +1,3 @@
Element Label, Node Label, Sxx
501, 101, 42.5
501, 102, -17.25
1 Element Label Node Label Sxx
2 501 101 42.5
3 501 102 -17.25
@@ -0,0 +1,235 @@
#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");
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}));
EXPECT_EQ(result.rows.back().position.entity_label, 11);
EXPECT_EQ(result.rows.back().values[2], -1.92e-4);
}
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");
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}));
}
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, -2.5, 3.75, -4.0, 5.5, -6.25}));
}
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