fix(result-contract-completion): enforce complete result provenance
This commit is contained in:
+6
-2
@@ -18,7 +18,11 @@ infer compatibility from a version prefix.
|
|||||||
|
|
||||||
- Root attributes are variable-length UTF-8 strings:
|
- Root attributes are variable-length UTF-8 strings:
|
||||||
`schema_version="2.0.0"`, `fesa_version`, and
|
`schema_version="2.0.0"`, `fesa_version`, and
|
||||||
`unit_policy="consistent_input_units_no_conversion"`.
|
`unit_policy="consistent_input_units_no_conversion"`, `input_source`, and
|
||||||
|
`input_fingerprint`. `input_source` is the UTF-8 path supplied to the solve
|
||||||
|
request. `input_fingerprint` is `fnv1a64:` followed by the 16 lowercase
|
||||||
|
hexadecimal digits of FNV-1a 64 over the original input bytes; it is a
|
||||||
|
reproducibility identifier, not a cryptographic integrity guarantee.
|
||||||
- Integer datasets use the stated little-endian fixed-width type. Floating
|
- Integer datasets use the stated little-endian fixed-width type. Floating
|
||||||
datasets use IEEE 754 little-endian `float64`. Strings are variable-length
|
datasets use IEEE 754 little-endian `float64`. Strings are variable-length
|
||||||
UTF-8.
|
UTF-8.
|
||||||
@@ -32,7 +36,7 @@ infer compatibility from a version prefix.
|
|||||||
attributes where listed. `components` is a comma-separated ordered list.
|
attributes where listed. `components` is a comma-separated ordered list.
|
||||||
- Step and frame group names are contiguous decimal indices beginning at zero.
|
- Step and frame group names are contiguous decimal indices beginning at zero.
|
||||||
Phase 1 requires exactly one analysis step and one result step with the same
|
Phase 1 requires exactly one analysis step and one result step with the same
|
||||||
name.
|
name, and exactly one result frame in that step.
|
||||||
|
|
||||||
## 3. Required objects
|
## 3. Required objects
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
@@ -12,6 +13,7 @@ namespace fesa {
|
|||||||
struct ParseDeckResult final {
|
struct ParseDeckResult final {
|
||||||
std::optional<ParsedDeck> deck;
|
std::optional<ParsedDeck> deck;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
std::string input_fingerprint;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] ParseDeckResult parse_deck(
|
[[nodiscard]] ParseDeckResult parse_deck(
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ struct Hdf5MetadataSnapshot final {
|
|||||||
std::string schema_version;
|
std::string schema_version;
|
||||||
std::string fesa_version;
|
std::string fesa_version;
|
||||||
std::string unit_policy;
|
std::string unit_policy;
|
||||||
|
std::string input_source;
|
||||||
|
std::string input_fingerprint;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hdf5InputIdentity final {
|
||||||
|
std::string source;
|
||||||
|
std::string fingerprint;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5SolverSettingsSnapshot final {
|
struct Hdf5SolverSettingsSnapshot final {
|
||||||
@@ -86,7 +93,8 @@ struct Hdf5ReadResult final {
|
|||||||
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
const ResultDatabase& database);
|
const ResultDatabase& database,
|
||||||
|
const Hdf5InputIdentity& input_identity);
|
||||||
|
|
||||||
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
||||||
const std::filesystem::path& path);
|
const std::filesystem::path& path);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <fesa/analysis/run_solver.hpp>
|
#include <fesa/analysis/run_solver.hpp>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <fesa/io/abaqus/parser.hpp>
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
@@ -8,6 +9,14 @@
|
|||||||
#include <fesa/io/hdf5/writer.hpp>
|
#include <fesa/io/hdf5/writer.hpp>
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string path_utf8(const std::filesystem::path& path) {
|
||||||
|
const std::u8string value = path.u8string();
|
||||||
|
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
||||||
ParseDeckResult parsed = parse_deck(request.input_path);
|
ParseDeckResult parsed = parse_deck(request.input_path);
|
||||||
@@ -20,13 +29,18 @@ AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
|||||||
return {false, std::nullopt, std::move(mapped.diagnostics)};
|
return {false, std::nullopt, std::move(mapped.diagnostics)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Hdf5InputIdentity identity{
|
||||||
|
path_utf8(request.input_path),
|
||||||
|
parsed.input_fingerprint,
|
||||||
|
};
|
||||||
|
|
||||||
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
|
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
|
||||||
if (!run.succeeded || !run.results.has_value()) {
|
if (!run.succeeded || !run.results.has_value()) {
|
||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Diagnostic> write_diagnostics = write_hdf5(
|
std::vector<Diagnostic> write_diagnostics = write_hdf5(
|
||||||
request.output_path, *mapped.domain, *run.results);
|
request.output_path, *mapped.domain, *run.results, identity);
|
||||||
if (!write_diagnostics.empty()) {
|
if (!write_diagnostics.empty()) {
|
||||||
return {false, std::nullopt, std::move(write_diagnostics)};
|
return {false, std::nullopt, std::move(write_diagnostics)};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <iterator>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -50,6 +53,20 @@ std::string uppercase_ascii(std::string value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string input_fingerprint(const std::string_view bytes) {
|
||||||
|
std::uint64_t fingerprint = 14695981039346656037ULL;
|
||||||
|
for (const char byte : bytes) {
|
||||||
|
fingerprint ^=
|
||||||
|
static_cast<std::uint8_t>(static_cast<unsigned char>(byte));
|
||||||
|
fingerprint *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream encoded;
|
||||||
|
encoded << "fnv1a64:" << std::hex << std::setfill('0')
|
||||||
|
<< std::setw(16) << fingerprint;
|
||||||
|
return encoded.str();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> split_fields(const std::string_view value) {
|
std::vector<std::string> split_fields(const std::string_view value) {
|
||||||
std::vector<std::string> fields;
|
std::vector<std::string> fields;
|
||||||
std::size_t first = 0;
|
std::size_t first = 0;
|
||||||
@@ -374,14 +391,26 @@ ParseDeckResult missing_parameter(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||||
std::ifstream input{path, std::ios::binary};
|
std::ifstream file{path, std::ios::binary};
|
||||||
if (!input) {
|
if (!file) {
|
||||||
return failure(
|
return failure(
|
||||||
DiagnosticStage::io,
|
DiagnosticStage::io,
|
||||||
"abaqus.io.open_failed",
|
"abaqus.io.open_failed",
|
||||||
"Unable to open Abaqus input file.",
|
"Unable to open Abaqus input file.",
|
||||||
SourceLocation{path, 0U, 0U});
|
SourceLocation{path, 0U, 0U});
|
||||||
}
|
}
|
||||||
|
const std::string source_bytes{
|
||||||
|
std::istreambuf_iterator<char>{file},
|
||||||
|
std::istreambuf_iterator<char>{},
|
||||||
|
};
|
||||||
|
if (file.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"abaqus.io.read_failed",
|
||||||
|
"Failed while reading Abaqus input file.",
|
||||||
|
SourceLocation{path, 0U, 0U});
|
||||||
|
}
|
||||||
|
std::istringstream input{source_bytes};
|
||||||
|
|
||||||
ParsedDeck deck;
|
ParsedDeck deck;
|
||||||
Scope scope = Scope::global;
|
Scope scope = Scope::global;
|
||||||
@@ -846,7 +875,11 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
*current_step_source);
|
*current_step_source);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {std::move(deck), {}};
|
return {
|
||||||
|
std::move(deck),
|
||||||
|
{},
|
||||||
|
input_fingerprint(source_bytes),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
+237
-78
@@ -1,6 +1,8 @@
|
|||||||
#include <fesa/io/hdf5/writer.hpp>
|
#include <fesa/io/hdf5/writer.hpp>
|
||||||
|
|
||||||
#include <fesa/core/version.hpp>
|
#include <fesa/core/version.hpp>
|
||||||
|
#include <fesa/fem/beam_frame.hpp>
|
||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
#include <hdf5.h>
|
#include <hdf5.h>
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <ranges>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -33,6 +36,45 @@ constexpr std::string_view solver_matrix_type =
|
|||||||
constexpr std::string_view solver_constraint_method =
|
constexpr std::string_view solver_constraint_method =
|
||||||
"essential_dof_elimination";
|
"essential_dof_elimination";
|
||||||
constexpr std::string_view solver_assembly = "deterministic_serial";
|
constexpr std::string_view solver_assembly = "deterministic_serial";
|
||||||
|
constexpr std::string_view fingerprint_prefix = "fnv1a64:";
|
||||||
|
|
||||||
|
[[nodiscard]] bool nearly_equal(
|
||||||
|
const double left,
|
||||||
|
const double right) noexcept {
|
||||||
|
const double scale = std::max({1.0, std::abs(left), std::abs(right)});
|
||||||
|
return std::abs(left - right) <=
|
||||||
|
256.0 * std::numeric_limits<double>::epsilon() * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool same_frame(
|
||||||
|
const BeamFrame& left,
|
||||||
|
const BeamFrame& right) noexcept {
|
||||||
|
return nearly_equal(left.ex.x, right.ex.x) &&
|
||||||
|
nearly_equal(left.ex.y, right.ex.y) &&
|
||||||
|
nearly_equal(left.ex.z, right.ex.z) &&
|
||||||
|
nearly_equal(left.ey.x, right.ey.x) &&
|
||||||
|
nearly_equal(left.ey.y, right.ey.y) &&
|
||||||
|
nearly_equal(left.ey.z, right.ey.z) &&
|
||||||
|
nearly_equal(left.ez.x, right.ez.x) &&
|
||||||
|
nearly_equal(left.ez.y, right.ez.y) &&
|
||||||
|
nearly_equal(left.ez.z, right.ez.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool valid_input_identity(
|
||||||
|
const Hdf5InputIdentity& identity) {
|
||||||
|
if (
|
||||||
|
identity.source.empty() ||
|
||||||
|
identity.fingerprint.size() != fingerprint_prefix.size() + 16U ||
|
||||||
|
!identity.fingerprint.starts_with(fingerprint_prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return std::ranges::all_of(
|
||||||
|
identity.fingerprint.substr(fingerprint_prefix.size()),
|
||||||
|
[](const char value) {
|
||||||
|
return (value >= '0' && value <= '9') ||
|
||||||
|
(value >= 'a' && value <= 'f');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class Hdf5Error final : public std::runtime_error {
|
class Hdf5Error final : public std::runtime_error {
|
||||||
public:
|
public:
|
||||||
@@ -322,6 +364,171 @@ std::optional<std::int64_t> find_unknown_result_node(
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<Diagnostic> validate_result_contract(
|
||||||
|
const Domain& domain,
|
||||||
|
const ResultDatabase& database) {
|
||||||
|
if (
|
||||||
|
database.steps.size() != 1U ||
|
||||||
|
database.steps[0].name != domain.step().name) {
|
||||||
|
return error_diagnostic(
|
||||||
|
"hdf5.analysis_result_mismatch",
|
||||||
|
"HDF5 schema 2.0.0 requires one result step matching the "
|
||||||
|
"Domain step.");
|
||||||
|
}
|
||||||
|
if (database.steps[0].frames.size() != 1U) {
|
||||||
|
return error_diagnostic(
|
||||||
|
"hdf5.incomplete_result_frame",
|
||||||
|
"HDF5 schema 2.0.0 requires exactly one result frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_set<std::int64_t> model_node_ids;
|
||||||
|
std::unordered_map<std::int64_t, const Node*> model_nodes;
|
||||||
|
for (const Node& node : domain.nodes()) {
|
||||||
|
model_node_ids.insert(node.id.value());
|
||||||
|
model_nodes.emplace(node.id.value(), &node);
|
||||||
|
}
|
||||||
|
if (const auto unknown =
|
||||||
|
find_unknown_result_node(database, model_node_ids)) {
|
||||||
|
return error_diagnostic(
|
||||||
|
"hdf5.result_node_not_in_model",
|
||||||
|
"Result node " + std::to_string(*unknown) +
|
||||||
|
" is not present in the serialized model.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_map<std::int64_t, const BeamElement*> model_elements;
|
||||||
|
for (const BeamElement& element : domain.beam_elements()) {
|
||||||
|
model_elements.emplace(element.id.value(), &element);
|
||||||
|
}
|
||||||
|
for (const ResultFrame& frame : database.steps[0].frames) {
|
||||||
|
for (std::size_t index = 0;
|
||||||
|
index < frame.nodal.node_ids.size(); ++index) {
|
||||||
|
const auto found =
|
||||||
|
model_nodes.find(frame.nodal.node_ids[index].value());
|
||||||
|
if (
|
||||||
|
found == model_nodes.end() ||
|
||||||
|
frame.nodal.origins[index] != found->second->origin) {
|
||||||
|
return error_diagnostic(
|
||||||
|
"hdf5.result_provenance_mismatch",
|
||||||
|
"Nodal result provenance does not match the model.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const BeamElementFrame& beam : frame.element.beams) {
|
||||||
|
const auto found = model_elements.find(beam.element.value());
|
||||||
|
if (found == model_elements.end()) {
|
||||||
|
return error_diagnostic(
|
||||||
|
"hdf5.result_element_not_in_model",
|
||||||
|
"Beam result references an element not present in "
|
||||||
|
"the serialized model.");
|
||||||
|
}
|
||||||
|
const BeamElement& element = *found->second;
|
||||||
|
const BeamSection& section = domain.section(element.section);
|
||||||
|
const BeamFrameResult expected_frame = make_beam_frame(
|
||||||
|
domain.node(element.nodes[0]).position,
|
||||||
|
domain.node(element.nodes[1]).position,
|
||||||
|
section.orientation);
|
||||||
|
if (
|
||||||
|
beam.origin != element.origin ||
|
||||||
|
beam.end_results[0].end_node != element.nodes[0] ||
|
||||||
|
beam.end_results[1].end_node != element.nodes[1] ||
|
||||||
|
beam.end_results[0].sigma_xx.size() !=
|
||||||
|
section.recovery_points.size() ||
|
||||||
|
!expected_frame.frame.has_value() ||
|
||||||
|
!same_frame(beam.local_frame, *expected_frame.frame)) {
|
||||||
|
return error_diagnostic(
|
||||||
|
"hdf5.result_element_mismatch",
|
||||||
|
"Beam result provenance, connectivity, local frame, "
|
||||||
|
"or recovery points do not match the model.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Result-database validation guarantees unique IDs. The unknown-ID
|
||||||
|
// checks above plus equal counts therefore imply exact coverage.
|
||||||
|
if (
|
||||||
|
frame.nodal.node_ids.size() != model_nodes.size() ||
|
||||||
|
frame.element.beams.size() != model_elements.size()) {
|
||||||
|
return error_diagnostic(
|
||||||
|
"hdf5.incomplete_result_frame",
|
||||||
|
"Every result frame must contain exactly one nodal row "
|
||||||
|
"per model node and one Beam row per model element.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Domain rebuild_domain(
|
||||||
|
const Hdf5ModelSnapshot& model,
|
||||||
|
const Hdf5AnalysisSnapshot& analysis) {
|
||||||
|
DomainBuilder builder;
|
||||||
|
std::vector<std::optional<NodeId>> node_ids(model.nodes.size());
|
||||||
|
for (const Hdf5NodeSnapshot& node : model.nodes) {
|
||||||
|
if (
|
||||||
|
node.dense_index >= node_ids.size() ||
|
||||||
|
node_ids[node.dense_index].has_value()) {
|
||||||
|
fail(
|
||||||
|
"hdf5.invalid_model_data",
|
||||||
|
"Serialized node dense indices are invalid.");
|
||||||
|
}
|
||||||
|
node_ids[node.dense_index] = node.id;
|
||||||
|
builder.add_node({node.id, node.origin, node.coordinates});
|
||||||
|
}
|
||||||
|
for (const IsotropicElastic& material : model.materials) {
|
||||||
|
builder.add_material(material);
|
||||||
|
}
|
||||||
|
for (const Hdf5SectionSnapshot& section : model.sections) {
|
||||||
|
builder.add_section({
|
||||||
|
section.id,
|
||||||
|
section.name,
|
||||||
|
section.area,
|
||||||
|
section.iy,
|
||||||
|
section.iz,
|
||||||
|
section.torsion_j,
|
||||||
|
section.shear_area_y,
|
||||||
|
section.shear_area_z,
|
||||||
|
section.shear_source,
|
||||||
|
section.orientation,
|
||||||
|
section.recovery_points,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const Hdf5ElementSnapshot& element : model.elements) {
|
||||||
|
if (
|
||||||
|
element.connectivity[0] >= node_ids.size() ||
|
||||||
|
element.connectivity[1] >= node_ids.size() ||
|
||||||
|
!node_ids[element.connectivity[0]].has_value() ||
|
||||||
|
!node_ids[element.connectivity[1]].has_value()) {
|
||||||
|
fail(
|
||||||
|
"hdf5.invalid_model_data",
|
||||||
|
"Serialized element connectivity is invalid.");
|
||||||
|
}
|
||||||
|
builder.add_beam_element({
|
||||||
|
element.id,
|
||||||
|
element.origin,
|
||||||
|
{
|
||||||
|
*node_ids[element.connectivity[0]],
|
||||||
|
*node_ids[element.connectivity[1]],
|
||||||
|
},
|
||||||
|
element.material,
|
||||||
|
element.section,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const NodeSet& set : model.node_sets) {
|
||||||
|
builder.add_node_set(set);
|
||||||
|
}
|
||||||
|
for (const ElementSet& set : model.element_sets) {
|
||||||
|
builder.add_element_set(set);
|
||||||
|
}
|
||||||
|
builder.set_step(analysis.step);
|
||||||
|
|
||||||
|
DomainBuildResult built = std::move(builder).build();
|
||||||
|
if (!built.domain.has_value()) {
|
||||||
|
std::string message = "Serialized model violates Domain rules.";
|
||||||
|
if (!built.diagnostics.empty()) {
|
||||||
|
message += " " + built.diagnostics.front().code + ": " +
|
||||||
|
built.diagnostics.front().message;
|
||||||
|
}
|
||||||
|
fail("hdf5.invalid_model_data", std::move(message));
|
||||||
|
}
|
||||||
|
return std::move(*built.domain);
|
||||||
|
}
|
||||||
|
|
||||||
void write_string_attribute(
|
void write_string_attribute(
|
||||||
Hdf5Context& context,
|
Hdf5Context& context,
|
||||||
const hid_t parent,
|
const hid_t parent,
|
||||||
@@ -2724,7 +2931,8 @@ ResultDatabase read_database(
|
|||||||
std::vector<Diagnostic> write_hdf5(
|
std::vector<Diagnostic> write_hdf5(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
const ResultDatabase& database) {
|
const ResultDatabase& database,
|
||||||
|
const Hdf5InputIdentity& input_identity) {
|
||||||
const Status validation = validate_result_database(database);
|
const Status validation = validate_result_database(database);
|
||||||
if (!validation.succeeded) {
|
if (!validation.succeeded) {
|
||||||
return validation.diagnostics;
|
return validation.diagnostics;
|
||||||
@@ -2734,68 +2942,14 @@ std::vector<Diagnostic> write_hdf5(
|
|||||||
"hdf5.unsupported_schema",
|
"hdf5.unsupported_schema",
|
||||||
"HDF5 writer supports schema version 2.0.0 only.")};
|
"HDF5 writer supports schema version 2.0.0 only.")};
|
||||||
}
|
}
|
||||||
if (
|
if (!valid_input_identity(input_identity)) {
|
||||||
database.steps.size() != 1U ||
|
|
||||||
database.steps[0].name != domain.step().name) {
|
|
||||||
return {error_diagnostic(
|
return {error_diagnostic(
|
||||||
"hdf5.analysis_result_mismatch",
|
"hdf5.invalid_input_identity",
|
||||||
"HDF5 schema 2.0.0 requires one result step matching the "
|
"HDF5 schema 2.0.0 requires an input source and a lowercase "
|
||||||
"Domain step.")};
|
"FNV-1a 64-bit fingerprint.")};
|
||||||
}
|
}
|
||||||
std::unordered_set<std::int64_t> model_node_ids;
|
if (const auto contract = validate_result_contract(domain, database)) {
|
||||||
std::unordered_map<std::int64_t, const Node*> model_nodes;
|
return {*contract};
|
||||||
for (const Node& node : domain.nodes()) {
|
|
||||||
model_node_ids.insert(node.id.value());
|
|
||||||
model_nodes.emplace(node.id.value(), &node);
|
|
||||||
}
|
|
||||||
if (const auto unknown =
|
|
||||||
find_unknown_result_node(database, model_node_ids)) {
|
|
||||||
return {error_diagnostic(
|
|
||||||
"hdf5.result_node_not_in_model",
|
|
||||||
"Result node " + std::to_string(*unknown) +
|
|
||||||
" is not present in the serialized model.")};
|
|
||||||
}
|
|
||||||
std::unordered_map<std::int64_t, const BeamElement*> model_elements;
|
|
||||||
for (const BeamElement& element : domain.beam_elements()) {
|
|
||||||
model_elements.emplace(element.id.value(), &element);
|
|
||||||
}
|
|
||||||
for (const ResultStep& step : database.steps) {
|
|
||||||
for (const ResultFrame& frame : step.frames) {
|
|
||||||
for (std::size_t index = 0;
|
|
||||||
index < frame.nodal.node_ids.size(); ++index) {
|
|
||||||
const auto found =
|
|
||||||
model_nodes.find(frame.nodal.node_ids[index].value());
|
|
||||||
if (
|
|
||||||
found == model_nodes.end() ||
|
|
||||||
frame.nodal.origins[index] != found->second->origin) {
|
|
||||||
return {error_diagnostic(
|
|
||||||
"hdf5.result_provenance_mismatch",
|
|
||||||
"Nodal result provenance does not match the model.")};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const BeamElementFrame& beam : frame.element.beams) {
|
|
||||||
const auto found = model_elements.find(beam.element.value());
|
|
||||||
if (found == model_elements.end()) {
|
|
||||||
return {error_diagnostic(
|
|
||||||
"hdf5.result_element_not_in_model",
|
|
||||||
"Beam result references an element not present in "
|
|
||||||
"the serialized model.")};
|
|
||||||
}
|
|
||||||
const BeamElement& element = *found->second;
|
|
||||||
const BeamSection& section = domain.section(element.section);
|
|
||||||
if (
|
|
||||||
beam.origin != element.origin ||
|
|
||||||
beam.end_results[0].end_node != element.nodes[0] ||
|
|
||||||
beam.end_results[1].end_node != element.nodes[1] ||
|
|
||||||
beam.end_results[0].sigma_xx.size() !=
|
|
||||||
section.recovery_points.size()) {
|
|
||||||
return {error_diagnostic(
|
|
||||||
"hdf5.result_element_mismatch",
|
|
||||||
"Beam result provenance, connectivity, or recovery "
|
|
||||||
"points do not match the model.")};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Hdf5Context context;
|
Hdf5Context context;
|
||||||
@@ -2819,6 +2973,13 @@ std::vector<Diagnostic> write_hdf5(
|
|||||||
context, file.get(), "fesa_version", version());
|
context, file.get(), "fesa_version", version());
|
||||||
write_string_attribute(
|
write_string_attribute(
|
||||||
context, file.get(), "unit_policy", unit_policy);
|
context, file.get(), "unit_policy", unit_policy);
|
||||||
|
write_string_attribute(
|
||||||
|
context, file.get(), "input_source", input_identity.source);
|
||||||
|
write_string_attribute(
|
||||||
|
context,
|
||||||
|
file.get(),
|
||||||
|
"input_fingerprint",
|
||||||
|
input_identity.fingerprint);
|
||||||
write_model(context, file.get(), domain);
|
write_model(context, file.get(), domain);
|
||||||
write_analysis(context, file.get(), domain);
|
write_analysis(context, file.get(), domain);
|
||||||
write_results(context, file.get(), database);
|
write_results(context, file.get(), database);
|
||||||
@@ -2862,39 +3023,37 @@ Hdf5ReadResult read_hdf5_results(const std::filesystem::path& path) {
|
|||||||
version,
|
version,
|
||||||
read_string_attribute(context, file.get(), "fesa_version"),
|
read_string_attribute(context, file.get(), "fesa_version"),
|
||||||
read_string_attribute(context, file.get(), "unit_policy"),
|
read_string_attribute(context, file.get(), "unit_policy"),
|
||||||
|
read_string_attribute(context, file.get(), "input_source"),
|
||||||
|
read_string_attribute(
|
||||||
|
context, file.get(), "input_fingerprint"),
|
||||||
};
|
};
|
||||||
if (metadata.unit_policy != unit_policy) {
|
if (metadata.unit_policy != unit_policy) {
|
||||||
fail(
|
fail(
|
||||||
"hdf5.read_failed",
|
"hdf5.read_failed",
|
||||||
"HDF5 unit policy is not supported.");
|
"HDF5 unit policy is not supported.");
|
||||||
}
|
}
|
||||||
|
if (!valid_input_identity({
|
||||||
|
metadata.input_source,
|
||||||
|
metadata.input_fingerprint,
|
||||||
|
})) {
|
||||||
|
fail(
|
||||||
|
"hdf5.invalid_input_identity",
|
||||||
|
"Serialized input identity is invalid.");
|
||||||
|
}
|
||||||
Hdf5ModelSnapshot model = read_model(context, file.get());
|
Hdf5ModelSnapshot model = read_model(context, file.get());
|
||||||
Hdf5AnalysisSnapshot analysis =
|
Hdf5AnalysisSnapshot analysis =
|
||||||
read_analysis(context, file.get(), model);
|
read_analysis(context, file.get(), model);
|
||||||
|
const Domain domain = rebuild_domain(model, analysis);
|
||||||
ResultDatabase database =
|
ResultDatabase database =
|
||||||
read_database(context, file.get(), version, model);
|
read_database(context, file.get(), version, model);
|
||||||
if (
|
|
||||||
database.steps.size() != 1U ||
|
|
||||||
database.steps[0].name != analysis.step.name) {
|
|
||||||
fail(
|
|
||||||
"hdf5.invalid_result_data",
|
|
||||||
"Result step does not match the analysis step.");
|
|
||||||
}
|
|
||||||
const Status validation = validate_result_database(database);
|
const Status validation = validate_result_database(database);
|
||||||
if (!validation.succeeded) {
|
if (!validation.succeeded) {
|
||||||
file.reset();
|
file.reset();
|
||||||
return {std::nullopt, validation.diagnostics, std::nullopt};
|
return {std::nullopt, validation.diagnostics, std::nullopt};
|
||||||
}
|
}
|
||||||
std::unordered_set<std::int64_t> model_node_ids;
|
if (const auto contract =
|
||||||
for (const Hdf5NodeSnapshot& node : model.nodes) {
|
validate_result_contract(domain, database)) {
|
||||||
model_node_ids.insert(node.id.value());
|
fail(contract->code, contract->message);
|
||||||
}
|
|
||||||
if (const auto unknown =
|
|
||||||
find_unknown_result_node(database, model_node_ids)) {
|
|
||||||
fail(
|
|
||||||
"hdf5.result_node_not_in_model",
|
|
||||||
"Result node " + std::to_string(*unknown) +
|
|
||||||
" is not present in the serialized model.");
|
|
||||||
}
|
}
|
||||||
file.reset();
|
file.reset();
|
||||||
if (!context.close_error.empty()) {
|
if (!context.close_error.empty()) {
|
||||||
|
|||||||
@@ -604,6 +604,12 @@ add_test(
|
|||||||
--gtest_filter=ElementFrame.*
|
--gtest_filter=ElementFrame.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ResultContractMetadata
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||||
|
--gtest_filter=CompleteResultContract.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_hdf5_results_tests
|
add_executable(fesa_hdf5_results_tests
|
||||||
integration/io/hdf5_results_test.cpp
|
integration/io/hdf5_results_test.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -98,6 +98,63 @@ void replace_step_time_with_vector(const std::filesystem::path& path) {
|
|||||||
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void delete_link(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view link_path) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_link_path{link_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(
|
||||||
|
H5Ldelete(file.get(), owned_link_path.c_str(), H5P_DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void copy_object(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view source_path,
|
||||||
|
const std::string_view target_path) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_source_path{source_path};
|
||||||
|
const std::string owned_target_path{target_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(H5Ocopy(
|
||||||
|
file.get(),
|
||||||
|
owned_source_path.c_str(),
|
||||||
|
file.get(),
|
||||||
|
owned_target_path.c_str(),
|
||||||
|
H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_double_attribute(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view object_path,
|
||||||
|
const std::string_view attribute_name,
|
||||||
|
const double value) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_object_path{object_path};
|
||||||
|
const std::string owned_attribute_name{attribute_name};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle object{
|
||||||
|
H5Oopen(file.get(), owned_object_path.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Oclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle attribute{
|
||||||
|
H5Aopen(object.get(), owned_attribute_name.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Aclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(
|
||||||
|
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value));
|
||||||
|
}
|
||||||
|
|
||||||
void write_int64_dataset(
|
void write_int64_dataset(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const std::string_view dataset_path,
|
const std::string_view dataset_path,
|
||||||
@@ -146,6 +203,30 @@ void write_double_dataset(
|
|||||||
values.data()));
|
values.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void write_uint8_dataset(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view dataset_path,
|
||||||
|
const std::span<const std::uint8_t> values) {
|
||||||
|
const std::string encoded_path = hdf5_path(path);
|
||||||
|
const std::string owned_dataset_path{dataset_path};
|
||||||
|
TestHdf5Handle file{
|
||||||
|
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
&H5Fclose,
|
||||||
|
};
|
||||||
|
TestHdf5Handle dataset{
|
||||||
|
H5Dopen2(
|
||||||
|
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
|
||||||
|
&H5Dclose,
|
||||||
|
};
|
||||||
|
require_hdf5_status(H5Dwrite(
|
||||||
|
dataset.get(),
|
||||||
|
H5T_NATIVE_UINT8,
|
||||||
|
H5S_ALL,
|
||||||
|
H5S_ALL,
|
||||||
|
H5P_DEFAULT,
|
||||||
|
values.data()));
|
||||||
|
}
|
||||||
|
|
||||||
void write_root_string_attribute(
|
void write_root_string_attribute(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const std::string_view name,
|
const std::string_view name,
|
||||||
@@ -176,7 +257,15 @@ std::filesystem::path self_contained_path() {
|
|||||||
"Temporary" / "fesa-self-contained.h5";
|
"Temporary" / "fesa-self-contained.h5";
|
||||||
}
|
}
|
||||||
|
|
||||||
fesa::Domain make_domain() {
|
const fesa::Hdf5InputIdentity& test_input_identity() {
|
||||||
|
static const fesa::Hdf5InputIdentity identity{
|
||||||
|
"beam model.inp",
|
||||||
|
"fnv1a64:0123456789abcdef",
|
||||||
|
};
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Domain make_domain(const bool add_unreported_node = false) {
|
||||||
fesa::DomainBuilder builder;
|
fesa::DomainBuilder builder;
|
||||||
builder.add_node({
|
builder.add_node({
|
||||||
fesa::NodeId{42},
|
fesa::NodeId{42},
|
||||||
@@ -186,8 +275,15 @@ fesa::Domain make_domain() {
|
|||||||
builder.add_node({
|
builder.add_node({
|
||||||
fesa::NodeId{7},
|
fesa::NodeId{7},
|
||||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
||||||
fesa::Vec3{4.0, 5.5, -6.25},
|
fesa::Vec3{2.25, -2.5, 3.75},
|
||||||
});
|
});
|
||||||
|
if (add_unreported_node) {
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{99},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1003},
|
||||||
|
fesa::Vec3{8.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
}
|
||||||
builder.add_material({
|
builder.add_material({
|
||||||
fesa::MaterialId{6},
|
fesa::MaterialId{6},
|
||||||
"Steel",
|
"Steel",
|
||||||
@@ -257,8 +353,13 @@ fesa::Domain make_domain() {
|
|||||||
return std::move(*built.domain);
|
return std::move(*built.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fesa::BeamSectionResult make_end_result(
|
||||||
|
double xi,
|
||||||
|
fesa::NodeId node,
|
||||||
|
double offset);
|
||||||
|
|
||||||
fesa::ResultDatabase make_database() {
|
fesa::ResultDatabase make_database() {
|
||||||
return {
|
fesa::ResultDatabase database{
|
||||||
"2.0.0",
|
"2.0.0",
|
||||||
{{
|
{{
|
||||||
"Load/Case",
|
"Load/Case",
|
||||||
@@ -286,6 +387,35 @@ fesa::ResultDatabase make_database() {
|
|||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
|
database.steps[0].frames[0].element.beams = {
|
||||||
|
{
|
||||||
|
fesa::ElementId{9},
|
||||||
|
{"BeamPart", "Beam-1", 2001},
|
||||||
|
{
|
||||||
|
{-1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, -1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
make_end_result(-1.0, fesa::NodeId{7}, 0.0),
|
||||||
|
make_end_result(1.0, fesa::NodeId{42}, 100.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fesa::ElementId{17},
|
||||||
|
{"BeamPart", "Beam-1", 2002},
|
||||||
|
{
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
make_end_result(-1.0, fesa::NodeId{42}, 200.0),
|
||||||
|
make_end_result(1.0, fesa::NodeId{7}, 300.0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
fesa::BeamSectionResult make_end_result(
|
fesa::BeamSectionResult make_end_result(
|
||||||
@@ -319,34 +449,6 @@ fesa::BeamSectionResult make_end_result(
|
|||||||
fesa::ResultDatabase make_complete_database() {
|
fesa::ResultDatabase make_complete_database() {
|
||||||
fesa::ResultDatabase database = make_database();
|
fesa::ResultDatabase database = make_database();
|
||||||
auto& frame = database.steps[0].frames[0];
|
auto& frame = database.steps[0].frames[0];
|
||||||
frame.element.beams = {
|
|
||||||
{
|
|
||||||
fesa::ElementId{9},
|
|
||||||
{"BeamPart", "Beam-1", 2001},
|
|
||||||
{
|
|
||||||
{1.0, 0.0, 0.0},
|
|
||||||
{0.0, 1.0, 0.0},
|
|
||||||
{0.0, 0.0, 1.0},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
make_end_result(-1.0, fesa::NodeId{7}, 0.0),
|
|
||||||
make_end_result(1.0, fesa::NodeId{42}, 100.0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fesa::ElementId{17},
|
|
||||||
{"BeamPart", "Beam-1", 2002},
|
|
||||||
{
|
|
||||||
{-1.0, 0.0, 0.0},
|
|
||||||
{0.0, 1.0, 0.0},
|
|
||||||
{0.0, 0.0, -1.0},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
make_end_result(-1.0, fesa::NodeId{42}, 200.0),
|
|
||||||
make_end_result(1.0, fesa::NodeId{7}, 300.0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
frame.diagnostics = {
|
frame.diagnostics = {
|
||||||
{
|
{
|
||||||
fesa::DiagnosticStage::solver,
|
fesa::DiagnosticStage::solver,
|
||||||
@@ -385,8 +487,8 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
const auto domain = make_domain();
|
const auto domain = make_domain();
|
||||||
const auto database = make_database();
|
const auto database = make_database();
|
||||||
|
|
||||||
const auto write_diagnostics =
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, domain, database);
|
path, domain, database, test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
|
|
||||||
const auto read = fesa::read_hdf5_results(path);
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
@@ -439,9 +541,9 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
read.model->nodes[1].origin,
|
read.model->nodes[1].origin,
|
||||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 4.0);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 2.25);
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, 5.5);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, -2.5);
|
||||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, -6.25);
|
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, 3.75);
|
||||||
|
|
||||||
ASSERT_EQ(read.model->elements.size(), 2U);
|
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||||
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
||||||
@@ -477,9 +579,12 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
|||||||
std::filesystem::create_directories(path.parent_path());
|
std::filesystem::create_directories(path.parent_path());
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_complete_database())
|
path,
|
||||||
.empty());
|
make_domain(),
|
||||||
|
make_complete_database(),
|
||||||
|
test_input_identity())
|
||||||
|
.empty());
|
||||||
|
|
||||||
const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(path);
|
const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(path);
|
||||||
ASSERT_TRUE(read.diagnostics.empty());
|
ASSERT_TRUE(read.diagnostics.empty());
|
||||||
@@ -493,6 +598,10 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
|||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
read.metadata->unit_policy,
|
read.metadata->unit_policy,
|
||||||
"consistent_input_units_no_conversion");
|
"consistent_input_units_no_conversion");
|
||||||
|
EXPECT_EQ(read.metadata->input_source, "beam model.inp");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_fingerprint,
|
||||||
|
"fnv1a64:0123456789abcdef");
|
||||||
|
|
||||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||||
ASSERT_EQ(read.model->elements.size(), 2U);
|
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||||
@@ -547,7 +656,7 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
|||||||
const auto& frame = read.database->steps[0].frames[0];
|
const auto& frame = read.database->steps[0].frames[0];
|
||||||
ASSERT_EQ(frame.element.beams.size(), 2U);
|
ASSERT_EQ(frame.element.beams.size(), 2U);
|
||||||
EXPECT_EQ(frame.element.beams[0].element, fesa::ElementId{9});
|
EXPECT_EQ(frame.element.beams[0].element, fesa::ElementId{9});
|
||||||
EXPECT_DOUBLE_EQ(frame.element.beams[0].local_frame.ex.x, 1.0);
|
EXPECT_DOUBLE_EQ(frame.element.beams[0].local_frame.ex.x, -1.0);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
frame.element.beams[0].end_results[0].end_node,
|
frame.element.beams[0].end_results[0].end_node,
|
||||||
fesa::NodeId{7});
|
fesa::NodeId{7});
|
||||||
@@ -568,6 +677,73 @@ TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
|||||||
EXPECT_FALSE(frame.diagnostics[1].source.has_value());
|
EXPECT_FALSE(frame.diagnostics[1].source.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsMissingResultFrameBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-result-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames.clear();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsIncompleteNodalCoverageBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-incomplete-nodal-results.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
const auto database = make_complete_database();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(true), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsIncompleteBeamCoverageBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-incomplete-beam-results.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames[0].element.beams.pop_back();
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelfContainedHdf5, RejectsFiniteLocalFrameThatDisagreesWithModel) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-wrong-local-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
auto database = make_complete_database();
|
||||||
|
database.steps[0].frames[0].element.beams[0].local_frame = {
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0},
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.result_element_mismatch"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
|
TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" / "fesa-schema-1.h5";
|
"Testing" / "Temporary" / "fesa-schema-1.h5";
|
||||||
@@ -575,19 +751,34 @@ TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
|
|||||||
auto database = make_database();
|
auto database = make_database();
|
||||||
database.schema_version = "1.0.0";
|
database.schema_version = "1.0.0";
|
||||||
|
|
||||||
const auto diagnostics =
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), database);
|
path, make_domain(), database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
|
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
|
||||||
EXPECT_FALSE(std::filesystem::exists(path));
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsMissingInputIdentityBeforeWriting) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-input-identity.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
const auto diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), {"", ""});
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
diagnostics, "hdf5.invalid_input_identity"));
|
||||||
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsUnlistedMinorSchemaVersion) {
|
TEST(Hdf5, RejectsUnlistedMinorSchemaVersion) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" / "fesa-schema-2-1.h5";
|
"Testing" / "Temporary" / "fesa-schema-2-1.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
write_root_string_attribute(path, "schema_version", "2.1.0");
|
write_root_string_attribute(path, "schema_version", "2.1.0");
|
||||||
|
|
||||||
const auto read = fesa::read_hdf5_results(path);
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
@@ -608,7 +799,8 @@ TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
|||||||
auto database = make_database();
|
auto database = make_database();
|
||||||
database.steps[0].frames[0].nodal.reaction.pop_back();
|
database.steps[0].frames[0].nodal.reaction.pop_back();
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(
|
EXPECT_TRUE(
|
||||||
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
||||||
@@ -630,7 +822,8 @@ TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
|
|||||||
std::nullopt,
|
std::nullopt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
ASSERT_TRUE(diagnostics.empty());
|
ASSERT_TRUE(diagnostics.empty());
|
||||||
|
|
||||||
@@ -653,9 +846,14 @@ TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
|||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto domain = make_domain();
|
const auto domain = make_domain();
|
||||||
auto database = make_database();
|
auto database = make_database();
|
||||||
database.steps[0].frames[0].nodal.node_ids[0] = fesa::NodeId{999};
|
auto& nodal = database.steps[0].frames[0].nodal;
|
||||||
|
nodal.node_ids.push_back(fesa::NodeId{999});
|
||||||
|
nodal.origins.push_back({"BeamPart", "Beam-1", 1999});
|
||||||
|
nodal.displacement.push_back({});
|
||||||
|
nodal.reaction.push_back({});
|
||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics =
|
||||||
|
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||||
|
|
||||||
EXPECT_TRUE(has_results_error(
|
EXPECT_TRUE(has_results_error(
|
||||||
diagnostics, "hdf5.result_node_not_in_model"));
|
diagnostics, "hdf5.result_node_not_in_model"));
|
||||||
@@ -679,8 +877,8 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonscalar-step-time.h5";
|
"fesa-nonscalar-step-time.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto write_diagnostics =
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database());
|
path, make_domain(), make_database(), test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
replace_step_time_with_vector(path);
|
replace_step_time_with_vector(path);
|
||||||
|
|
||||||
@@ -691,19 +889,88 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
|||||||
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedResultWithoutRequiredFrame) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-missing-serialized-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
delete_link(path, "/results/steps/0/frames/0");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedResultWithExtraFrame) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-extra-serialized-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
copy_object(
|
||||||
|
path,
|
||||||
|
"/results/steps/0/frames/0",
|
||||||
|
"/results/steps/0/frames/1");
|
||||||
|
write_double_attribute(
|
||||||
|
path, "/results/steps/0/frames/1", "step_time", 2.5);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsInvalidSerializedInputFingerprint) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-invalid-input-fingerprint.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
write_root_string_attribute(
|
||||||
|
path, "input_fingerprint", "sha256:not-the-contract");
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_FALSE(read.metadata.has_value());
|
||||||
|
EXPECT_FALSE(read.analysis.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.invalid_input_identity"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-invalid-result-node.h5";
|
"fesa-invalid-result-node.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
const auto write_diagnostics =
|
auto database = make_database();
|
||||||
fesa::write_hdf5(path, make_domain(), make_database());
|
auto& nodal = database.steps[0].frames[0].nodal;
|
||||||
|
nodal.node_ids.push_back(fesa::NodeId{99});
|
||||||
|
nodal.origins.push_back({"BeamPart", "Beam-1", 1003});
|
||||||
|
nodal.displacement.push_back({});
|
||||||
|
nodal.reaction.push_back({});
|
||||||
|
const auto write_diagnostics = fesa::write_hdf5(
|
||||||
|
path, make_domain(true), database, test_input_identity());
|
||||||
ASSERT_TRUE(write_diagnostics.empty());
|
ASSERT_TRUE(write_diagnostics.empty());
|
||||||
const std::array<std::int64_t, 2> node_ids{999, 42};
|
const std::array<std::int64_t, 3> node_ids{42, 7, 100};
|
||||||
write_int64_dataset(
|
write_int64_dataset(path, "/model/nodes/internal_id", node_ids);
|
||||||
path,
|
|
||||||
"/results/steps/0/frames/0/nodal/node_ids",
|
|
||||||
node_ids);
|
|
||||||
|
|
||||||
const auto read = fesa::read_hdf5_results(path);
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
@@ -718,8 +985,9 @@ TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-node-ids.h5";
|
"fesa-duplicate-node-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{42, 42};
|
const std::array<std::int64_t, 2> ids{42, 42};
|
||||||
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
||||||
|
|
||||||
@@ -736,8 +1004,9 @@ TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-element-ids.h5";
|
"fesa-duplicate-element-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{9, 9};
|
const std::array<std::int64_t, 2> ids{9, 9};
|
||||||
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
||||||
|
|
||||||
@@ -754,8 +1023,9 @@ TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-duplicate-section-ids.h5";
|
"fesa-duplicate-section-ids.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<std::int64_t, 2> ids{4, 4};
|
const std::array<std::int64_t, 2> ids{4, 4};
|
||||||
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
||||||
|
|
||||||
@@ -772,15 +1042,16 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonfinite-coordinates.h5";
|
"fesa-nonfinite-coordinates.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 6> coordinates{
|
const std::array<double, 6> coordinates{
|
||||||
std::numeric_limits<double>::quiet_NaN(),
|
std::numeric_limits<double>::quiet_NaN(),
|
||||||
-2.5,
|
-2.5,
|
||||||
3.75,
|
3.75,
|
||||||
4.0,
|
2.25,
|
||||||
5.5,
|
-2.5,
|
||||||
-6.25,
|
3.75,
|
||||||
};
|
};
|
||||||
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||||
|
|
||||||
@@ -792,13 +1063,128 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
|||||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedZeroLengthElement) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-zero-length-element.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 6> coordinates{
|
||||||
|
1.25, -2.5, 3.75, 1.25, -2.5, 3.75};
|
||||||
|
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedOrientationParallelToElement) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-parallel-orientation.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 6> orientations{
|
||||||
|
1.0, 0.0, 0.0, 0.0, 1.0, 0.0};
|
||||||
|
write_double_dataset(
|
||||||
|
path, "/model/sections/orientation", orientations);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsDuplicateSerializedNodeOrigins) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-duplicate-node-origins.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<std::int64_t, 2> labels{1001, 1001};
|
||||||
|
write_int64_dataset(path, "/model/nodes/local_label", labels);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsDuplicateSerializedBoundaryConditions) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-duplicate-boundary-conditions.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<std::int64_t, 2> node_ids{42, 42};
|
||||||
|
const std::array<std::uint8_t, 2> dofs{1, 1};
|
||||||
|
write_int64_dataset(
|
||||||
|
path,
|
||||||
|
"/analysis/steps/0/boundary_conditions/node_ids",
|
||||||
|
node_ids);
|
||||||
|
write_uint8_dataset(
|
||||||
|
path, "/analysis/steps/0/boundary_conditions/dofs", dofs);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Hdf5, RejectsSerializedLocalFrameThatDisagreesWithModel) {
|
||||||
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
|
"Testing" / "Temporary" /
|
||||||
|
"fesa-serialized-wrong-local-frame.h5";
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
|
const std::array<double, 18> local_frames{
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
};
|
||||||
|
write_double_dataset(
|
||||||
|
path,
|
||||||
|
"/results/steps/0/frames/0/element/beam/local_frame",
|
||||||
|
local_frames);
|
||||||
|
|
||||||
|
const auto read = fesa::read_hdf5_results(path);
|
||||||
|
|
||||||
|
EXPECT_FALSE(read.database.has_value());
|
||||||
|
EXPECT_FALSE(read.model.has_value());
|
||||||
|
EXPECT_TRUE(has_results_error(
|
||||||
|
read.diagnostics, "hdf5.result_element_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonfinite-shear-area.h5";
|
"fesa-nonfinite-shear-area.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 2> shear_areas{
|
const std::array<double, 2> shear_areas{
|
||||||
std::numeric_limits<double>::infinity(),
|
std::numeric_limits<double>::infinity(),
|
||||||
0.05,
|
0.05,
|
||||||
@@ -818,8 +1204,9 @@ TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
|
|||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-nonpositive-shear-area.h5";
|
"fesa-nonpositive-shear-area.h5";
|
||||||
std::filesystem::remove(path);
|
std::filesystem::remove(path);
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::write_hdf5(
|
||||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
path, make_domain(), make_database(), test_input_identity())
|
||||||
|
.empty());
|
||||||
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
||||||
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ std::string quote(const std::filesystem::path& path) {
|
|||||||
return '"' + path.string() + '"';
|
return '"' + path.string() + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string path_utf8(const std::filesystem::path& path) {
|
||||||
|
const std::u8string value = path.u8string();
|
||||||
|
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||||
|
}
|
||||||
|
|
||||||
std::string read_text(const std::filesystem::path& path) {
|
std::string read_text(const std::filesystem::path& path) {
|
||||||
std::ifstream input{path, std::ios::binary};
|
std::ifstream input{path, std::ios::binary};
|
||||||
return {
|
return {
|
||||||
@@ -93,8 +98,15 @@ TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
|
|||||||
fesa::read_hdf5_results(output.path());
|
fesa::read_hdf5_results(output.path());
|
||||||
ASSERT_TRUE(read.database.has_value());
|
ASSERT_TRUE(read.database.has_value());
|
||||||
ASSERT_TRUE(read.model.has_value());
|
ASSERT_TRUE(read.model.has_value());
|
||||||
|
ASSERT_TRUE(read.metadata.has_value());
|
||||||
EXPECT_TRUE(read.diagnostics.empty());
|
EXPECT_TRUE(read.diagnostics.empty());
|
||||||
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_source,
|
||||||
|
path_utf8(fixture_path("minimal_cantilever.inp")));
|
||||||
|
EXPECT_EQ(
|
||||||
|
read.metadata->input_fingerprint,
|
||||||
|
"fnv1a64:73f31da4615f09b3");
|
||||||
|
|
||||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||||
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
|||||||
|
|
||||||
ASSERT_TRUE(result.deck.has_value());
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
EXPECT_TRUE(result.diagnostics.empty());
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.input_fingerprint,
|
||||||
|
"fnv1a64:73f31da4615f09b3");
|
||||||
EXPECT_TRUE(result.deck->parts.empty());
|
EXPECT_TRUE(result.deck->parts.empty());
|
||||||
EXPECT_FALSE(result.deck->assembly.has_value());
|
EXPECT_FALSE(result.deck->assembly.has_value());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user