feat(result-contract-completion): step 1 — complete-result-contract
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -12,6 +13,7 @@
|
||||
#include <fesa/assembly/equation_system.hpp>
|
||||
#include <fesa/assembly/serial_assembler.hpp>
|
||||
#include <fesa/constraints/essential_bc.hpp>
|
||||
#include <fesa/elements/beam/beam3d2.hpp>
|
||||
#include <fesa/fem/dof_manager.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(
|
||||
const Domain& domain,
|
||||
const DofManager& dofs,
|
||||
@@ -48,9 +62,11 @@ NodalFrame build_nodal_frame(
|
||||
|
||||
NodalFrame nodal;
|
||||
nodal.node_ids = std::move(node_ids);
|
||||
nodal.origins.reserve(nodal.node_ids.size());
|
||||
nodal.displacement.reserve(nodal.node_ids.size());
|
||||
nodal.reaction.reserve(nodal.node_ids.size());
|
||||
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_reaction{};
|
||||
for (std::size_t component = 0; component < 6; ++component) {
|
||||
@@ -67,6 +83,70 @@ NodalFrame build_nodal_frame(
|
||||
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
|
||||
|
||||
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||
@@ -114,6 +194,14 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||
"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{
|
||||
"1.0.0",
|
||||
{{
|
||||
@@ -122,6 +210,7 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||
1.0,
|
||||
build_nodal_frame(
|
||||
domain, dofs, displacement, reaction),
|
||||
std::move(element),
|
||||
{},
|
||||
}},
|
||||
}},
|
||||
|
||||
@@ -1364,7 +1364,13 @@ Hdf5ModelSnapshot read_model(
|
||||
ResultDatabase read_database(
|
||||
Hdf5Context& context,
|
||||
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, {}};
|
||||
auto results_group = open_group(context, file, "results");
|
||||
auto steps_group = open_group(context, results_group.get(), "steps");
|
||||
@@ -1419,10 +1425,16 @@ ResultDatabase read_database(
|
||||
"Nodal result datasets have inconsistent row counts.");
|
||||
}
|
||||
frame.nodal.node_ids.reserve(node_ids.size());
|
||||
frame.nodal.origins.reserve(node_ids.size());
|
||||
frame.nodal.displacement.reserve(node_ids.size());
|
||||
frame.nodal.reaction.reserve(node_ids.size());
|
||||
for (std::size_t index = 0; index < node_ids.size(); ++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> reaction_values{};
|
||||
std::copy_n(
|
||||
@@ -1538,7 +1550,7 @@ Hdf5ReadResult read_hdf5_results(const std::filesystem::path& path) {
|
||||
}
|
||||
Hdf5ModelSnapshot model = read_model(context, file.get());
|
||||
ResultDatabase database =
|
||||
read_database(context, file.get(), version);
|
||||
read_database(context, file.get(), version, model);
|
||||
const Status validation = validate_result_database(database);
|
||||
if (!validation.succeeded) {
|
||||
file.reset();
|
||||
|
||||
@@ -43,6 +43,12 @@ void validate_nodal_frame(
|
||||
"Nodal IDs, displacement, and reaction fields must have "
|
||||
"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;
|
||||
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
|
||||
|
||||
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_element_frame(
|
||||
frame.element, frame.nodal, diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user