feat(results-and-pipeline): step 0 — result-database
This commit is contained in:
@@ -38,6 +38,7 @@ add_library(fesa_core STATIC
|
|||||||
src/fesa/io/abaqus/semantic_mapper.cpp
|
src/fesa/io/abaqus/semantic_mapper.cpp
|
||||||
src/fesa/model/domain.cpp
|
src/fesa/model/domain.cpp
|
||||||
src/fesa/model/domain_builder.cpp
|
src/fesa/model/domain_builder.cpp
|
||||||
|
src/fesa/results/result_database.cpp
|
||||||
src/fesa/solvers/linear/pardiso_linear_solver.cpp
|
src/fesa/solvers/linear/pardiso_linear_solver.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/core/status.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct NodalFrame final {
|
||||||
|
std::vector<NodeId> node_ids;
|
||||||
|
std::vector<std::array<double, 6>> displacement;
|
||||||
|
std::vector<std::array<double, 6>> reaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResultFrame final {
|
||||||
|
double step_time;
|
||||||
|
NodalFrame nodal;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResultStep final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<ResultFrame> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResultDatabase final {
|
||||||
|
std::string schema_version;
|
||||||
|
std::vector<ResultStep> steps;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] Status validate_result_database(
|
||||||
|
const ResultDatabase& database);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#include <fesa/results/result_database.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void add_error(
|
||||||
|
std::vector<Diagnostic>& 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<double, 6>& field) {
|
||||||
|
return std::ranges::all_of(
|
||||||
|
field,
|
||||||
|
[](const double component) {
|
||||||
|
return std::isfinite(component);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate_nodal_frame(
|
||||||
|
const NodalFrame& nodal,
|
||||||
|
std::vector<Diagnostic>& 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<std::int64_t> 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<Diagnostic> diagnostics;
|
||||||
|
std::set<std::string> 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<double> 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
|
||||||
@@ -375,3 +375,34 @@ set_property(
|
|||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${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 "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=NodalFrame.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ResultDatabase
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=ResultDatabase.*
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <limits>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <fesa/results/result_database.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::array<double, 6> 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<double>::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<double>::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<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user