diff --git a/CMakeLists.txt b/CMakeLists.txt index eaba369..2ba8079 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(fesa_core STATIC src/fesa/io/abaqus/semantic_mapper.cpp src/fesa/model/domain.cpp src/fesa/model/domain_builder.cpp + src/fesa/results/result_database.cpp src/fesa/solvers/linear/pardiso_linear_solver.cpp ) diff --git a/include/fesa/results/result_database.hpp b/include/fesa/results/result_database.hpp new file mode 100644 index 0000000..818db88 --- /dev/null +++ b/include/fesa/results/result_database.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace fesa { + +struct NodalFrame final { + std::vector node_ids; + std::vector> displacement; + std::vector> reaction; +}; + +struct ResultFrame final { + double step_time; + NodalFrame nodal; + std::vector diagnostics; +}; + +struct ResultStep final { + std::string name; + std::vector frames; +}; + +struct ResultDatabase final { + std::string schema_version; + std::vector steps; +}; + +[[nodiscard]] Status validate_result_database( + const ResultDatabase& database); + +} // namespace fesa diff --git a/src/fesa/results/result_database.cpp b/src/fesa/results/result_database.cpp new file mode 100644 index 0000000..4c71090 --- /dev/null +++ b/src/fesa/results/result_database.cpp @@ -0,0 +1,115 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace fesa { +namespace { + +void add_error( + std::vector& diagnostics, + std::string code, + std::string message) { + diagnostics.push_back({ + DiagnosticStage::results, + Severity::error, + std::move(code), + std::move(message), + std::nullopt, + }); +} + +bool is_finite(const std::array& field) { + return std::ranges::all_of( + field, + [](const double component) { + return std::isfinite(component); + }); +} + +void validate_nodal_frame( + const NodalFrame& nodal, + std::vector& diagnostics) { + if ( + nodal.displacement.size() != nodal.node_ids.size() || + nodal.reaction.size() != nodal.node_ids.size()) { + add_error( + diagnostics, + "results.nodal_size_mismatch", + "Nodal IDs, displacement, and reaction fields must have " + "matching sizes."); + } + + std::set node_ids; + for (const NodeId node_id : nodal.node_ids) { + if (!node_ids.insert(node_id.value()).second) { + add_error( + diagnostics, + "results.duplicate_node_id", + "Nodal frame contains duplicate node ID " + + std::to_string(node_id.value()) + "."); + } + } + + for (const auto& displacement : nodal.displacement) { + if (!is_finite(displacement)) { + add_error( + diagnostics, + "results.nonfinite_value", + "Nodal displacement contains a nonfinite component."); + } + } + + for (const auto& reaction : nodal.reaction) { + if (!is_finite(reaction)) { + add_error( + diagnostics, + "results.nonfinite_value", + "Nodal reaction contains a nonfinite component."); + } + } +} + +} // namespace + +Status validate_result_database(const ResultDatabase& database) { + std::vector diagnostics; + std::set step_names; + + for (const ResultStep& step : database.steps) { + if (!step_names.insert(step.name).second) { + add_error( + diagnostics, + "results.duplicate_step_name", + "Result database contains duplicate step name '" + + step.name + "'."); + } + + std::set frame_times; + for (const ResultFrame& frame : step.frames) { + if (!std::isfinite(frame.step_time)) { + add_error( + diagnostics, + "results.nonfinite_value", + "Result frame has a nonfinite step time."); + } else if (!frame_times.insert(frame.step_time).second) { + add_error( + diagnostics, + "results.duplicate_frame_time", + "Result step '" + step.name + + "' contains duplicate frame time " + + std::to_string(frame.step_time) + "."); + } + + validate_nodal_frame(frame.nodal, diagnostics); + } + } + + return {diagnostics.empty(), std::move(diagnostics)}; +} + +} // namespace fesa diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fe4c5c9..dd083b2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -375,3 +375,34 @@ set_property( PROPERTY ENVIRONMENT_MODIFICATION ${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS} ) + +add_executable(fesa_result_database_tests + unit/results/result_database_test.cpp +) + +target_compile_features(fesa_result_database_tests PRIVATE cxx_std_20) +target_compile_options( + fesa_result_database_tests + PRIVATE + /W4 + /permissive- + /EHsc +) + +target_link_libraries(fesa_result_database_tests + PRIVATE + fesa_core + GTest::gtest_main +) + +add_test( + NAME NodalFrame + COMMAND "$" + --gtest_filter=NodalFrame.* +) + +add_test( + NAME ResultDatabase + COMMAND "$" + --gtest_filter=ResultDatabase.* +) diff --git a/tests/unit/results/result_database_test.cpp b/tests/unit/results/result_database_test.cpp new file mode 100644 index 0000000..1d65dd6 --- /dev/null +++ b/tests/unit/results/result_database_test.cpp @@ -0,0 +1,138 @@ +#include + +#include +#include +#include +#include +#include + +#include + +namespace { + +std::array zeros() { + return {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; +} + +fesa::ResultDatabase valid_database() { + fesa::NodalFrame nodal{ + {fesa::NodeId{0}, fesa::NodeId{1}}, + { + zeros(), + {1.0, 2.0, 3.0, 0.1, 0.2, 0.3}, + }, + { + {-10.0, -20.0, -30.0, -1.0, -2.0, -3.0}, + zeros(), + }, + }; + fesa::ResultFrame frame{1.0, std::move(nodal), {}}; + fesa::ResultStep step{"Load", {std::move(frame)}}; + return {"1.0.0", {std::move(step)}}; +} + +bool has_diagnostic( + const fesa::Status& status, + const std::string_view code) { + return std::ranges::any_of( + status.diagnostics, + [code](const fesa::Diagnostic& diagnostic) { + return diagnostic.code == code && + diagnostic.stage == fesa::DiagnosticStage::results && + diagnostic.severity == fesa::Severity::error; + }); +} + +TEST(NodalFrame, AcceptsFiniteSixComponentFieldsWithMatchingNodeIds) { + const auto database = valid_database(); + + const auto status = fesa::validate_result_database(database); + + EXPECT_TRUE(status.succeeded); + EXPECT_TRUE(status.diagnostics.empty()); +} + +TEST(NodalFrame, RejectsDisplacementSizeMismatch) { + auto database = valid_database(); + database.steps[0].frames[0].nodal.displacement.pop_back(); + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch")); +} + +TEST(NodalFrame, RejectsReactionSizeMismatch) { + auto database = valid_database(); + database.steps[0].frames[0].nodal.reaction.pop_back(); + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch")); +} + +TEST(NodalFrame, RejectsDuplicateNodeId) { + auto database = valid_database(); + database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0}; + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.duplicate_node_id")); +} + +TEST(NodalFrame, RejectsNonfiniteDisplacement) { + auto database = valid_database(); + database.steps[0].frames[0].nodal.displacement[1][2] = + std::numeric_limits::quiet_NaN(); + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value")); +} + +TEST(NodalFrame, RejectsNonfiniteReaction) { + auto database = valid_database(); + database.steps[0].frames[0].nodal.reaction[0][4] = + std::numeric_limits::infinity(); + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value")); +} + +TEST(ResultDatabase, RejectsDuplicateStepName) { + auto database = valid_database(); + database.steps.push_back(database.steps[0]); + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.duplicate_step_name")); +} + +TEST(ResultDatabase, RejectsDuplicateFrameTimeWithinStep) { + auto database = valid_database(); + database.steps[0].frames.push_back(database.steps[0].frames[0]); + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.duplicate_frame_time")); +} + +TEST(ResultDatabase, RejectsNonfiniteFrameTime) { + auto database = valid_database(); + database.steps[0].frames[0].step_time = + std::numeric_limits::quiet_NaN(); + + const auto status = fesa::validate_result_database(database); + + EXPECT_FALSE(status.succeeded); + EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value")); +} + +} // namespace