feat(beam-reference-qualification): correlate Abaqus beam results

This commit is contained in:
KOKO\Mimi
2026-08-03 01:45:30 +09:00
parent b68f6ee143
commit 675379779f
21 changed files with 962 additions and 119 deletions
+8
View File
@@ -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
+38
View File
@@ -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
+60
View File
@@ -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};
+25 -6
View File
@@ -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) {