feat(result-contract-completion): step 1 — complete-result-contract
This commit is contained in:
@@ -2,23 +2,62 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
#include <fesa/core/status.hpp>
|
#include <fesa/core/status.hpp>
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
|
#include <fesa/model/entity_origin.hpp>
|
||||||
#include <fesa/model/ids.hpp>
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
|
enum class FieldCoordinateSystem { global, element_local };
|
||||||
|
|
||||||
struct NodalFrame final {
|
struct NodalFrame final {
|
||||||
|
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||||
|
FieldCoordinateSystem::global;
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
displacement_components{
|
||||||
|
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"};
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
reaction_components{
|
||||||
|
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"};
|
||||||
|
|
||||||
std::vector<NodeId> node_ids;
|
std::vector<NodeId> node_ids;
|
||||||
|
std::vector<EntityOrigin> origins;
|
||||||
std::vector<std::array<double, 6>> displacement;
|
std::vector<std::array<double, 6>> displacement;
|
||||||
std::vector<std::array<double, 6>> reaction;
|
std::vector<std::array<double, 6>> reaction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct BeamElementFrame final {
|
||||||
|
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||||
|
FieldCoordinateSystem::element_local;
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
section_strain_components{
|
||||||
|
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||||
|
"kappa_z"};
|
||||||
|
inline static constexpr std::array<std::string_view, 6>
|
||||||
|
section_force_components{
|
||||||
|
"N", "Vy", "Vz", "T", "My", "Mz"};
|
||||||
|
inline static constexpr std::string_view axial_stress_component =
|
||||||
|
"sigma_xx";
|
||||||
|
|
||||||
|
ElementId element;
|
||||||
|
EntityOrigin origin;
|
||||||
|
BeamFrame local_frame;
|
||||||
|
std::array<BeamSectionResult, 2> end_results;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ElementFrame final {
|
||||||
|
std::vector<BeamElementFrame> beams;
|
||||||
|
};
|
||||||
|
|
||||||
struct ResultFrame final {
|
struct ResultFrame final {
|
||||||
double step_time;
|
double step_time;
|
||||||
NodalFrame nodal;
|
NodalFrame nodal;
|
||||||
|
ElementFrame element;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
#include <stdexcept>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include <fesa/assembly/equation_system.hpp>
|
#include <fesa/assembly/equation_system.hpp>
|
||||||
#include <fesa/assembly/serial_assembler.hpp>
|
#include <fesa/assembly/serial_assembler.hpp>
|
||||||
#include <fesa/constraints/essential_bc.hpp>
|
#include <fesa/constraints/essential_bc.hpp>
|
||||||
|
#include <fesa/elements/beam/beam3d2.hpp>
|
||||||
#include <fesa/fem/dof_manager.hpp>
|
#include <fesa/fem/dof_manager.hpp>
|
||||||
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
||||||
|
|
||||||
@@ -34,6 +36,18 @@ AnalysisRunResult equation_failure(
|
|||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnalysisRunResult results_failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message) {
|
||||||
|
return failure({{
|
||||||
|
DiagnosticStage::results,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::nullopt,
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
NodalFrame build_nodal_frame(
|
NodalFrame build_nodal_frame(
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
const DofManager& dofs,
|
const DofManager& dofs,
|
||||||
@@ -48,9 +62,11 @@ NodalFrame build_nodal_frame(
|
|||||||
|
|
||||||
NodalFrame nodal;
|
NodalFrame nodal;
|
||||||
nodal.node_ids = std::move(node_ids);
|
nodal.node_ids = std::move(node_ids);
|
||||||
|
nodal.origins.reserve(nodal.node_ids.size());
|
||||||
nodal.displacement.reserve(nodal.node_ids.size());
|
nodal.displacement.reserve(nodal.node_ids.size());
|
||||||
nodal.reaction.reserve(nodal.node_ids.size());
|
nodal.reaction.reserve(nodal.node_ids.size());
|
||||||
for (const NodeId node_id : nodal.node_ids) {
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
|
nodal.origins.push_back(domain.node(node_id).origin);
|
||||||
std::array<double, 6> node_displacement{};
|
std::array<double, 6> node_displacement{};
|
||||||
std::array<double, 6> node_reaction{};
|
std::array<double, 6> node_reaction{};
|
||||||
for (std::size_t component = 0; component < 6; ++component) {
|
for (std::size_t component = 0; component < 6; ++component) {
|
||||||
@@ -67,6 +83,70 @@ NodalFrame build_nodal_frame(
|
|||||||
return nodal;
|
return nodal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElementFrame build_element_frame(
|
||||||
|
const Domain& domain,
|
||||||
|
const DofManager& dofs,
|
||||||
|
const std::vector<double>& displacement) {
|
||||||
|
std::vector<const BeamElement*> elements;
|
||||||
|
elements.reserve(domain.beam_elements().size());
|
||||||
|
for (const BeamElement& element : domain.beam_elements()) {
|
||||||
|
elements.push_back(&element);
|
||||||
|
}
|
||||||
|
std::ranges::sort(
|
||||||
|
elements,
|
||||||
|
{},
|
||||||
|
[](const BeamElement* element) {
|
||||||
|
return element->id.value();
|
||||||
|
});
|
||||||
|
|
||||||
|
ElementFrame frame;
|
||||||
|
frame.beams.reserve(elements.size());
|
||||||
|
for (const BeamElement* element : elements) {
|
||||||
|
const Beam3D2Input input{
|
||||||
|
{
|
||||||
|
domain.node(element->nodes[0]).position,
|
||||||
|
domain.node(element->nodes[1]).position,
|
||||||
|
},
|
||||||
|
element->nodes,
|
||||||
|
domain.material(element->material),
|
||||||
|
domain.section(element->section),
|
||||||
|
};
|
||||||
|
const BeamKernelResult kernel = compute_beam3d2(input);
|
||||||
|
if (!kernel.contribution.has_value()) {
|
||||||
|
const std::string message = kernel.diagnostics.empty()
|
||||||
|
? "Beam recovery requires a valid element input."
|
||||||
|
: kernel.diagnostics.front().message;
|
||||||
|
throw std::runtime_error{message};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<double, 12> element_displacement{};
|
||||||
|
const std::array<std::size_t, 12> full_dofs =
|
||||||
|
dofs.element_full_dofs(*element);
|
||||||
|
for (std::size_t local = 0; local < full_dofs.size(); ++local) {
|
||||||
|
element_displacement[local] = displacement[full_dofs[local]];
|
||||||
|
}
|
||||||
|
std::vector<BeamSectionResult> recovered = recover_beam3d2(
|
||||||
|
input,
|
||||||
|
element_displacement,
|
||||||
|
input.section.recovery_points);
|
||||||
|
if (recovered.size() != 2U) {
|
||||||
|
throw std::logic_error{
|
||||||
|
"Beam recovery must return exactly two end results."};
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.beams.push_back({
|
||||||
|
element->id,
|
||||||
|
element->origin,
|
||||||
|
kernel.contribution->frame,
|
||||||
|
{
|
||||||
|
std::move(recovered[0]),
|
||||||
|
std::move(recovered[1]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||||
@@ -114,6 +194,14 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
|||||||
"equation.reaction_recovery_failed", error.what());
|
"equation.reaction_recovery_failed", error.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElementFrame element;
|
||||||
|
try {
|
||||||
|
element = build_element_frame(domain, dofs, displacement);
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
return results_failure(
|
||||||
|
"results.element_recovery_failed", error.what());
|
||||||
|
}
|
||||||
|
|
||||||
ResultDatabase database{
|
ResultDatabase database{
|
||||||
"1.0.0",
|
"1.0.0",
|
||||||
{{
|
{{
|
||||||
@@ -122,6 +210,7 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
|||||||
1.0,
|
1.0,
|
||||||
build_nodal_frame(
|
build_nodal_frame(
|
||||||
domain, dofs, displacement, reaction),
|
domain, dofs, displacement, reaction),
|
||||||
|
std::move(element),
|
||||||
{},
|
{},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -1364,7 +1364,13 @@ Hdf5ModelSnapshot read_model(
|
|||||||
ResultDatabase read_database(
|
ResultDatabase read_database(
|
||||||
Hdf5Context& context,
|
Hdf5Context& context,
|
||||||
const hid_t file,
|
const hid_t file,
|
||||||
const std::string& version) {
|
const std::string& version,
|
||||||
|
const Hdf5ModelSnapshot& model) {
|
||||||
|
std::unordered_map<std::int64_t, EntityOrigin> node_origins;
|
||||||
|
for (const Hdf5NodeSnapshot& node : model.nodes) {
|
||||||
|
node_origins.emplace(node.id.value(), node.origin);
|
||||||
|
}
|
||||||
|
|
||||||
ResultDatabase database{version, {}};
|
ResultDatabase database{version, {}};
|
||||||
auto results_group = open_group(context, file, "results");
|
auto results_group = open_group(context, file, "results");
|
||||||
auto steps_group = open_group(context, results_group.get(), "steps");
|
auto steps_group = open_group(context, results_group.get(), "steps");
|
||||||
@@ -1419,10 +1425,16 @@ ResultDatabase read_database(
|
|||||||
"Nodal result datasets have inconsistent row counts.");
|
"Nodal result datasets have inconsistent row counts.");
|
||||||
}
|
}
|
||||||
frame.nodal.node_ids.reserve(node_ids.size());
|
frame.nodal.node_ids.reserve(node_ids.size());
|
||||||
|
frame.nodal.origins.reserve(node_ids.size());
|
||||||
frame.nodal.displacement.reserve(node_ids.size());
|
frame.nodal.displacement.reserve(node_ids.size());
|
||||||
frame.nodal.reaction.reserve(node_ids.size());
|
frame.nodal.reaction.reserve(node_ids.size());
|
||||||
for (std::size_t index = 0; index < node_ids.size(); ++index) {
|
for (std::size_t index = 0; index < node_ids.size(); ++index) {
|
||||||
frame.nodal.node_ids.emplace_back(node_ids[index]);
|
frame.nodal.node_ids.emplace_back(node_ids[index]);
|
||||||
|
const auto origin = node_origins.find(node_ids[index]);
|
||||||
|
frame.nodal.origins.push_back(
|
||||||
|
origin == node_origins.end()
|
||||||
|
? EntityOrigin{}
|
||||||
|
: origin->second);
|
||||||
std::array<double, 6> displacement_values{};
|
std::array<double, 6> displacement_values{};
|
||||||
std::array<double, 6> reaction_values{};
|
std::array<double, 6> reaction_values{};
|
||||||
std::copy_n(
|
std::copy_n(
|
||||||
@@ -1538,7 +1550,7 @@ Hdf5ReadResult read_hdf5_results(const std::filesystem::path& path) {
|
|||||||
}
|
}
|
||||||
Hdf5ModelSnapshot model = read_model(context, file.get());
|
Hdf5ModelSnapshot model = read_model(context, file.get());
|
||||||
ResultDatabase database =
|
ResultDatabase database =
|
||||||
read_database(context, file.get(), version);
|
read_database(context, file.get(), version, model);
|
||||||
const Status validation = validate_result_database(database);
|
const Status validation = validate_result_database(database);
|
||||||
if (!validation.succeeded) {
|
if (!validation.succeeded) {
|
||||||
file.reset();
|
file.reset();
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ void validate_nodal_frame(
|
|||||||
"Nodal IDs, displacement, and reaction fields must have "
|
"Nodal IDs, displacement, and reaction fields must have "
|
||||||
"matching sizes.");
|
"matching sizes.");
|
||||||
}
|
}
|
||||||
|
if (nodal.origins.size() != nodal.node_ids.size()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nodal_origin_size_mismatch",
|
||||||
|
"Nodal IDs and origins must have matching sizes.");
|
||||||
|
}
|
||||||
|
|
||||||
std::set<std::int64_t> node_ids;
|
std::set<std::int64_t> node_ids;
|
||||||
for (const NodeId node_id : nodal.node_ids) {
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
@@ -74,6 +80,84 @@ void validate_nodal_frame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void validate_beam_section_result(
|
||||||
|
const BeamSectionResult& result,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
if (
|
||||||
|
!std::isfinite(result.xi) ||
|
||||||
|
!is_finite(result.section_strain) ||
|
||||||
|
!is_finite(result.section_force) ||
|
||||||
|
!std::isfinite(result.centroid_sigma_xx) ||
|
||||||
|
!std::ranges::all_of(
|
||||||
|
result.sigma_xx,
|
||||||
|
[](const double value) { return std::isfinite(value); })) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nonfinite_value",
|
||||||
|
"Beam section result contains a nonfinite value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate_element_frame(
|
||||||
|
const ElementFrame& element,
|
||||||
|
const NodalFrame& nodal,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
std::set<std::int64_t> nodal_ids;
|
||||||
|
for (const NodeId node_id : nodal.node_ids) {
|
||||||
|
nodal_ids.insert(node_id.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::int64_t> element_ids;
|
||||||
|
for (const BeamElementFrame& beam : element.beams) {
|
||||||
|
if (!element_ids.insert(beam.element.value()).second) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.duplicate_element_id",
|
||||||
|
"Element frame contains duplicate element ID " +
|
||||||
|
std::to_string(beam.element.value()) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!is_finite(beam.local_frame.ex) ||
|
||||||
|
!is_finite(beam.local_frame.ey) ||
|
||||||
|
!is_finite(beam.local_frame.ez)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.nonfinite_value",
|
||||||
|
"Beam local frame contains a nonfinite component.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeamSectionResult& first = beam.end_results[0];
|
||||||
|
const BeamSectionResult& second = beam.end_results[1];
|
||||||
|
if (first.end_node == second.end_node) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.duplicate_beam_end_node",
|
||||||
|
"Beam element result contains the same node at both ends.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
first.xi != -1.0 || second.xi != 1.0 ||
|
||||||
|
!nodal_ids.contains(first.end_node.value()) ||
|
||||||
|
!nodal_ids.contains(second.end_node.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.invalid_beam_connectivity",
|
||||||
|
"Beam end results must follow (-1, +1) connectivity and "
|
||||||
|
"reference nodes in the nodal frame.");
|
||||||
|
}
|
||||||
|
if (first.sigma_xx.size() != second.sigma_xx.size()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"results.recovery_point_count_mismatch",
|
||||||
|
"Beam end results must have matching recovery-point "
|
||||||
|
"counts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_beam_section_result(first, diagnostics);
|
||||||
|
validate_beam_section_result(second, diagnostics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Status validate_result_database(const ResultDatabase& database) {
|
Status validate_result_database(const ResultDatabase& database) {
|
||||||
@@ -106,6 +190,8 @@ Status validate_result_database(const ResultDatabase& database) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_nodal_frame(frame.nodal, diagnostics);
|
validate_nodal_frame(frame.nodal, diagnostics);
|
||||||
|
validate_element_frame(
|
||||||
|
frame.element, frame.nodal, diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -598,6 +598,12 @@ add_test(
|
|||||||
--gtest_filter=ResultDatabase.*
|
--gtest_filter=ResultDatabase.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ElementFrame
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=ElementFrame.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_hdf5_results_tests
|
add_executable(fesa_hdf5_results_tests
|
||||||
integration/io/hdf5_results_test.cpp
|
integration/io/hdf5_results_test.cpp
|
||||||
)
|
)
|
||||||
@@ -674,8 +680,14 @@ add_test(
|
|||||||
--gtest_filter=StaticEquilibrium.*
|
--gtest_filter=StaticEquilibrium.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME CompleteResultContract
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_linear_static_analysis_tests>"
|
||||||
|
--gtest_filter=CompleteResultContract.*
|
||||||
|
)
|
||||||
|
|
||||||
set_property(
|
set_property(
|
||||||
TEST LinearStaticAnalysis StaticEquilibrium
|
TEST LinearStaticAnalysis StaticEquilibrium CompleteResultContract
|
||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -226,6 +226,12 @@ fesa::ResultDatabase make_database() {
|
|||||||
1.25,
|
1.25,
|
||||||
{
|
{
|
||||||
{fesa::NodeId{7}, fesa::NodeId{42}},
|
{fesa::NodeId{7}, fesa::NodeId{42}},
|
||||||
|
{
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart", "Beam-1", 1002},
|
||||||
|
fesa::EntityOrigin{
|
||||||
|
"BeamPart", "Beam-1", 1001},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
||||||
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
||||||
@@ -236,6 +242,7 @@ fesa::ResultDatabase make_database() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
|
{},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
@@ -279,6 +286,12 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
frame.nodal.node_ids,
|
frame.nodal.node_ids,
|
||||||
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.nodal.origins,
|
||||||
|
(std::vector<fesa::EntityOrigin>{
|
||||||
|
{"BeamPart", "Beam-1", 1002},
|
||||||
|
{"BeamPart", "Beam-1", 1001},
|
||||||
|
}));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
frame.nodal.displacement[0],
|
frame.nodal.displacement[0],
|
||||||
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ fesa::Domain build_axial_domain(const bool all_constrained) {
|
|||||||
2.0,
|
2.0,
|
||||||
fesa::ShearPropertySource::input,
|
fesa::ShearPropertySource::input,
|
||||||
fesa::Vec3{0.0, 1.0, 0.0},
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
{},
|
{{1.0, 2.0}, {-1.0, -2.0}},
|
||||||
});
|
});
|
||||||
builder.add_beam_element({
|
builder.add_beam_element({
|
||||||
fesa::ElementId{0},
|
fesa::ElementId{0},
|
||||||
@@ -214,4 +214,49 @@ TEST(LinearStaticAnalysis, SolvesAllConstrainedSystemWithoutPardiso) {
|
|||||||
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
|
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, PreservesOriginsConnectivityAndBeamRecovery) {
|
||||||
|
const fesa::Domain domain = build_axial_domain(false);
|
||||||
|
|
||||||
|
const fesa::AnalysisRunResult run =
|
||||||
|
fesa::LinearStaticAnalysis{}.run(domain);
|
||||||
|
|
||||||
|
ASSERT_TRUE(run.succeeded);
|
||||||
|
ASSERT_TRUE(run.results.has_value());
|
||||||
|
const fesa::ResultFrame& frame = run.results->steps[0].frames[0];
|
||||||
|
EXPECT_EQ(
|
||||||
|
frame.nodal.origins,
|
||||||
|
(std::vector<fesa::EntityOrigin>{
|
||||||
|
{"BeamPart", "Beam-1", 2},
|
||||||
|
{"BeamPart", "Beam-1", 1},
|
||||||
|
}));
|
||||||
|
|
||||||
|
ASSERT_EQ(frame.element.beams.size(), 1U);
|
||||||
|
const fesa::BeamElementFrame& beam = frame.element.beams[0];
|
||||||
|
EXPECT_EQ(beam.element, fesa::ElementId{0});
|
||||||
|
EXPECT_EQ(
|
||||||
|
beam.origin,
|
||||||
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1}));
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.x, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.y, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ex.z, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.x, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.y, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ey.z, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.x, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.y, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.local_frame.ez.z, 1.0);
|
||||||
|
EXPECT_EQ(beam.end_results[0].end_node, fesa::NodeId{20});
|
||||||
|
EXPECT_EQ(beam.end_results[1].end_node, fesa::NodeId{4});
|
||||||
|
EXPECT_DOUBLE_EQ(beam.end_results[0].xi, -1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(beam.end_results[1].xi, 1.0);
|
||||||
|
for (const fesa::BeamSectionResult& end : beam.end_results) {
|
||||||
|
EXPECT_NEAR(end.section_strain[0], 0.05, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.section_force[0], 10.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.centroid_sigma_xx, 5.0, 1.0e-12);
|
||||||
|
ASSERT_EQ(end.sigma_xx.size(), 2U);
|
||||||
|
EXPECT_NEAR(end.sigma_xx[0], 5.0, 1.0e-12);
|
||||||
|
EXPECT_NEAR(end.sigma_xx[1], 5.0, 1.0e-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ std::array<double, 6> zeros() {
|
|||||||
fesa::ResultDatabase valid_database() {
|
fesa::ResultDatabase valid_database() {
|
||||||
fesa::NodalFrame nodal{
|
fesa::NodalFrame nodal{
|
||||||
{fesa::NodeId{0}, fesa::NodeId{1}},
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
{
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 10},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 20},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
zeros(),
|
zeros(),
|
||||||
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
|
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
|
||||||
@@ -26,7 +30,39 @@ fesa::ResultDatabase valid_database() {
|
|||||||
zeros(),
|
zeros(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
fesa::ResultFrame frame{1.0, std::move(nodal), {}};
|
fesa::BeamElementFrame beam{
|
||||||
|
fesa::ElementId{3},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 30},
|
||||||
|
{
|
||||||
|
fesa::Vec3{1.0, 0.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
fesa::Vec3{0.0, 0.0, 1.0},
|
||||||
|
},
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
-1.0,
|
||||||
|
fesa::NodeId{0},
|
||||||
|
zeros(),
|
||||||
|
zeros(),
|
||||||
|
0.0,
|
||||||
|
{1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
1.0,
|
||||||
|
fesa::NodeId{1},
|
||||||
|
zeros(),
|
||||||
|
zeros(),
|
||||||
|
0.0,
|
||||||
|
{2.0},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
fesa::ResultFrame frame{
|
||||||
|
1.0,
|
||||||
|
std::move(nodal),
|
||||||
|
{{std::move(beam)}},
|
||||||
|
{},
|
||||||
|
};
|
||||||
fesa::ResultStep step{"Load", {std::move(frame)}};
|
fesa::ResultStep step{"Load", {std::move(frame)}};
|
||||||
return {"1.0.0", {std::move(step)}};
|
return {"1.0.0", {std::move(step)}};
|
||||||
}
|
}
|
||||||
@@ -72,6 +108,17 @@ TEST(NodalFrame, RejectsReactionSizeMismatch) {
|
|||||||
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
|
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, RejectsNodeOriginSizeMismatch) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0].nodal.origins.pop_back();
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
status, "results.nodal_origin_size_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(NodalFrame, RejectsDuplicateNodeId) {
|
TEST(NodalFrame, RejectsDuplicateNodeId) {
|
||||||
auto database = valid_database();
|
auto database = valid_database();
|
||||||
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
|
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
|
||||||
@@ -135,4 +182,110 @@ TEST(ResultDatabase, RejectsNonfiniteFrameTime) {
|
|||||||
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CompleteResultContract, DeclaresCoordinatesAndComponentOrdering) {
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::coordinate_system,
|
||||||
|
fesa::FieldCoordinateSystem::global);
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::displacement_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::NodalFrame::reaction_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::coordinate_system,
|
||||||
|
fesa::FieldCoordinateSystem::element_local);
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::section_strain_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||||
|
"kappa_z"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::section_force_components,
|
||||||
|
(std::array<std::string_view, 6>{
|
||||||
|
"N", "Vy", "Vz", "T", "My", "Mz"}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
fesa::BeamElementFrame::axial_stress_component,
|
||||||
|
std::string_view{"sigma_xx"});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, AcceptsFiniteConnectedBeamResults) {
|
||||||
|
const auto status = fesa::validate_result_database(valid_database());
|
||||||
|
|
||||||
|
EXPECT_TRUE(status.succeeded);
|
||||||
|
EXPECT_TRUE(status.diagnostics.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsDuplicateElementId) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& beams = database.steps[0].frames[0].element.beams;
|
||||||
|
beams.push_back(beams[0]);
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_element_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsDuplicateEndNode) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& ends = database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results;
|
||||||
|
ends[1].end_node = ends[0].end_node;
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_beam_end_node"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsWrongEndConnectivity) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[0].xi = 1.0;
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsEndNodeAbsentFromNodalFrame) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[1].end_node = fesa::NodeId{99};
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ElementFrame, RejectsNonfiniteLocalFrameAndSectionValue) {
|
||||||
|
auto database = valid_database();
|
||||||
|
auto& beam = database.steps[0].frames[0].element.beams[0];
|
||||||
|
beam.local_frame.ey.y = std::numeric_limits<double>::infinity();
|
||||||
|
beam.end_results[1].section_force[4] =
|
||||||
|
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(ElementFrame, RejectsMismatchedRecoveryPointCount) {
|
||||||
|
auto database = valid_database();
|
||||||
|
database.steps[0].frames[0]
|
||||||
|
.element.beams[0].end_results[1].sigma_xx.push_back(3.0);
|
||||||
|
|
||||||
|
const auto status = fesa::validate_result_database(database);
|
||||||
|
|
||||||
|
EXPECT_FALSE(status.succeeded);
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
status, "results.recovery_point_count_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user