From 83fd1d1c7e5a8df07ba8314d2049d1cd4da88e7b Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 16 Aug 2026 06:59:50 +0900 Subject: [PATCH] feat(cpp-object-oriented-modular-refactoring): step 6 - io-application-google-style --- include/fesa/app/fesa_application.h | 21 + include/fesa/app/fesa_application.hpp | 14 - include/fesa/io/abaqus/domain_mapper.h | 24 + include/fesa/io/abaqus/domain_mapper.hpp | 15 - include/fesa/io/abaqus/input_reader.h | 24 + include/fesa/io/abaqus/input_reader.hpp | 17 - include/fesa/io/abaqus/input_syntax.h | 46 + include/fesa/io/abaqus/input_syntax.hpp | 37 - include/fesa/io/hdf5/hdf5_results_writer.h | 28 + include/fesa/io/hdf5/hdf5_results_writer.hpp | 17 - src/fesa/analysis/linear_static_analysis.cpp | 8 +- src/fesa/app/fesa_application.cpp | 145 +- src/fesa/app/main.cpp | 22 +- src/fesa/io/abaqus/domain_mapper.cpp | 4499 ++++++++-------- src/fesa/io/abaqus/input_reader.cpp | 346 +- src/fesa/io/hdf5/hdf5_results_writer.cpp | 4535 ++++++++--------- .../integration/app/fesa_application_test.cpp | 632 ++- .../b33_reference_comparison_test.cpp | 406 +- .../reference/mitc4_reference_cases_test.cpp | 232 +- .../reference/mitc4_reference_comparison.cpp | 1561 +++--- tests/reference/mitc4_reference_comparison.h | 84 + .../reference/mitc4_reference_comparison.hpp | 81 - .../mitc4_reference_comparison_test.cpp | 1281 +++-- tests/reference/reference_comparison.cpp | 2560 +++++----- tests/reference/reference_comparison.h | 81 + tests/reference/reference_comparison.hpp | 83 - tests/reference/reference_comparison_test.cpp | 1134 ++--- tests/unit/io/abaqus/domain_mapper_test.cpp | 1348 ++--- tests/unit/io/abaqus/input_reader_test.cpp | 184 +- tests/unit/io/abaqus/input_syntax_test.cpp | 126 +- .../unit/io/hdf5/hdf5_results_writer_test.cpp | 2353 +++++---- 31 files changed, 10499 insertions(+), 11445 deletions(-) create mode 100644 include/fesa/app/fesa_application.h delete mode 100644 include/fesa/app/fesa_application.hpp create mode 100644 include/fesa/io/abaqus/domain_mapper.h delete mode 100644 include/fesa/io/abaqus/domain_mapper.hpp create mode 100644 include/fesa/io/abaqus/input_reader.h delete mode 100644 include/fesa/io/abaqus/input_reader.hpp create mode 100644 include/fesa/io/abaqus/input_syntax.h delete mode 100644 include/fesa/io/abaqus/input_syntax.hpp create mode 100644 include/fesa/io/hdf5/hdf5_results_writer.h delete mode 100644 include/fesa/io/hdf5/hdf5_results_writer.hpp create mode 100644 tests/reference/mitc4_reference_comparison.h delete mode 100644 tests/reference/mitc4_reference_comparison.hpp create mode 100644 tests/reference/reference_comparison.h delete mode 100644 tests/reference/reference_comparison.hpp diff --git a/include/fesa/app/fesa_application.h b/include/fesa/app/fesa_application.h new file mode 100644 index 0000000..3d0f447 --- /dev/null +++ b/include/fesa/app/fesa_application.h @@ -0,0 +1,21 @@ +#ifndef FESA_APP_FESA_APPLICATION_H_ +#define FESA_APP_FESA_APPLICATION_H_ + +#include +#include + +namespace fesa { + +/// @brief Owns the argv-independent CLI contract and stable process exit codes. +class FesaApplication { + public: + /// @brief Runs one solver invocation from operands and options after argv[0]. + /// @param arguments Input path and optional `--output` pair. + /// @return The stable CLI exit code for usage, input, model, solver, or + /// output status. + int Run(const std::vector& arguments); +}; + +} // namespace fesa + +#endif // FESA_APP_FESA_APPLICATION_H_ diff --git a/include/fesa/app/fesa_application.hpp b/include/fesa/app/fesa_application.hpp deleted file mode 100644 index 9111a05..0000000 --- a/include/fesa/app/fesa_application.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include -#include - -namespace fesa { - -// Owns the argv-independent command-line contract and stable process codes. -class FesaApplication { -public: - int run(const std::vector& arguments); -}; - -} // namespace fesa diff --git a/include/fesa/io/abaqus/domain_mapper.h b/include/fesa/io/abaqus/domain_mapper.h new file mode 100644 index 0000000..9e6ec57 --- /dev/null +++ b/include/fesa/io/abaqus/domain_mapper.h @@ -0,0 +1,24 @@ +#ifndef FESA_IO_ABAQUS_DOMAIN_MAPPER_H_ +#define FESA_IO_ABAQUS_DOMAIN_MAPPER_H_ + +#include "fesa/core/status.h" +#include "fesa/io/abaqus/input_syntax.h" +#include "fesa/model/domain.h" + +namespace fesa { + +/// @brief Maps syntax-only blocks into an approved immutable semantic model. +/// @note Source identity and declaration order are preserved through mapping. +class AbaqusDomainMapper { + public: + /// @brief Resolves supported Abaqus syntax into a complete Domain candidate. + /// @param input Parsed syntax whose source locations remain valid for + /// mapping. + /// @return A committed Domain or structured input/model diagnostics; partial + /// domains are never returned. + Result Map(const ParsedInput& input) const; +}; + +} // namespace fesa + +#endif // FESA_IO_ABAQUS_DOMAIN_MAPPER_H_ diff --git a/include/fesa/io/abaqus/domain_mapper.hpp b/include/fesa/io/abaqus/domain_mapper.hpp deleted file mode 100644 index cbfb03d..0000000 --- a/include/fesa/io/abaqus/domain_mapper.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "fesa/core/status.h" -#include "fesa/io/abaqus/input_syntax.hpp" -#include "fesa/model/domain.h" - -namespace fesa { - -// Converts syntax-only blocks into the approved immutable B33 semantic model. -class AbaqusDomainMapper { -public: - Result map(const ParsedInput& input) const; -}; - -} // namespace fesa diff --git a/include/fesa/io/abaqus/input_reader.h b/include/fesa/io/abaqus/input_reader.h new file mode 100644 index 0000000..c5635a8 --- /dev/null +++ b/include/fesa/io/abaqus/input_reader.h @@ -0,0 +1,24 @@ +#ifndef FESA_IO_ABAQUS_INPUT_READER_H_ +#define FESA_IO_ABAQUS_INPUT_READER_H_ + +#include + +#include "fesa/core/status.h" +#include "fesa/io/abaqus/input_syntax.h" + +namespace fesa { + +/// @brief Reads Abaqus physical keyword, data, and comment syntax. +/// @note Semantic policy is applied later by AbaqusDomainMapper. +class AbaqusInputReader { + public: + /// @brief Parses one input file without applying semantic mapping policy. + /// @param input_path Path to the exact source bytes whose identity is + /// retained. + /// @return Parsed syntax or a structured input diagnostic. + Result Read(const std::filesystem::path& input_path) const; +}; + +} // namespace fesa + +#endif // FESA_IO_ABAQUS_INPUT_READER_H_ diff --git a/include/fesa/io/abaqus/input_reader.hpp b/include/fesa/io/abaqus/input_reader.hpp deleted file mode 100644 index 4d305df..0000000 --- a/include/fesa/io/abaqus/input_reader.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "fesa/core/status.h" -#include "fesa/io/abaqus/input_syntax.hpp" - -#include - -namespace fesa { - -// Reads only physical keyword/data/comment syntax; semantic policy is applied -// later by AbaqusDomainMapper. -class AbaqusInputReader { -public: - Result read(const std::filesystem::path& inputPath) const; -}; - -} // namespace fesa diff --git a/include/fesa/io/abaqus/input_syntax.h b/include/fesa/io/abaqus/input_syntax.h new file mode 100644 index 0000000..d678eea --- /dev/null +++ b/include/fesa/io/abaqus/input_syntax.h @@ -0,0 +1,46 @@ +#ifndef FESA_IO_ABAQUS_INPUT_SYNTAX_H_ +#define FESA_IO_ABAQUS_INPUT_SYNTAX_H_ + +#include +#include +#include +#include + +#include "fesa/core/source_identity.h" + +namespace fesa { + +/// @brief Stores a canonical parameter name and its optional source value. +/// @note Parameter names are canonicalized for lookup while values remain +/// source text. +struct KeywordParameter { + std::string name; + std::optional value; +}; + +/// @brief Stores one parsed data row with its source location. +struct DataLine { + std::vector fields; + SourceLocation location; +}; + +/// @brief Stores one syntax-only keyword block and its following data rows. +struct KeywordBlock { + std::string canonical_name; + std::string original_line; + std::vector parameters; + std::vector data; + SourceLocation location; +}; + +/// @brief Stores the parsed syntax and stable identity of one Abaqus input +/// file. +struct ParsedInput { + std::filesystem::path source_path; + std::string source_content_identity; + std::vector blocks; +}; + +} // namespace fesa + +#endif // FESA_IO_ABAQUS_INPUT_SYNTAX_H_ diff --git a/include/fesa/io/abaqus/input_syntax.hpp b/include/fesa/io/abaqus/input_syntax.hpp deleted file mode 100644 index aa91d8e..0000000 --- a/include/fesa/io/abaqus/input_syntax.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "fesa/core/source_identity.h" - -#include -#include -#include -#include - -namespace fesa { - -// Names are canonicalized for syntax lookup while values remain source text. -struct KeywordParameter { - std::string name; - std::optional value; -}; - -struct DataLine { - std::vector fields; - SourceLocation location; -}; - -struct KeywordBlock { - std::string canonicalName; - std::string originalLine; - std::vector parameters; - std::vector data; - SourceLocation location; -}; - -struct ParsedInput { - std::filesystem::path sourcePath; - std::string sourceContentIdentity; - std::vector blocks; -}; - -} // namespace fesa diff --git a/include/fesa/io/hdf5/hdf5_results_writer.h b/include/fesa/io/hdf5/hdf5_results_writer.h new file mode 100644 index 0000000..dfed4f3 --- /dev/null +++ b/include/fesa/io/hdf5/hdf5_results_writer.h @@ -0,0 +1,28 @@ +#ifndef FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_ +#define FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_ + +#include "fesa/results/results_writer.h" + +namespace fesa { + +/// @brief Writes authoritative schema-v0 HDF5 results atomically. +/// @note HDF5 and platform types remain private to the implementation. +class Hdf5ResultsWriter final : public ResultsWriter { + public: + /// @brief Writes and self-checks a complete candidate before finalization. + /// @param output_path Final authoritative path; the candidate is created in + /// the same directory. + /// @param domain Immutable source and model identity. + /// @param state Fully recovered analysis state. + /// @param diagnostics Deterministically ordered run diagnostics. + /// @return Success only after atomic replacement or a structured output + /// failure. + /// @note A failed candidate does not replace an existing valid final file. + Status Write(const std::filesystem::path& output_path, const Domain& domain, + const AnalysisState& state, + const std::vector& diagnostics) override; +}; + +} // namespace fesa + +#endif // FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_ diff --git a/include/fesa/io/hdf5/hdf5_results_writer.hpp b/include/fesa/io/hdf5/hdf5_results_writer.hpp deleted file mode 100644 index 35ab6d1..0000000 --- a/include/fesa/io/hdf5/hdf5_results_writer.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "fesa/results/results_writer.h" - -namespace fesa { - -// Writes schema-v0 output while keeping backend and platform types private. -class Hdf5ResultsWriter final : public ResultsWriter { -public: - Status Write( - const std::filesystem::path& outputPath, - const Domain& domain, - const AnalysisState& state, - const std::vector& diagnostics) override; -}; - -} // namespace fesa diff --git a/src/fesa/analysis/linear_static_analysis.cpp b/src/fesa/analysis/linear_static_analysis.cpp index b603846..be1401f 100644 --- a/src/fesa/analysis/linear_static_analysis.cpp +++ b/src/fesa/analysis/linear_static_analysis.cpp @@ -5,8 +5,8 @@ #include "fesa/assembly/load_assembler.h" #include "fesa/assembly/parallel_for.h" #include "fesa/assembly/sparse_assembler.h" -#include "fesa/io/abaqus/domain_mapper.hpp" -#include "fesa/io/abaqus/input_reader.hpp" +#include "fesa/io/abaqus/domain_mapper.h" +#include "fesa/io/abaqus/input_reader.h" #include "fesa/results/result_recovery.h" #include "fesa/results/results_writer.h" #include "fesa/solvers/linear/linear_solver.h" @@ -65,11 +65,11 @@ Status LinearStaticAnalysis::Initialize(const AnalysisRequest& request) { diagnostics_.clear(); request_ = request; - const auto parsed = AbaqusInputReader{}.read(request_.input_path); + const auto parsed = AbaqusInputReader{}.Read(request_.input_path); if (!parsed.HasValue()) { return parsed.GetStatus(); } - auto domain = AbaqusDomainMapper{}.map(parsed.Value()); + auto domain = AbaqusDomainMapper{}.Map(parsed.Value()); if (!domain.HasValue()) { return domain.GetStatus(); } diff --git a/src/fesa/app/fesa_application.cpp b/src/fesa/app/fesa_application.cpp index 7c8128a..dd6614a 100644 --- a/src/fesa/app/fesa_application.cpp +++ b/src/fesa/app/fesa_application.cpp @@ -1,16 +1,16 @@ -#include "fesa/app/fesa_application.hpp" - -#include "fesa/analysis/linear_static_analysis.h" -#include "fesa/assembly/parallel_for.h" -#include "fesa/core/diagnostic.h" -#include "fesa/io/hdf5/hdf5_results_writer.hpp" -#include "fesa/solvers/linear/mkl_pardiso_solver.h" +#include "fesa/app/fesa_application.h" #include #include #include #include +#include "fesa/analysis/linear_static_analysis.h" +#include "fesa/assembly/parallel_for.h" +#include "fesa/core/diagnostic.h" +#include "fesa/io/hdf5/hdf5_results_writer.h" +#include "fesa/solvers/linear/mkl_pardiso_solver.h" + namespace fesa { namespace { @@ -21,93 +21,84 @@ constexpr int kModelExitCode = 4; constexpr int kSolverExitCode = 5; constexpr int kOutputExitCode = 6; -bool startsWithOption(const std::string& argument) { - return !argument.empty() && argument.front() == '-'; +bool StartsWithOption(const std::string& argument) { + return !argument.empty() && argument.front() == '-'; } -Diagnostic usageDiagnostic() { - return { - Severity::kError, - "cli-usage", - {{}, 0U}, - "", - "", - "Usage: fesa.exe [--output ]."}; +Diagnostic UsageDiagnostic() { + return {Severity::kError, + "cli-usage", + {{}, 0U}, + "", + "", + "Usage: fesa.exe [--output ]."}; } -const char* severityName(const Severity severity) { - return severity == Severity::kWarning ? "warning" : "error"; +const char* SeverityName(const Severity severity) { + return severity == Severity::kWarning ? "warning" : "error"; } -void writeDiagnostics(std::vector diagnostics) { - SortDiagnostics(diagnostics); - for (const auto& diagnostic : diagnostics) { - // Stable field labels and tab separators keep empty source fields - // explicit without depending on locale-specific formatting. - std::cerr - << "severity=" << severityName(diagnostic.severity) - << '\t' << "code=" << diagnostic.code - << '\t' << "file=" - << diagnostic.location.file.generic_u8string() - << '\t' << "line=" << diagnostic.location.line - << '\t' << "keyword=" << diagnostic.keyword - << '\t' << "entity_identity=" << diagnostic.entity_identity - << '\t' << "message=" << diagnostic.message - << '\n'; - } +void WriteDiagnostics(std::vector diagnostics) { + SortDiagnostics(diagnostics); + for (const auto& diagnostic : diagnostics) { + // Stable field labels and tab separators keep empty source fields + // explicit without depending on locale-specific formatting. + std::cerr << "severity=" << SeverityName(diagnostic.severity) << '\t' + << "code=" << diagnostic.code << '\t' + << "file=" << diagnostic.location.file.generic_u8string() << '\t' + << "line=" << diagnostic.location.line << '\t' + << "keyword=" << diagnostic.keyword << '\t' + << "entity_identity=" << diagnostic.entity_identity << '\t' + << "message=" << diagnostic.message << '\n'; + } } -int exitCodeFor(const Status& status) { - switch (status.Category().value_or(FailureCategory::kInput)) { +int ExitCodeFor(const Status& status) { + switch (status.Category().value_or(FailureCategory::kInput)) { case FailureCategory::kInput: - return kInputExitCode; + return kInputExitCode; case FailureCategory::kModel: - return kModelExitCode; + return kModelExitCode; case FailureCategory::kSolver: - return kSolverExitCode; + return kSolverExitCode; case FailureCategory::kOutput: - return kOutputExitCode; - } - return kInputExitCode; + return kOutputExitCode; + } + return kInputExitCode; } -} // namespace +} // namespace -int FesaApplication::run(const std::vector& arguments) { - const bool defaultOutputForm = - arguments.size() == 1U && - !arguments[0U].empty() && - !startsWithOption(arguments[0U]); - const bool explicitOutputForm = - arguments.size() == 3U && - !arguments[0U].empty() && - !startsWithOption(arguments[0U]) && - arguments[1U] == "--output" && - !arguments[2U].empty() && - !startsWithOption(arguments[2U]); - if (!defaultOutputForm && !explicitOutputForm) { - writeDiagnostics({usageDiagnostic()}); - return kUsageExitCode; - } +int FesaApplication::Run(const std::vector& arguments) { + const bool default_output_form = arguments.size() == 1U && + !arguments[0U].empty() && + !StartsWithOption(arguments[0U]); + const bool explicit_output_form = + arguments.size() == 3U && !arguments[0U].empty() && + !StartsWithOption(arguments[0U]) && arguments[1U] == "--output" && + !arguments[2U].empty() && !StartsWithOption(arguments[2U]); + if (!default_output_form && !explicit_output_form) { + WriteDiagnostics({UsageDiagnostic()}); + return kUsageExitCode; + } - AnalysisRequest request; - request.input_path = arguments[0U]; - request.output_path = explicitOutputForm - ? std::filesystem::path{arguments[2U]} - : std::filesystem::current_path() / "results.h5"; + AnalysisRequest request; + request.input_path = arguments[0U]; + request.output_path = explicit_output_form + ? std::filesystem::path{arguments[2U]} + : std::filesystem::current_path() / "results.h5"; - TbbParallelFor parallelFor; - MklPardisoSolver linearSolver; - Hdf5ResultsWriter resultsWriter; - LinearStaticAnalysis analysis{ - parallelFor, linearSolver, resultsWriter}; - const Status status = analysis.Run(request); - if (status.IsOk()) { - return kSuccessExitCode; - } + TbbParallelFor parallel_for; + MklPardisoSolver linear_solver; + Hdf5ResultsWriter results_writer; + LinearStaticAnalysis analysis{parallel_for, linear_solver, results_writer}; + const Status status = analysis.Run(request); + if (status.IsOk()) { + return kSuccessExitCode; + } - writeDiagnostics(status.Diagnostics()); - return exitCodeFor(status); + WriteDiagnostics(status.Diagnostics()); + return ExitCodeFor(status); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/app/main.cpp b/src/fesa/app/main.cpp index cf77fc3..5a92a9c 100644 --- a/src/fesa/app/main.cpp +++ b/src/fesa/app/main.cpp @@ -1,17 +1,17 @@ -#include "fesa/app/fesa_application.hpp" - #include #include #include +#include "fesa/app/fesa_application.h" + int main(const int argc, char* argv[]) { - std::vector arguments; - if (argc > 1) { - arguments.reserve(static_cast(argc - 1)); - } - // The application boundary receives only operands and options, not argv[0]. - for (int index = 1; index < argc; ++index) { - arguments.emplace_back(argv[index]); - } - return fesa::FesaApplication{}.run(arguments); + std::vector arguments; + if (argc > 1) { + arguments.reserve(static_cast(argc - 1)); + } + // The application boundary receives only operands and options, not argv[0]. + for (int index = 1; index < argc; ++index) { + arguments.emplace_back(argv[index]); + } + return fesa::FesaApplication{}.Run(arguments); } diff --git a/src/fesa/io/abaqus/domain_mapper.cpp b/src/fesa/io/abaqus/domain_mapper.cpp index b29b632..af70de9 100644 --- a/src/fesa/io/abaqus/domain_mapper.cpp +++ b/src/fesa/io/abaqus/domain_mapper.cpp @@ -1,6 +1,4 @@ -#include "fesa/io/abaqus/domain_mapper.hpp" - -#include "fesa/model/shell_geometry.h" +#include "fesa/io/abaqus/domain_mapper.h" #include #include @@ -16,2477 +14,2212 @@ #include #include +#include "fesa/model/shell_geometry.h" + namespace fesa { namespace { -std::string uppercaseAscii(std::string value) { - std::transform( - value.begin(), value.end(), value.begin(), [](char character) { - if (character >= 'a' && character <= 'z') { - return static_cast(character - 'a' + 'A'); - } - return character; - }); - return value; -} - -bool equalName(const std::string& left, const std::string& right) { - return uppercaseAscii(left) == uppercaseAscii(right); -} - -std::vector withoutTrailingEmpty( - std::vector fields) { - while (!fields.empty() && fields.back().empty()) { - fields.pop_back(); +std::string UppercaseAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](char character) { + if (character >= 'a' && character <= 'z') { + return static_cast(character - 'a' + 'A'); } - return fields; + return character; + }); + return value; +} + +bool EqualName(const std::string& left, const std::string& right) { + return UppercaseAscii(left) == UppercaseAscii(right); +} + +std::vector WithoutTrailingEmpty(std::vector fields) { + while (!fields.empty() && fields.back().empty()) { + fields.pop_back(); + } + return fields; } struct RawNode { - std::int64_t label; - std::string labelText; - std::array coordinates; - SourceLocation location; + std::int64_t label; + std::string label_text; + std::array coordinates; + SourceLocation location; }; struct RawElement { - enum class Type { - b33, - s4, - s4r - }; + enum class Type { kB33, kS4, kS4r }; - std::int64_t label; - std::string labelText; - Type type; - std::vector nodeLabels; - SourceLocation location; + std::int64_t label; + std::string label_text; + Type type; + std::vector node_labels; + SourceLocation location; }; -enum class ElementFamily { - beam, - shell -}; +enum class ElementFamily { kBeam, kShell }; struct RawSet { - std::string name; - std::vector members; - SourceLocation location; + std::string name; + std::vector members; + SourceLocation location; }; struct RawSection { - std::string elementSetName; - std::string materialName; - std::array properties; - std::array firstAxis; - std::vector> sectionPoints; - SourceLocation location; + std::string element_set_name; + std::string material_name; + std::array properties; + std::array first_axis; + std::vector> section_points; + SourceLocation location; }; struct RawShellSection { - std::string elementSetName; - std::string materialName; - double thickness; - SourceLocation location; + std::string element_set_name; + std::string material_name; + double thickness; + SourceLocation location; }; struct RawPart { - std::string name; - SourceLocation location; - std::vector nodes; - std::vector elements; - std::vector nodeSets; - std::vector elementSets; - std::vector sections; - std::vector shellSections; + std::string name; + SourceLocation location; + std::vector nodes; + std::vector elements; + std::vector node_sets; + std::vector element_sets; + std::vector sections; + std::vector shell_sections; }; struct RawInstance { - std::string name; - std::string partName; - SourceLocation location; + std::string name; + std::string part_name; + SourceLocation location; }; struct RawAssemblySet { - bool isNodeSet; - std::string name; - std::string instanceName; - std::vector members; - SourceLocation location; + bool is_node_set; + std::string name; + std::string instance_name; + std::vector members; + SourceLocation location; }; struct RawMaterial { - std::string name; - double youngsModulus{0.0}; - double poissonRatio{0.0}; - bool hasElastic{false}; - SourceLocation location; - SourceLocation elasticLocation; + std::string name; + double youngs_modulus{0.0}; + double poisson_ratio{0.0}; + bool has_elastic{false}; + SourceLocation location; + SourceLocation elastic_location; }; struct RawStep { - SourceLocation location; - bool hasStatic{false}; - std::array staticValues{}; - std::vector boundaries; - std::vector loads; + SourceLocation location; + bool has_static{false}; + std::array static_values{}; + std::vector boundaries; + std::vector loads; }; struct MappingFailure { - FailureCategory category; - Diagnostic diagnostic; + FailureCategory category; + Diagnostic diagnostic; }; +/// @brief Builds one complete Domain candidate while retaining source identity. +/// @note The candidate is committed only after all blocks and cross-references +/// have been validated. class MappingContext { -public: - explicit MappingContext(const ParsedInput& input) : input_{input} {} + public: + explicit MappingContext(const ParsedInput& input) : input_{input} {} - Result run() { - parseBlocks(); + Result Run() { + ParseBlocks(); + if (!failure_) { + FinalizeModel(); + } + if (failure_) { + return Result::Failure(Status::Failure( + failure_->category, {std::move(failure_->diagnostic)})); + } + SortDiagnostics(definition_.warnings); + return Domain::Create(std::move(definition_)); + } + + private: + const KeywordParameter* Parameter(const KeywordBlock& block, + std::string_view name) const { + const auto found = + std::find_if(block.parameters.begin(), block.parameters.end(), + [name](const KeywordParameter& candidate) { + return candidate.name == name; + }); + return found == block.parameters.end() ? nullptr : &*found; + } + + bool Fail(FailureCategory category, std::string code, + const SourceLocation& location, std::string keyword, + std::string entity_identity, std::string message) { + if (!failure_) { + failure_ = MappingFailure{ + category, + {Severity::kError, std::move(code), location, std::move(keyword), + std::move(entity_identity), std::move(message)}}; + } + return false; + } + + bool InputFailure(std::string code, const SourceLocation& location, + std::string keyword, std::string entity_identity, + std::string message) { + return Fail(FailureCategory::kInput, std::move(code), location, + std::move(keyword), std::move(entity_identity), + std::move(message)); + } + + bool ModelFailure(std::string code, const SourceLocation& location, + std::string keyword, std::string entity_identity, + std::string message) { + return Fail(FailureCategory::kModel, std::move(code), location, + std::move(keyword), std::move(entity_identity), + std::move(message)); + } + + bool InvalidKeywordLocation(const KeywordBlock& block, std::string message) { + return InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", std::move(message)); + } + + bool ValidateParameters(const KeywordBlock& block, + const std::vector& allowed) { + std::set seen; + for (const auto& candidate : block.parameters) { + if (!seen.insert(candidate.name).second) { + return InputFailure("duplicate-entity", block.location, + block.canonical_name, candidate.name, + "A keyword parameter may be declared only once."); + } + if (std::find(allowed.begin(), allowed.end(), candidate.name) == + allowed.end()) { + return InputFailure( + "invalid-keyword-parameter", block.location, block.canonical_name, + candidate.name, + "The keyword parameter is outside the approved subset."); + } + } + return true; + } + + const std::string* RequiredParameterValue(const KeywordBlock& block, + std::string_view name) { + const auto* found = Parameter(block, name); + if (found == nullptr || !found->value || found->value->empty()) { + InputFailure("invalid-keyword-parameter", block.location, + block.canonical_name, std::string{name}, + "The keyword requires a non-empty parameter value."); + return nullptr; + } + return &*found->value; + } + + bool RequireNoData(const KeywordBlock& block) { + if (!block.data.empty()) { + return InputFailure("invalid-data-arity", block.data.front().location, + block.canonical_name, "", + "This keyword does not accept data rows."); + } + return true; + } + + bool ParseInteger(const std::string& text, std::int64_t& value, + const SourceLocation& location, const std::string& keyword, + bool positive) { + if (text.empty()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A numeric field cannot be empty."); + } + char* end = nullptr; + errno = 0; + const long long parsed = std::strtoll(text.c_str(), &end, 10); + if (errno == ERANGE || end != text.c_str() + text.size() || + (positive && parsed <= 0)) { + return InputFailure( + "invalid-numeric-value", location, keyword, text, + "The field must be a valid positive base-10 integer."); + } + value = static_cast(parsed); + return true; + } + + bool TryPositiveInteger(const std::string& text, std::int64_t& value) const { + if (text.empty()) { + return false; + } + char* end = nullptr; + errno = 0; + const long long parsed = std::strtoll(text.c_str(), &end, 10); + if (errno == ERANGE || end != text.c_str() + text.size() || parsed <= 0) { + return false; + } + value = static_cast(parsed); + return true; + } + + bool ParseDouble(const std::string& text, double& value, + const SourceLocation& location, const std::string& keyword) { + if (text.empty()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A numeric field cannot be empty."); + } + char* end = nullptr; + errno = 0; + const double parsed = std::strtod(text.c_str(), &end); + if (errno == ERANGE || end != text.c_str() + text.size() || + !std::isfinite(parsed)) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "The field must be a finite floating-point value."); + } + value = parsed; + return true; + } + + bool ParseModelDouble(const std::string& text, double& value, + const SourceLocation& location, + const std::string& keyword, + const std::string& diagnostic_code, + const std::string& message) { + if (text.empty()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A numeric field cannot be empty."); + } + char* end = nullptr; + errno = 0; + const double parsed = std::strtod(text.c_str(), &end); + if (end != text.c_str() + text.size()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "The field must use floating-point syntax."); + } + if (errno == ERANGE || !std::isfinite(parsed)) { + return ModelFailure(diagnostic_code, location, keyword, text, message); + } + value = parsed; + return true; + } + + bool ContainsName(const std::vector& values, + const std::string& name) const { + return std::any_of( + values.begin(), values.end(), + [&name](const RawPart& value) { return EqualName(value.name, name); }); + } + + bool ContainsName(const std::vector& values, + const std::string& name) const { + return std::any_of(values.begin(), values.end(), + [&name](const RawInstance& value) { + return EqualName(value.name, name); + }); + } + + bool ContainsName(const std::vector& values, + const std::string& name) const { + return std::any_of(values.begin(), values.end(), + [&name](const RawMaterial& value) { + return EqualName(value.name, name); + }); + } + + bool ContainsSetName(const std::vector& sets, + const std::string& name) const { + const auto matches = [&name](const RawSet& set) { + return EqualName(set.name, name); + }; + return std::any_of(sets.begin(), sets.end(), matches); + } + + bool ContainsAssemblySetName(bool node_set, const std::string& name) const { + return std::any_of(assembly_sets_.begin(), assembly_sets_.end(), + [node_set, &name](const RawAssemblySet& set) { + return set.is_node_set == node_set && + EqualName(set.name, name); + }); + } + + void ParseBlocks() { + definition_.source_path = input_.source_path; + definition_.source_content_identity = input_.source_content_identity; + + for (std::size_t index = 0U; index < input_.blocks.size() && !failure_; + ++index) { + const auto& block = input_.blocks[index]; + if (in_instance_) { + ParseInstanceBlock(block); + } else if (current_part_) { + ParsePartBlock(block); + } else if (in_assembly_) { + ParseAssemblyBlock(block); + } else if (in_step_) { + ParseStepBlock(block); + } else { + ParseTopLevelBlock(block, index); + } + } + + if (!failure_ && + (current_part_ || in_assembly_ || in_instance_ || in_step_)) { + const auto location = input_.blocks.empty() + ? SourceLocation{input_.source_path, 0U} + : input_.blocks.back().location; + InputFailure("unclosed-keyword-block", location, "", "", + "A part, assembly, instance, or step block was not closed."); + } + } + + void ParseTopLevelBlock(const KeywordBlock& block, std::size_t index) { + if (step_seen_) { + if (block.canonical_name == "STEP") { + ParseStepStart(block); + } else { + InvalidKeywordLocation( + block, "The sole analysis step must be the final top-level block."); + } + return; + } + + if (block.canonical_name != "ELASTIC") { + material_eligible_.reset(); + } + pending_section_.reset(); + active_output_ = false; + + if (block.canonical_name == "HEADING") { + if (index != 0U || heading_seen_ || !ValidateParameters(block, {})) { if (!failure_) { - finalizeModel(); + InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", + "HEADING is optional only as the first keyword."); } - if (failure_) { - return Result::Failure(Status::Failure( - failure_->category, {std::move(failure_->diagnostic)})); + return; + } + heading_seen_ = true; + for (std::size_t row = 0U; row < block.data.size(); ++row) { + if (row != 0U) { + definition_.heading.push_back('\n'); } - SortDiagnostics(definition_.warnings); - return Domain::Create(std::move(definition_)); - } - -private: - const KeywordParameter* parameter( - const KeywordBlock& block, - std::string_view name) const { - const auto found = std::find_if( - block.parameters.begin(), - block.parameters.end(), - [name](const KeywordParameter& candidate) { - return candidate.name == name; - }); - return found == block.parameters.end() ? nullptr : &*found; - } - - bool fail( - FailureCategory category, - std::string code, - const SourceLocation& location, - std::string keyword, - std::string entityIdentity, - std::string message) { - if (!failure_) { - failure_ = MappingFailure{ - category, - {Severity::kError, - std::move(code), - location, - std::move(keyword), - std::move(entityIdentity), - std::move(message)}}; + for (std::size_t field = 0U; field < block.data[row].fields.size(); + ++field) { + if (field != 0U) { + definition_.heading.push_back(','); + } + definition_.heading += block.data[row].fields[field]; } - return false; + } + return; + } + if (block.canonical_name == "PREPRINT") { + if (!RequireNoData(block)) { + return; + } + AddIgnoredWarning(block); + return; + } + if (block.canonical_name == "PART") { + ParsePartStart(block); + return; + } + if (block.canonical_name == "ASSEMBLY") { + ParseAssemblyStart(block); + return; + } + if (block.canonical_name == "MATERIAL") { + ParseMaterial(block); + return; + } + if (block.canonical_name == "ELASTIC") { + ParseElastic(block); + return; + } + if (block.canonical_name == "BOUNDARY") { + if (!assembly_seen_ || materials_.empty()) { + InvalidKeywordLocation( + block, "Model boundary data follows the assembly and materials."); + return; + } + model_boundary_seen_ = true; + ParseBoundary(block, model_boundaries_); + return; + } + if (block.canonical_name == "STEP") { + ParseStepStart(block); + return; + } + if (block.canonical_name == "END PART" || + block.canonical_name == "END ASSEMBLY" || + block.canonical_name == "END INSTANCE" || + block.canonical_name == "END STEP") { + InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", + "The closing keyword has no matching open block."); + return; + } + RejectUnknown(block); + } + + void ParsePartStart(const KeywordBlock& block) { + if (assembly_seen_ || !materials_.empty() || model_boundary_seen_) { + InvalidKeywordLocation( + block, "All part blocks must precede the sole assembly block."); + return; + } + if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) { + return; + } + const auto* name = RequiredParameterValue(block, "NAME"); + if (name == nullptr) { + return; + } + if (ContainsName(parts_, *name)) { + InputFailure("duplicate-entity", block.location, block.canonical_name, + *name, + "Part names are unique under case-insensitive lookup."); + return; + } + parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}}); + current_part_ = parts_.size() - 1U; + part_elements_seen_ = false; + part_sets_seen_ = false; + part_sections_seen_ = false; + beam_section_context_active_ = false; + } + + void ParsePartBlock(const KeywordBlock& block) { + if (block.canonical_name == "END PART") { + if (!ValidateParameters(block, {}) || !RequireNoData(block)) { + return; + } + const RawPart& part = parts_[*current_part_]; + const bool shell_part = model_element_family_ == ElementFamily::kShell; + if (part.nodes.empty() || part.elements.empty() || + (!shell_part && part.sections.empty())) { + InvalidKeywordLocation(block, + "A part closes only after node, element, " + "and matching section blocks."); + return; + } + current_part_.reset(); + pending_section_.reset(); + beam_section_context_active_ = false; + return; + } + if (block.canonical_name == "PART") { + InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", + "Nested part blocks are not supported."); + return; + } + if (block.canonical_name == "ASSEMBLY") { + InputFailure("unsupported-nested-assembly", block.location, + block.canonical_name, "", + "An assembly cannot be nested in a part."); + return; } - bool inputFailure( - std::string code, - const SourceLocation& location, - std::string keyword, - std::string entityIdentity, - std::string message) { - return fail( - FailureCategory::kInput, - std::move(code), - location, - std::move(keyword), - std::move(entityIdentity), - std::move(message)); - } - - bool modelFailure( - std::string code, - const SourceLocation& location, - std::string keyword, - std::string entityIdentity, - std::string message) { - return fail( - FailureCategory::kModel, - std::move(code), - location, - std::move(keyword), - std::move(entityIdentity), - std::move(message)); - } - - bool invalidKeywordLocation( - const KeywordBlock& block, - std::string message) { - return inputFailure( - "invalid-keyword-location", - block.location, - block.canonicalName, + RawPart& part = parts_[*current_part_]; + if (block.canonical_name == "NODE") { + beam_section_context_active_ = false; + if (part_elements_seen_ || part_sets_seen_ || part_sections_seen_) { + InvalidKeywordLocation( + block, + "NODE blocks must precede element, set, and section blocks."); + return; + } + pending_section_.reset(); + ParseNodes(block, part); + } else if (block.canonical_name == "ELEMENT") { + beam_section_context_active_ = false; + if (part.nodes.empty() || part_sets_seen_ || part_sections_seen_) { + InvalidKeywordLocation(block, + "ELEMENT blocks follow at least one NODE block " + "and precede sets and sections."); + return; + } + pending_section_.reset(); + ParseElements(block, part); + part_elements_seen_ = !failure_; + } else if (block.canonical_name == "NSET") { + beam_section_context_active_ = false; + if (!part_elements_seen_ || part_sections_seen_) { + InvalidKeywordLocation( + block, + "Part sets follow element blocks and precede beam sections."); + return; + } + pending_section_.reset(); + ParseSet(block, true, part); + part_sets_seen_ = !failure_; + } else if (block.canonical_name == "ELSET") { + beam_section_context_active_ = false; + if (!part_elements_seen_ || part_sections_seen_) { + InvalidKeywordLocation( + block, + "Part sets follow element blocks and precede beam sections."); + return; + } + pending_section_.reset(); + ParseSet(block, false, part); + part_sets_seen_ = !failure_; + } else if (block.canonical_name == "BEAM GENERAL SECTION") { + if (!part_elements_seen_) { + InvalidKeywordLocation( + block, + "BEAM GENERAL SECTION follows the part mesh and optional sets."); + return; + } + if (model_element_family_ == ElementFamily::kShell) { + InputFailure( + "unsupported-mixed-element-model", block.location, + block.canonical_name, "", + "Beam-section semantics cannot be mixed with shell elements."); + return; + } + ParseBeamSection(block, part); + part_sections_seen_ = !failure_; + beam_section_context_active_ = !failure_; + } else if (block.canonical_name == "SHELL SECTION") { + beam_section_context_active_ = false; + if (!part_elements_seen_) { + InvalidKeywordLocation( + block, "SHELL SECTION follows the part mesh and optional sets."); + return; + } + if (model_element_family_ == ElementFamily::kBeam) { + InputFailure( + "unsupported-mixed-element-model", block.location, + block.canonical_name, "", + "Shell-section semantics cannot be mixed with beam elements."); + return; + } + ParseShellSection(block, part); + part_sections_seen_ = !failure_; + } else if (block.canonical_name == "SECTION POINTS") { + ParseSectionPoints(block, part); + } else if (block.canonical_name == "TRANSVERSE SHEAR STIFFNESS") { + pending_section_.reset(); + if (!beam_section_context_active_) { + InputFailure( + "invalid-keyword-location", block.location, block.canonical_name, "", - std::move(message)); + "The ignored shear keyword still requires beam-section context."); + return; + } + AddIgnoredWarning(block); + } else { + pending_section_.reset(); + beam_section_context_active_ = false; + RejectUnknown(block); } + } - bool validateParameters( - const KeywordBlock& block, - const std::vector& allowed) { - std::set seen; - for (const auto& candidate : block.parameters) { - if (!seen.insert(candidate.name).second) { - return inputFailure( - "duplicate-entity", - block.location, - block.canonicalName, - candidate.name, - "A keyword parameter may be declared only once."); - } - if (std::find(allowed.begin(), allowed.end(), candidate.name) == - allowed.end()) { - return inputFailure( - "invalid-keyword-parameter", - block.location, - block.canonicalName, - candidate.name, - "The keyword parameter is outside the approved subset."); - } - } - return true; + void ParseNodes(const KeywordBlock& block, RawPart& part) { + if (!ValidateParameters(block, {}) || block.data.empty()) { + if (!failure_) { + InputFailure("invalid-data-arity", block.location, block.canonical_name, + "", "NODE requires at least one four-field data row."); + } + return; } - - const std::string* requiredParameterValue( - const KeywordBlock& block, - std::string_view name) { - const auto* found = parameter(block, name); - if (found == nullptr || !found->value || found->value->empty()) { - inputFailure( - "invalid-keyword-parameter", - block.location, - block.canonicalName, - std::string{name}, - "The keyword requires a non-empty parameter value."); - return nullptr; - } - return &*found->value; - } - - bool requireNoData(const KeywordBlock& block) { - if (!block.data.empty()) { - return inputFailure( - "invalid-data-arity", - block.data.front().location, - block.canonicalName, - "", - "This keyword does not accept data rows."); - } - return true; - } - - bool parseInteger( - const std::string& text, - std::int64_t& value, - const SourceLocation& location, - const std::string& keyword, - bool positive) { - if (text.empty()) { - return inputFailure( - "invalid-numeric-value", location, keyword, text, - "A numeric field cannot be empty."); - } - char* end = nullptr; - errno = 0; - const long long parsed = std::strtoll(text.c_str(), &end, 10); - if (errno == ERANGE || end != text.c_str() + text.size() || - (positive && parsed <= 0)) { - return inputFailure( - "invalid-numeric-value", location, keyword, text, - "The field must be a valid positive base-10 integer."); - } - value = static_cast(parsed); - return true; - } - - bool tryPositiveInteger(const std::string& text, std::int64_t& value) const { - if (text.empty()) { - return false; - } - char* end = nullptr; - errno = 0; - const long long parsed = std::strtoll(text.c_str(), &end, 10); - if (errno == ERANGE || end != text.c_str() + text.size() || parsed <= 0) { - return false; - } - value = static_cast(parsed); - return true; - } - - bool parseDouble( - const std::string& text, - double& value, - const SourceLocation& location, - const std::string& keyword) { - if (text.empty()) { - return inputFailure( - "invalid-numeric-value", location, keyword, text, - "A numeric field cannot be empty."); - } - char* end = nullptr; - errno = 0; - const double parsed = std::strtod(text.c_str(), &end); - if (errno == ERANGE || end != text.c_str() + text.size() || - !std::isfinite(parsed)) { - return inputFailure( - "invalid-numeric-value", location, keyword, text, - "The field must be a finite floating-point value."); - } - value = parsed; - return true; - } - - bool parseModelDouble( - const std::string& text, - double& value, - const SourceLocation& location, - const std::string& keyword, - const std::string& diagnosticCode, - const std::string& message) { - if (text.empty()) { - return inputFailure( - "invalid-numeric-value", location, keyword, text, - "A numeric field cannot be empty."); - } - char* end = nullptr; - errno = 0; - const double parsed = std::strtod(text.c_str(), &end); - if (end != text.c_str() + text.size()) { - return inputFailure( - "invalid-numeric-value", location, keyword, text, - "The field must use floating-point syntax."); - } - if (errno == ERANGE || !std::isfinite(parsed)) { - return modelFailure( - diagnosticCode, location, keyword, text, message); - } - value = parsed; - return true; - } - - bool containsName( - const std::vector& values, - const std::string& name) const { - return std::any_of( - values.begin(), values.end(), [&name](const RawPart& value) { - return equalName(value.name, name); - }); - } - - bool containsName( - const std::vector& values, - const std::string& name) const { - return std::any_of( - values.begin(), values.end(), [&name](const RawInstance& value) { - return equalName(value.name, name); - }); - } - - bool containsName( - const std::vector& values, - const std::string& name) const { - return std::any_of( - values.begin(), values.end(), [&name](const RawMaterial& value) { - return equalName(value.name, name); - }); - } - - bool containsSetName( - const std::vector& sets, - const std::string& name) const { - const auto matches = [&name](const RawSet& set) { - return equalName(set.name, name); - }; - return std::any_of(sets.begin(), sets.end(), matches); - } - - bool containsAssemblySetName( - bool nodeSet, - const std::string& name) const { - return std::any_of( - assemblySets_.begin(), - assemblySets_.end(), - [nodeSet, &name](const RawAssemblySet& set) { - return set.isNodeSet == nodeSet && equalName(set.name, name); - }); - } - - void parseBlocks() { - definition_.source_path = input_.sourcePath; - definition_.source_content_identity = input_.sourceContentIdentity; - - for (std::size_t index = 0U; - index < input_.blocks.size() && !failure_; - ++index) { - const auto& block = input_.blocks[index]; - if (inInstance_) { - parseInstanceBlock(block); - } else if (currentPart_) { - parsePartBlock(block); - } else if (inAssembly_) { - parseAssemblyBlock(block); - } else if (inStep_) { - parseStepBlock(block); - } else { - parseTopLevelBlock(block, index); - } - } - - if (!failure_ && - (currentPart_ || inAssembly_ || inInstance_ || inStep_)) { - const auto location = input_.blocks.empty() - ? SourceLocation{input_.sourcePath, 0U} - : input_.blocks.back().location; - inputFailure( - "unclosed-keyword-block", - location, - "", - "", - "A part, assembly, instance, or step block was not closed."); - } - } - - void parseTopLevelBlock(const KeywordBlock& block, std::size_t index) { - if (stepSeen_) { - if (block.canonicalName == "STEP") { - parseStepStart(block); - } else { - invalidKeywordLocation( - block, - "The sole analysis step must be the final top-level block."); - } - return; - } - - if (block.canonicalName != "ELASTIC") { - materialEligible_.reset(); - } - pendingSection_.reset(); - activeOutput_ = false; - - if (block.canonicalName == "HEADING") { - if (index != 0U || headingSeen_ || - !validateParameters(block, {})) { - if (!failure_) { - inputFailure( - "invalid-keyword-location", - block.location, - block.canonicalName, - "", - "HEADING is optional only as the first keyword."); - } - return; - } - headingSeen_ = true; - for (std::size_t row = 0U; row < block.data.size(); ++row) { - if (row != 0U) { - definition_.heading.push_back('\n'); - } - for (std::size_t field = 0U; - field < block.data[row].fields.size(); - ++field) { - if (field != 0U) { - definition_.heading.push_back(','); - } - definition_.heading += block.data[row].fields[field]; - } - } - return; - } - if (block.canonicalName == "PREPRINT") { - if (!requireNoData(block)) { - return; - } - addIgnoredWarning(block); - return; - } - if (block.canonicalName == "PART") { - parsePartStart(block); - return; - } - if (block.canonicalName == "ASSEMBLY") { - parseAssemblyStart(block); - return; - } - if (block.canonicalName == "MATERIAL") { - parseMaterial(block); - return; - } - if (block.canonicalName == "ELASTIC") { - parseElastic(block); - return; - } - if (block.canonicalName == "BOUNDARY") { - if (!assemblySeen_ || materials_.empty()) { - invalidKeywordLocation( - block, - "Model boundary data follows the assembly and materials."); - return; - } - modelBoundarySeen_ = true; - parseBoundary(block, modelBoundaries_); - return; - } - if (block.canonicalName == "STEP") { - parseStepStart(block); - return; - } - if (block.canonicalName == "END PART" || - block.canonicalName == "END ASSEMBLY" || - block.canonicalName == "END INSTANCE" || - block.canonicalName == "END STEP") { - inputFailure( - "invalid-keyword-location", - block.location, - block.canonicalName, - "", - "The closing keyword has no matching open block."); - return; - } - rejectUnknown(block); - } - - void parsePartStart(const KeywordBlock& block) { - if (assemblySeen_ || !materials_.empty() || modelBoundarySeen_) { - invalidKeywordLocation( - block, - "All part blocks must precede the sole assembly block."); - return; - } - if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) { - return; - } - const auto* name = requiredParameterValue(block, "NAME"); - if (name == nullptr) { - return; - } - if (containsName(parts_, *name)) { - inputFailure( - "duplicate-entity", - block.location, - block.canonicalName, - *name, - "Part names are unique under case-insensitive lookup."); - return; - } - parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}}); - currentPart_ = parts_.size() - 1U; - partElementsSeen_ = false; - partSetsSeen_ = false; - partSectionsSeen_ = false; - beamSectionContextActive_ = false; - } - - void parsePartBlock(const KeywordBlock& block) { - if (block.canonicalName == "END PART") { - if (!validateParameters(block, {}) || !requireNoData(block)) { - return; - } - const RawPart& part = parts_[*currentPart_]; - const bool shellPart = modelElementFamily_ == ElementFamily::shell; - if (part.nodes.empty() || part.elements.empty() || - (!shellPart && part.sections.empty())) { - invalidKeywordLocation( - block, - "A part closes only after node, element, and matching section blocks."); - return; - } - currentPart_.reset(); - pendingSection_.reset(); - beamSectionContextActive_ = false; - return; - } - if (block.canonicalName == "PART") { - inputFailure( - "invalid-keyword-location", - block.location, - block.canonicalName, - "", - "Nested part blocks are not supported."); - return; - } - if (block.canonicalName == "ASSEMBLY") { - inputFailure( - "unsupported-nested-assembly", - block.location, - block.canonicalName, - "", - "An assembly cannot be nested in a part."); - return; - } - - RawPart& part = parts_[*currentPart_]; - if (block.canonicalName == "NODE") { - beamSectionContextActive_ = false; - if (partElementsSeen_ || partSetsSeen_ || partSectionsSeen_) { - invalidKeywordLocation( - block, - "NODE blocks must precede element, set, and section blocks."); - return; - } - pendingSection_.reset(); - parseNodes(block, part); - } else if (block.canonicalName == "ELEMENT") { - beamSectionContextActive_ = false; - if (part.nodes.empty() || partSetsSeen_ || partSectionsSeen_) { - invalidKeywordLocation( - block, - "ELEMENT blocks follow at least one NODE block and precede sets and sections."); - return; - } - pendingSection_.reset(); - parseElements(block, part); - partElementsSeen_ = !failure_; - } else if (block.canonicalName == "NSET") { - beamSectionContextActive_ = false; - if (!partElementsSeen_ || partSectionsSeen_) { - invalidKeywordLocation( - block, - "Part sets follow element blocks and precede beam sections."); - return; - } - pendingSection_.reset(); - parseSet(block, true, part); - partSetsSeen_ = !failure_; - } else if (block.canonicalName == "ELSET") { - beamSectionContextActive_ = false; - if (!partElementsSeen_ || partSectionsSeen_) { - invalidKeywordLocation( - block, - "Part sets follow element blocks and precede beam sections."); - return; - } - pendingSection_.reset(); - parseSet(block, false, part); - partSetsSeen_ = !failure_; - } else if (block.canonicalName == "BEAM GENERAL SECTION") { - if (!partElementsSeen_) { - invalidKeywordLocation( - block, - "BEAM GENERAL SECTION follows the part mesh and optional sets."); - return; - } - if (modelElementFamily_ == ElementFamily::shell) { - inputFailure( - "unsupported-mixed-element-model", block.location, - block.canonicalName, "", - "Beam-section semantics cannot be mixed with shell elements."); - return; - } - parseBeamSection(block, part); - partSectionsSeen_ = !failure_; - beamSectionContextActive_ = !failure_; - } else if (block.canonicalName == "SHELL SECTION") { - beamSectionContextActive_ = false; - if (!partElementsSeen_) { - invalidKeywordLocation( - block, - "SHELL SECTION follows the part mesh and optional sets."); - return; - } - if (modelElementFamily_ == ElementFamily::beam) { - inputFailure( - "unsupported-mixed-element-model", block.location, - block.canonicalName, "", - "Shell-section semantics cannot be mixed with beam elements."); - return; - } - parseShellSection(block, part); - partSectionsSeen_ = !failure_; - } else if (block.canonicalName == "SECTION POINTS") { - parseSectionPoints(block, part); - } else if (block.canonicalName == "TRANSVERSE SHEAR STIFFNESS") { - pendingSection_.reset(); - if (!beamSectionContextActive_) { - inputFailure( - "invalid-keyword-location", - block.location, - block.canonicalName, - "", - "The ignored shear keyword still requires beam-section context."); - return; - } - addIgnoredWarning(block); - } else { - pendingSection_.reset(); - beamSectionContextActive_ = false; - rejectUnknown(block); - } - } - - void parseNodes(const KeywordBlock& block, RawPart& part) { - if (!validateParameters(block, {}) || block.data.empty()) { - if (!failure_) { - inputFailure( - "invalid-data-arity", block.location, block.canonicalName, - "", "NODE requires at least one four-field data row."); - } - return; - } - for (const auto& row : block.data) { - if (row.fields.size() != 4U) { - inputFailure( - "invalid-data-arity", row.location, block.canonicalName, - "", "NODE rows require label, x, y, z."); - return; - } - RawNode node{}; - node.labelText = row.fields[0]; - node.location = row.location; - if (!parseInteger( - row.fields[0], node.label, row.location, - block.canonicalName, true)) { - return; - } - if (std::any_of( - part.nodes.begin(), part.nodes.end(), - [&node](const RawNode& existing) { + for (const auto& row : block.data) { + if (row.fields.size() != 4U) { + InputFailure("invalid-data-arity", row.location, block.canonical_name, + "", "NODE rows require label, x, y, z."); + return; + } + RawNode node{}; + node.label_text = row.fields[0]; + node.location = row.location; + if (!ParseInteger(row.fields[0], node.label, row.location, + block.canonical_name, true)) { + return; + } + if (std::any_of(part.nodes.begin(), part.nodes.end(), + [&node](const RawNode& existing) { return existing.label == node.label; - })) { - inputFailure( - "duplicate-entity", row.location, block.canonicalName, - node.labelText, "Node labels are unique within a part."); - return; - } - for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { - if (!parseDouble( - row.fields[coordinate + 1U], - node.coordinates[coordinate], - row.location, - block.canonicalName)) { - return; - } - } - part.nodes.push_back(std::move(node)); + })) { + InputFailure("duplicate-entity", row.location, block.canonical_name, + node.label_text, "Node labels are unique within a part."); + return; + } + for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { + if (!ParseDouble(row.fields[coordinate + 1U], + node.coordinates[coordinate], row.location, + block.canonical_name)) { + return; } + } + part.nodes.push_back(std::move(node)); } + } - void parseElements(const KeywordBlock& block, RawPart& part) { - if (!validateParameters(block, {"TYPE"})) { - return; + void ParseElements(const KeywordBlock& block, RawPart& part) { + if (!ValidateParameters(block, {"TYPE"})) { + return; + } + const auto* type = RequiredParameterValue(block, "TYPE"); + if (type == nullptr) { + return; + } + RawElement::Type element_type{}; + ElementFamily element_family{}; + std::size_t expected_field_count = 0U; + if (EqualName(*type, "B33")) { + element_type = RawElement::Type::kB33; + element_family = ElementFamily::kBeam; + expected_field_count = 3U; + } else if (EqualName(*type, "S4")) { + element_type = RawElement::Type::kS4; + element_family = ElementFamily::kShell; + expected_field_count = 5U; + } else if (EqualName(*type, "S4R")) { + element_type = RawElement::Type::kS4r; + element_family = ElementFamily::kShell; + expected_field_count = 5U; + } else { + InputFailure( + "unsupported-element-formulation", block.location, + block.canonical_name, *type, + "Only TYPE=B33, TYPE=S4, and TYPE=S4R belong to the approved " + "element subsets."); + return; + } + if (model_element_family_ && *model_element_family_ != element_family) { + InputFailure("unsupported-mixed-element-model", block.location, + block.canonical_name, *type, + "Beam and shell elements cannot be mixed in one model."); + return; + } + model_element_family_ = element_family; + if (block.data.empty()) { + InputFailure(element_family == ElementFamily::kShell + ? "invalid-shell-connectivity" + : "invalid-data-arity", + block.location, block.canonical_name, "", + "ELEMENT requires at least one row with the approved " + "connectivity arity."); + return; + } + for (const auto& row : block.data) { + if (row.fields.size() != expected_field_count) { + InputFailure( + element_family == ElementFamily::kShell + ? "invalid-shell-connectivity" + : "invalid-data-arity", + row.location, block.canonical_name, "", + element_family == ElementFamily::kShell + ? "S4 and S4R rows require a label and exactly four nodes." + : "B33 rows require label, node 1, node 2."); + return; + } + RawElement element{}; + element.label_text = row.fields[0]; + element.type = element_type; + element.location = row.location; + if (!ParseInteger(row.fields[0], element.label, row.location, + block.canonical_name, true)) { + return; + } + element.node_labels.reserve(expected_field_count - 1U); + for (std::size_t field = 1U; field < expected_field_count; ++field) { + std::int64_t node_label = 0; + if (!ParseInteger(row.fields[field], node_label, row.location, + block.canonical_name, true)) { + return; } - const auto* type = requiredParameterValue(block, "TYPE"); - if (type == nullptr) { - return; + element.node_labels.push_back(node_label); + } + if (element_family == ElementFamily::kShell) { + const std::set distinct_nodes{element.node_labels.begin(), + element.node_labels.end()}; + if (distinct_nodes.size() != element.node_labels.size()) { + InputFailure( + "invalid-shell-connectivity", row.location, block.canonical_name, + element.label_text, + "Shell connectivity requires four distinct source nodes."); + return; } - RawElement::Type elementType{}; - ElementFamily elementFamily{}; - std::size_t expectedFieldCount = 0U; - if (equalName(*type, "B33")) { - elementType = RawElement::Type::b33; - elementFamily = ElementFamily::beam; - expectedFieldCount = 3U; - } else if (equalName(*type, "S4")) { - elementType = RawElement::Type::s4; - elementFamily = ElementFamily::shell; - expectedFieldCount = 5U; - } else if (equalName(*type, "S4R")) { - elementType = RawElement::Type::s4r; - elementFamily = ElementFamily::shell; - expectedFieldCount = 5U; - } else { - inputFailure( - "unsupported-element-formulation", - block.location, - block.canonicalName, - *type, - "Only TYPE=B33, TYPE=S4, and TYPE=S4R belong to the approved element subsets."); - return; - } - if (modelElementFamily_ && *modelElementFamily_ != elementFamily) { - inputFailure( - "unsupported-mixed-element-model", - block.location, - block.canonicalName, - *type, - "Beam and shell elements cannot be mixed in one model."); - return; - } - modelElementFamily_ = elementFamily; - if (block.data.empty()) { - inputFailure( - elementFamily == ElementFamily::shell - ? "invalid-shell-connectivity" - : "invalid-data-arity", - block.location, block.canonicalName, "", - "ELEMENT requires at least one row with the approved connectivity arity."); - return; - } - for (const auto& row : block.data) { - if (row.fields.size() != expectedFieldCount) { - inputFailure( - elementFamily == ElementFamily::shell - ? "invalid-shell-connectivity" - : "invalid-data-arity", - row.location, block.canonicalName, "", - elementFamily == ElementFamily::shell - ? "S4 and S4R rows require a label and exactly four nodes." - : "B33 rows require label, node 1, node 2."); - return; - } - RawElement element{}; - element.labelText = row.fields[0]; - element.type = elementType; - element.location = row.location; - if (!parseInteger( - row.fields[0], element.label, row.location, - block.canonicalName, true)) { - return; - } - element.nodeLabels.reserve(expectedFieldCount - 1U); - for (std::size_t field = 1U; field < expectedFieldCount; ++field) { - std::int64_t nodeLabel = 0; - if (!parseInteger( - row.fields[field], nodeLabel, row.location, - block.canonicalName, true)) { - return; - } - element.nodeLabels.push_back(nodeLabel); - } - if (elementFamily == ElementFamily::shell) { - const std::set distinctNodes{ - element.nodeLabels.begin(), element.nodeLabels.end()}; - if (distinctNodes.size() != element.nodeLabels.size()) { - inputFailure( - "invalid-shell-connectivity", row.location, - block.canonicalName, element.labelText, - "Shell connectivity requires four distinct source nodes."); - return; - } - } - if (std::any_of( - part.elements.begin(), part.elements.end(), - [&element](const RawElement& existing) { + } + if (std::any_of(part.elements.begin(), part.elements.end(), + [&element](const RawElement& existing) { return existing.label == element.label; + })) { + InputFailure("duplicate-entity", row.location, block.canonical_name, + element.label_text, + "Element labels are unique within a part."); + return; + } + part.elements.push_back(std::move(element)); + } + } + + void ParseShellSection(const KeywordBlock& block, RawPart& part) { + for (const auto& candidate : block.parameters) { + if (candidate.name != "ELSET" && candidate.name != "MATERIAL") { + InputFailure("unsupported-shell-section-option", block.location, + block.canonical_name, candidate.name, + "The shell-section option is outside the centered " + "single-layer subset."); + return; + } + } + if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) { + return; + } + const auto* element_set = RequiredParameterValue(block, "ELSET"); + const auto* material = RequiredParameterValue(block, "MATERIAL"); + if (element_set == nullptr || material == nullptr) { + return; + } + if (block.data.size() != 1U || block.data[0].fields.empty() || + block.data[0].fields.size() > 2U) { + InputFailure("unsupported-shell-section-option", block.location, + block.canonical_name, *element_set, + "SHELL SECTION requires one thickness row with one optional " + "integration-point field."); + return; + } + double thickness = 0.0; + if (!ParseModelDouble(block.data[0].fields[0], thickness, + block.data[0].location, block.canonical_name, + "invalid-shell-thickness", + "Shell thickness must be finite and positive.")) { + return; + } + if (!(thickness > 0.0)) { + ModelFailure("invalid-shell-thickness", block.data[0].location, + block.canonical_name, *element_set, + "Shell thickness must be finite and positive."); + return; + } + if (block.data[0].fields.size() == 2U) { + std::int64_t ignored_integration_points = 0; + if (!TryPositiveInteger(block.data[0].fields[1], + ignored_integration_points)) { + InputFailure("unsupported-shell-section-option", block.data[0].location, + block.canonical_name, block.data[0].fields[1], + "The optional shell integration-point field must be a " + "positive integer."); + return; + } + } + part.shell_sections.push_back( + {*element_set, *material, thickness, block.location}); + } + + bool ParseSetMembers(const KeywordBlock& block, bool generate, + std::vector& members) { + if (block.data.empty()) { + return InputFailure("invalid-data-arity", block.location, + block.canonical_name, "", + "A set requires at least one member row."); + } + for (const auto& data : block.data) { + auto fields = WithoutTrailingEmpty(data.fields); + if (generate) { + if (fields.size() != 3U) { + return InputFailure("invalid-data-arity", data.location, + block.canonical_name, "", + "GENERATE rows require first, last, increment."); + } + std::int64_t first = 0; + std::int64_t last = 0; + std::int64_t increment = 0; + if (!ParseInteger(fields[0], first, data.location, block.canonical_name, + true) || + !ParseInteger(fields[1], last, data.location, block.canonical_name, + true)) { + return false; + } + if (!ParseInteger(fields[2], increment, data.location, + block.canonical_name, false) || + increment <= 0 || first > last || (last - first) % increment != 0) { + return InputFailure("invalid-set-range", data.location, + block.canonical_name, "", + "GENERATE requires an inclusive range reached by " + "a positive increment."); + } + for (std::int64_t label = first; label <= last;) { + members.push_back(label); + if (label > last - increment) { + break; + } + label += increment; + } + } else { + if (fields.empty() || std::any_of(fields.begin(), fields.end(), + [](const std::string& field) { + return field.empty(); + })) { + return InputFailure( + "invalid-data-arity", data.location, block.canonical_name, "", + "Explicit set rows require non-empty member labels."); + } + for (const auto& field : fields) { + std::int64_t label = 0; + if (!ParseInteger(field, label, data.location, block.canonical_name, + true)) { + return false; + } + members.push_back(label); + } + } + } + std::set unique; + for (const auto member : members) { + if (!unique.insert(member).second) { + return InputFailure("duplicate-entity", block.location, + block.canonical_name, std::to_string(member), + "A set cannot repeat the same source member."); + } + } + return true; + } + + void ParseSet(const KeywordBlock& block, bool node_set, RawPart& part) { + const std::string_view name_parameter = node_set ? "NSET" : "ELSET"; + if (!ValidateParameters(block, {name_parameter, "GENERATE"})) { + return; + } + const auto* name = RequiredParameterValue(block, name_parameter); + if (name == nullptr) { + return; + } + const auto* generate_parameter = Parameter(block, "GENERATE"); + if (generate_parameter != nullptr && generate_parameter->value) { + InputFailure("invalid-keyword-parameter", block.location, + block.canonical_name, "GENERATE", + "GENERATE is a valueless flag."); + return; + } + const auto& sets = node_set ? part.node_sets : part.element_sets; + if (ContainsSetName(sets, *name)) { + InputFailure("duplicate-entity", block.location, block.canonical_name, + *name, + "Set names are unique within their node-set or element-set " + "namespace."); + return; + } + RawSet set{*name, {}, block.location}; + if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) { + return; + } + if (node_set) { + part.node_sets.push_back(std::move(set)); + } else { + part.element_sets.push_back(std::move(set)); + } + } + + void ParseBeamSection(const KeywordBlock& block, RawPart& part) { + pending_section_.reset(); + if (!ValidateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) { + return; + } + const auto* element_set = RequiredParameterValue(block, "ELSET"); + const auto* material = RequiredParameterValue(block, "MATERIAL"); + const auto* section = RequiredParameterValue(block, "SECTION"); + if (element_set == nullptr || material == nullptr || section == nullptr) { + return; + } + if (!EqualName(*section, "GENERAL")) { + InputFailure("unsupported-section-formulation", block.location, + block.canonical_name, *section, + "Only SECTION=GENERAL belongs to the approved subset."); + return; + } + if (std::any_of(part.sections.begin(), part.sections.end(), + [element_set](const RawSection& existing) { + return EqualName(existing.element_set_name, *element_set); })) { - inputFailure( - "duplicate-entity", row.location, block.canonicalName, - element.labelText, - "Element labels are unique within a part."); - return; - } - part.elements.push_back(std::move(element)); - } + InputFailure("duplicate-entity", block.location, block.canonical_name, + *element_set, + "An element set can receive only one beam section."); + return; } - - void parseShellSection(const KeywordBlock& block, RawPart& part) { - for (const auto& candidate : block.parameters) { - if (candidate.name != "ELSET" && candidate.name != "MATERIAL") { - inputFailure( - "unsupported-shell-section-option", block.location, - block.canonicalName, candidate.name, - "The shell-section option is outside the centered single-layer subset."); - return; - } - } - if (!validateParameters(block, {"ELSET", "MATERIAL"})) { - return; - } - const auto* elementSet = requiredParameterValue(block, "ELSET"); - const auto* material = requiredParameterValue(block, "MATERIAL"); - if (elementSet == nullptr || material == nullptr) { - return; - } - if (block.data.size() != 1U || block.data[0].fields.empty() || - block.data[0].fields.size() > 2U) { - inputFailure( - "unsupported-shell-section-option", block.location, - block.canonicalName, *elementSet, - "SHELL SECTION requires one thickness row with one optional integration-point field."); - return; - } - double thickness = 0.0; - if (!parseModelDouble( - block.data[0].fields[0], thickness, - block.data[0].location, block.canonicalName, - "invalid-shell-thickness", - "Shell thickness must be finite and positive.")) { - return; - } - if (!(thickness > 0.0)) { - modelFailure( - "invalid-shell-thickness", block.data[0].location, - block.canonicalName, *elementSet, - "Shell thickness must be finite and positive."); - return; - } - if (block.data[0].fields.size() == 2U) { - std::int64_t ignoredIntegrationPoints = 0; - if (!tryPositiveInteger( - block.data[0].fields[1], ignoredIntegrationPoints)) { - inputFailure( - "unsupported-shell-section-option", block.data[0].location, - block.canonicalName, block.data[0].fields[1], - "The optional shell integration-point field must be a positive integer."); - return; - } - } - part.shellSections.push_back( - {*elementSet, *material, thickness, block.location}); + if (block.data.size() != 2U || block.data[0].fields.size() != 5U || + block.data[1].fields.size() != 3U) { + InputFailure("invalid-data-arity", block.location, block.canonical_name, + *element_set, + "A general section requires one five-field property row and " + "one three-field axis row."); + return; } - - bool parseSetMembers( - const KeywordBlock& block, - bool generate, - std::vector& members) { - if (block.data.empty()) { - return inputFailure( - "invalid-data-arity", block.location, block.canonicalName, - "", "A set requires at least one member row."); - } - for (const auto& data : block.data) { - auto fields = withoutTrailingEmpty(data.fields); - if (generate) { - if (fields.size() != 3U) { - return inputFailure( - "invalid-data-arity", data.location, - block.canonicalName, "", - "GENERATE rows require first, last, increment."); - } - std::int64_t first = 0; - std::int64_t last = 0; - std::int64_t increment = 0; - if (!parseInteger( - fields[0], first, data.location, - block.canonicalName, true) || - !parseInteger( - fields[1], last, data.location, - block.canonicalName, true)) { - return false; - } - if (!parseInteger( - fields[2], increment, data.location, - block.canonicalName, false) || - increment <= 0 || first > last || - (last - first) % increment != 0) { - return inputFailure( - "invalid-set-range", data.location, - block.canonicalName, "", - "GENERATE requires an inclusive range reached by a positive increment."); - } - for (std::int64_t label = first; label <= last;) { - members.push_back(label); - if (label > last - increment) { - break; - } - label += increment; - } - } else { - if (fields.empty() || - std::any_of( - fields.begin(), fields.end(), - [](const std::string& field) { return field.empty(); })) { - return inputFailure( - "invalid-data-arity", data.location, - block.canonicalName, "", - "Explicit set rows require non-empty member labels."); - } - for (const auto& field : fields) { - std::int64_t label = 0; - if (!parseInteger( - field, label, data.location, - block.canonicalName, true)) { - return false; - } - members.push_back(label); - } - } - } - std::set unique; - for (const auto member : members) { - if (!unique.insert(member).second) { - return inputFailure( - "duplicate-entity", block.location, - block.canonicalName, std::to_string(member), - "A set cannot repeat the same source member."); - } - } - return true; + RawSection raw{*element_set, *material, {}, {}, {}, block.location}; + for (std::size_t property = 0U; property < 5U; ++property) { + if (!ParseModelDouble(block.data[0].fields[property], + raw.properties[property], block.data[0].location, + block.canonical_name, "invalid-beam-property", + "Beam section properties must be finite.")) { + return; + } } + for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { + if (!ParseModelDouble(block.data[1].fields[coordinate], + raw.first_axis[coordinate], block.data[1].location, + block.canonical_name, "invalid-beam-guide-vector", + "The beam guide vector must be finite.")) { + return; + } + } + part.sections.push_back(std::move(raw)); + pending_section_ = part.sections.size() - 1U; + } - void parseSet(const KeywordBlock& block, bool nodeSet, RawPart& part) { - const std::string_view nameParameter = nodeSet ? "NSET" : "ELSET"; - if (!validateParameters(block, {nameParameter, "GENERATE"})) { - return; - } - const auto* name = requiredParameterValue(block, nameParameter); - if (name == nullptr) { - return; - } - const auto* generateParameter = parameter(block, "GENERATE"); - if (generateParameter != nullptr && generateParameter->value) { - inputFailure( - "invalid-keyword-parameter", block.location, - block.canonicalName, "GENERATE", - "GENERATE is a valueless flag."); - return; - } - const auto& sets = nodeSet ? part.nodeSets : part.elementSets; - if (containsSetName(sets, *name)) { - inputFailure( - "duplicate-entity", block.location, block.canonicalName, - *name, - "Set names are unique within their node-set or element-set namespace."); - return; - } - RawSet set{*name, {}, block.location}; - if (!parseSetMembers(block, generateParameter != nullptr, set.members)) { - return; - } - if (nodeSet) { - part.nodeSets.push_back(std::move(set)); + void ParseSectionPoints(const KeywordBlock& block, RawPart& part) { + if (!pending_section_ || !ValidateParameters(block, {}) || + block.data.empty()) { + if (!failure_) { + InputFailure( + "invalid-keyword-location", block.location, block.canonical_name, + "", "SECTION POINTS must immediately follow a general section."); + } + return; + } + RawSection& section = part.sections[*pending_section_]; + for (const auto& row : block.data) { + if (row.fields.size() != 2U) { + InputFailure("invalid-data-arity", row.location, block.canonical_name, + section.element_set_name, + "Section-point rows require x1 and x2."); + return; + } + std::array point{}; + if (!ParseDouble(row.fields[0], point[0], row.location, + block.canonical_name) || + !ParseDouble(row.fields[1], point[1], row.location, + block.canonical_name)) { + return; + } + if (std::find(section.section_points.begin(), + section.section_points.end(), + point) != section.section_points.end()) { + InputFailure("duplicate-entity", row.location, block.canonical_name, + section.element_set_name, + "Section points must be unique within a section."); + return; + } + section.section_points.push_back(point); + } + pending_section_.reset(); + } + + void ParseAssemblyStart(const KeywordBlock& block) { + if (assembly_seen_) { + InputFailure("unsupported-nested-assembly", block.location, + block.canonical_name, "", + "V0 accepts exactly one non-nested assembly."); + return; + } + if (parts_.empty() || !materials_.empty() || model_boundary_seen_) { + InvalidKeywordLocation( + block, + "The sole assembly follows all part blocks and precedes model data."); + return; + } + if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) { + return; + } + if (RequiredParameterValue(block, "NAME") == nullptr) { + return; + } + assembly_seen_ = true; + in_assembly_ = true; + assembly_set_seen_ = false; + } + + void ParseAssemblyBlock(const KeywordBlock& block) { + if (block.canonical_name == "END ASSEMBLY") { + if (!ValidateParameters(block, {}) || !RequireNoData(block)) { + return; + } + if (instances_.empty()) { + InvalidKeywordLocation( + block, "The assembly requires at least one identity instance."); + return; + } + in_assembly_ = false; + return; + } + if (block.canonical_name == "ASSEMBLY") { + InputFailure("unsupported-nested-assembly", block.location, + block.canonical_name, "", + "Nested or duplicate assembly blocks are unsupported."); + } else if (block.canonical_name == "INSTANCE") { + if (assembly_set_seen_) { + InvalidKeywordLocation( + block, "All identity instances must precede assembly-level sets."); + return; + } + ParseInstanceStart(block); + } else if (block.canonical_name == "NSET") { + if (instances_.empty()) { + InvalidKeywordLocation( + block, "Assembly sets follow at least one identity instance."); + return; + } + assembly_set_seen_ = true; + ParseAssemblySet(block, true); + } else if (block.canonical_name == "ELSET") { + if (instances_.empty()) { + InvalidKeywordLocation( + block, "Assembly sets follow at least one identity instance."); + return; + } + assembly_set_seen_ = true; + ParseAssemblySet(block, false); + } else { + RejectUnknown(block); + } + } + + void ParseInstanceStart(const KeywordBlock& block) { + if (Parameter(block, "DEPENDENT") != nullptr || + Parameter(block, "INDEPENDENT") != nullptr) { + InputFailure( + "unsupported-instance-mesh-semantics", block.location, + block.canonical_name, "", + "Dependent and independent instance mesh semantics are unsupported."); + return; + } + if (!ValidateParameters(block, {"NAME", "PART"})) { + return; + } + const auto* name = RequiredParameterValue(block, "NAME"); + const auto* part = RequiredParameterValue(block, "PART"); + if (name == nullptr || part == nullptr) { + return; + } + if (!block.data.empty()) { + InputFailure("unsupported-instance-transform", + block.data.front().location, block.canonical_name, *name, + "Instance translation or rotation data is unsupported."); + return; + } + if (ContainsName(instances_, *name)) { + InputFailure("duplicate-entity", block.location, block.canonical_name, + *name, "Instance names are globally unique."); + return; + } + instances_.push_back({*name, *part, block.location}); + in_instance_ = true; + } + + void ParseInstanceBlock(const KeywordBlock& block) { + if (block.canonical_name == "END INSTANCE") { + if (!ValidateParameters(block, {}) || !RequireNoData(block)) { + return; + } + in_instance_ = false; + return; + } + if (block.canonical_name == "ASSEMBLY") { + InputFailure("unsupported-nested-assembly", block.location, + block.canonical_name, "", + "An assembly cannot be nested in an instance."); + return; + } + InputFailure("unsupported-instance-mesh-semantics", block.location, + block.canonical_name, instances_.back().name, + "Instance-local mesh definitions are unsupported."); + } + + void ParseAssemblySet(const KeywordBlock& block, bool node_set) { + const std::string_view name_parameter = node_set ? "NSET" : "ELSET"; + if (!ValidateParameters(block, {name_parameter, "INSTANCE", "GENERATE"})) { + return; + } + const auto* name = RequiredParameterValue(block, name_parameter); + const auto* instance = RequiredParameterValue(block, "INSTANCE"); + if (name == nullptr || instance == nullptr) { + return; + } + const auto* generate_parameter = Parameter(block, "GENERATE"); + if (generate_parameter != nullptr && generate_parameter->value) { + InputFailure("invalid-keyword-parameter", block.location, + block.canonical_name, "GENERATE", + "GENERATE is a valueless flag."); + return; + } + if (ContainsAssemblySetName(node_set, *name)) { + InputFailure("duplicate-entity", block.location, block.canonical_name, + *name, + "Assembly set names are unique within their node-set or " + "element-set namespace."); + return; + } + RawAssemblySet set{node_set, *name, *instance, {}, block.location}; + if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) { + return; + } + assembly_sets_.push_back(std::move(set)); + } + + void ParseMaterial(const KeywordBlock& block) { + if (!assembly_seen_ || model_boundary_seen_) { + InvalidKeywordLocation(block, + "Material definitions follow the assembly and " + "precede model boundaries."); + return; + } + if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) { + return; + } + const auto* name = RequiredParameterValue(block, "NAME"); + if (name == nullptr) { + return; + } + if (ContainsName(materials_, *name)) { + InputFailure("duplicate-entity", block.location, block.canonical_name, + *name, "Material names are globally unique."); + return; + } + materials_.push_back({*name, 0.0, 0.0, false, block.location, {}}); + material_eligible_ = materials_.size() - 1U; + } + + void ParseElastic(const KeywordBlock& block) { + if (!material_eligible_) { + InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", + "ELASTIC must immediately follow MATERIAL."); + return; + } + if (!ValidateParameters(block, {}) || block.data.size() != 1U || + block.data[0].fields.size() != 2U) { + if (!failure_) { + InputFailure("invalid-data-arity", block.location, block.canonical_name, + materials_[*material_eligible_].name, + "ELASTIC requires exactly one E, nu row."); + } + return; + } + RawMaterial& material = materials_[*material_eligible_]; + if (material.has_elastic) { + InputFailure("duplicate-entity", block.location, block.canonical_name, + material.name, "A material accepts one ELASTIC definition."); + return; + } + if (!ParseModelDouble(block.data[0].fields[0], material.youngs_modulus, + block.data[0].location, block.canonical_name, + model_element_family_ == ElementFamily::kShell + ? "invalid-shell-material" + : "invalid-beam-property", + "Elastic material values must be finite.") || + !ParseModelDouble(block.data[0].fields[1], material.poisson_ratio, + block.data[0].location, block.canonical_name, + model_element_family_ == ElementFamily::kShell + ? "invalid-shell-material" + : "invalid-beam-property", + "Elastic material values must be finite.")) { + return; + } + material.has_elastic = true; + material.elastic_location = block.data[0].location; + } + + void ParseBoundary(const KeywordBlock& block, + std::vector& destination) { + if (!ValidateParameters(block, {}) || block.data.empty()) { + if (!failure_) { + InputFailure("invalid-data-arity", block.location, block.canonical_name, + "", "BOUNDARY requires one or more data rows."); + } + return; + } + for (const auto& row : block.data) { + if (row.fields.size() != 3U && row.fields.size() != 4U) { + InputFailure("invalid-data-arity", row.location, block.canonical_name, + "", + "BOUNDARY rows require target, first DOF, last DOF, and " + "optional value."); + return; + } + if (row.fields[0].empty()) { + InputFailure("unresolved-reference", row.location, block.canonical_name, + "", "A boundary target cannot be empty."); + return; + } + std::int64_t first = 0; + std::int64_t last = 0; + if (!ParseInteger(row.fields[1], first, row.location, + block.canonical_name, true) || + !ParseInteger(row.fields[2], last, row.location, block.canonical_name, + true)) { + return; + } + if (first < 1 || first > 6 || last < first || last > 6) { + InputFailure("invalid-dof", row.location, block.canonical_name, + row.fields[0], + "Boundary DOFs must be an ordered range in 1..6."); + return; + } + double value = 0.0; + if (row.fields.size() == 4U && + !ParseDouble(row.fields[3], value, row.location, + block.canonical_name)) { + return; + } + destination.push_back({row.fields[0], static_cast(first), + static_cast(last), value, row.location}); + } + } + + void ParseCload(const KeywordBlock& block, std::vector& loads) { + if (!ValidateParameters(block, {}) || block.data.empty()) { + if (!failure_) { + InputFailure("invalid-data-arity", block.location, block.canonical_name, + "", "CLOAD requires one or more rows."); + } + return; + } + for (const auto& row : block.data) { + if (row.fields.size() != 3U || row.fields[0].empty()) { + InputFailure("invalid-data-arity", row.location, block.canonical_name, + "", "CLOAD rows require target, DOF, magnitude."); + return; + } + std::int64_t dof = 0; + double magnitude = 0.0; + if (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name, + true) || + !ParseDouble(row.fields[2], magnitude, row.location, + block.canonical_name)) { + return; + } + if (dof < 1 || dof > 6) { + InputFailure("invalid-dof", row.location, block.canonical_name, + row.fields[0], "CLOAD DOF must be in 1..6."); + return; + } + loads.push_back( + {row.fields[0], static_cast(dof), magnitude, row.location}); + } + } + + void ParseStepStart(const KeywordBlock& block) { + if (step_seen_) { + InputFailure("unsupported-multiple-step", block.location, + block.canonical_name, "", + "V0 accepts exactly one analysis step."); + return; + } + if (!assembly_seen_ || materials_.empty()) { + InvalidKeywordLocation(block, + "The sole step follows the complete assembly and " + "material definitions."); + return; + } + if (!ValidateParameters(block, {"NAME", "NLGEOM"}) || + !RequireNoData(block)) { + return; + } + const auto* nlgeom = Parameter(block, "NLGEOM"); + if (nlgeom != nullptr) { + if (!nlgeom->value || nlgeom->value->empty()) { + InputFailure("invalid-keyword-parameter", block.location, + block.canonical_name, "NLGEOM", + "NLGEOM requires NO in the approved subset."); + return; + } + if (!EqualName(*nlgeom->value, "NO")) { + if (model_element_family_ == ElementFamily::kShell) { + InputFailure("unsupported-nonlinear-geometry", block.location, + block.canonical_name, *nlgeom->value, + "Only absent NLGEOM or NLGEOM=NO is supported."); } else { - part.elementSets.push_back(std::move(set)); + ModelFailure("unsupported-nonlinear-geometry", block.location, + block.canonical_name, *nlgeom->value, + "Only absent NLGEOM or NLGEOM=NO is supported."); } + return; + } + } + step_seen_ = true; + in_step_ = true; + step_ = RawStep{block.location, false, {}, {}, {}}; + step_load_seen_ = false; + step_no_op_seen_ = false; + } + + void ParseStepBlock(const KeywordBlock& block) { + if (block.canonical_name == "END STEP") { + active_output_ = false; + if (!ValidateParameters(block, {}) || !RequireNoData(block)) { + return; + } + if (!step_.has_static) { + InvalidKeywordLocation( + block, + "The sole step requires exactly one leading STATIC procedure."); + return; + } + in_step_ = false; + return; + } + if (block.canonical_name == "STEP") { + InputFailure("unsupported-multiple-step", block.location, + block.canonical_name, "", + "A second or nested step is unsupported."); + return; + } + if (block.canonical_name == "STATIC") { + active_output_ = false; + if (!step_.boundaries.empty() || !step_.loads.empty() || + step_no_op_seen_) { + InvalidKeywordLocation( + block, "STATIC must be the first keyword in the sole step."); + return; + } + ParseStatic(block); + } else if (block.canonical_name == "BOUNDARY") { + active_output_ = false; + if (!step_.has_static || step_load_seen_ || step_no_op_seen_) { + InvalidKeywordLocation( + block, + "Step boundaries follow STATIC and precede loads " + "and no-op requests."); + return; + } + ParseBoundary(block, step_.boundaries); + } else if (block.canonical_name == "CLOAD") { + active_output_ = false; + if (!step_.has_static || step_no_op_seen_) { + InvalidKeywordLocation( + block, + "CLOAD follows STATIC and boundaries and precedes no-op requests."); + return; + } + ParseCload(block, step_.loads); + step_load_seen_ = !failure_; + } else if (block.canonical_name == "RESTART") { + active_output_ = false; + if (!step_.has_static) { + InvalidKeywordLocation( + block, "Step no-op requests follow STATIC, boundaries, and loads."); + return; + } + step_no_op_seen_ = true; + if (RequireNoData(block)) { + AddIgnoredWarning(block); + } + } else if (block.canonical_name == "OUTPUT") { + if (!step_.has_static) { + InvalidKeywordLocation( + block, "Step no-op requests follow STATIC, boundaries, and loads."); + return; + } + step_no_op_seen_ = true; + ParseOutputRoot(block); + } else if (block.canonical_name == "NODE OUTPUT" || + block.canonical_name == "ELEMENT OUTPUT" || + block.canonical_name == "CONTACT OUTPUT") { + ParseOutputChild(block); + } else { + active_output_ = false; + RejectUnknown(block); + } + } + + void ParseStatic(const KeywordBlock& block) { + if (step_.has_static || !ValidateParameters(block, {}) || + block.data.size() != 1U || block.data[0].fields.size() != 4U) { + if (!failure_) { + InputFailure("invalid-static-data", block.location, + block.canonical_name, "", + "STATIC requires exactly one row of four values."); + } + return; + } + for (std::size_t field = 0U; field < 4U; ++field) { + if (!ParseDouble(block.data[0].fields[field], step_.static_values[field], + block.data[0].location, block.canonical_name)) { + return; + } + if (step_.static_values[field] <= 0.0) { + InputFailure("invalid-static-data", block.data[0].location, + block.canonical_name, "", + "All four STATIC fields must be positive."); + return; + } + } + if (step_.static_values[2] > step_.static_values[3]) { + InputFailure("invalid-static-data", block.data[0].location, + block.canonical_name, "", + "STATIC minimum increment cannot exceed maximum increment."); + return; + } + step_.has_static = true; + } + + void ParseOutputRoot(const KeywordBlock& block) { + const auto* field = Parameter(block, "FIELD"); + const auto* history = Parameter(block, "HISTORY"); + if ((field == nullptr) == (history == nullptr) || + (field != nullptr && field->value) || + (history != nullptr && history->value)) { + InputFailure("unsupported-keyword", block.location, block.canonical_name, + "", "OUTPUT must select exactly FIELD or HISTORY."); + return; + } + active_output_ = true; + AddIgnoredWarning(block); + } + + void ParseOutputChild(const KeywordBlock& block) { + if (!active_output_) { + InputFailure( + "invalid-keyword-location", block.location, block.canonical_name, "", + "Output variable keywords require an active OUTPUT request."); + return; + } + AddIgnoredWarning(block); + } + + void AddIgnoredWarning(const KeywordBlock& block) { + // One warning per allowlisted keyword keeps no-op provenance stable; + // subordinate variable rows remain attached to that keyword record. + definition_.warnings.push_back( + {Severity::kWarning, "ignored-input-keyword", block.location, + block.canonical_name, "", + "The allowlisted Abaqus keyword is ignored without semantic effect."}); + } + + void RejectUnknown(const KeywordBlock& block) { + if (block.canonical_name == "DLOAD" && + model_element_family_ == ElementFamily::kShell) { + InputFailure("unsupported-distributed-load", block.location, + block.canonical_name, "", + "Distributed, pressure, gravity, body, edge, and follower " + "loads are unsupported."); + return; + } + InputFailure("unsupported-keyword", block.location, block.canonical_name, + "", "The keyword is outside the approved Abaqus subset."); + } + + const RawPart* FindPart(const std::string& name) const { + const auto found = std::find_if( + parts_.begin(), parts_.end(), + [&name](const RawPart& part) { return EqualName(part.name, name); }); + return found == parts_.end() ? nullptr : &*found; + } + + const RawInstance* FindInstance(const std::string& name) const { + const auto found = std::find_if(instances_.begin(), instances_.end(), + [&name](const RawInstance& instance) { + return EqualName(instance.name, name); + }); + return found == instances_.end() ? nullptr : &*found; + } + + std::optional FindMaterialIndex(const std::string& name) const { + for (std::size_t index = 0U; index < materials_.size(); ++index) { + if (EqualName(materials_[index].name, name)) { + return static_cast(index); + } + } + return std::nullopt; + } + + const RawSet* FindSet(const std::vector& sets, + const std::string& name) const { + const auto found = std::find_if( + sets.begin(), sets.end(), + [&name](const RawSet& set) { return EqualName(set.name, name); }); + return found == sets.end() ? nullptr : &*found; + } + + const RawNode* FindNode(const RawPart& part, std::int64_t label) const { + const auto found = std::find_if( + part.nodes.begin(), part.nodes.end(), + [label](const RawNode& node) { return node.label == label; }); + return found == part.nodes.end() ? nullptr : &*found; + } + + const RawElement* FindElement(const RawPart& part, std::int64_t label) const { + const auto found = std::find_if( + part.elements.begin(), part.elements.end(), + [label](const RawElement& element) { return element.label == label; }); + return found == part.elements.end() ? nullptr : &*found; + } + + void FinalizeModel() { + if (parts_.empty() || !assembly_seen_ || instances_.empty() || + materials_.empty() || !step_seen_ || !step_.has_static) { + const auto location = input_.blocks.empty() + ? SourceLocation{input_.source_path, 0U} + : input_.blocks.back().location; + InputFailure("invalid-model-cardinality", location, "", "", + "The model requires part, assembly, identity instance, " + "material, and one STATIC step."); + return; } - void parseBeamSection(const KeywordBlock& block, RawPart& part) { - pendingSection_.reset(); - if (!validateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) { - return; - } - const auto* elementSet = requiredParameterValue(block, "ELSET"); - const auto* material = requiredParameterValue(block, "MATERIAL"); - const auto* section = requiredParameterValue(block, "SECTION"); - if (elementSet == nullptr || material == nullptr || section == nullptr) { - return; - } - if (!equalName(*section, "GENERAL")) { - inputFailure( - "unsupported-section-formulation", block.location, - block.canonicalName, *section, - "Only SECTION=GENERAL belongs to the approved subset."); - return; - } - if (std::any_of( - part.sections.begin(), part.sections.end(), - [elementSet](const RawSection& existing) { - return equalName(existing.elementSetName, *elementSet); - })) { - inputFailure( - "duplicate-entity", block.location, block.canonicalName, - *elementSet, - "An element set can receive only one beam section."); - return; - } - if (block.data.size() != 2U || block.data[0].fields.size() != 5U || - block.data[1].fields.size() != 3U) { - inputFailure( - "invalid-data-arity", block.location, block.canonicalName, - *elementSet, - "A general section requires one five-field property row and one three-field axis row."); - return; - } - RawSection raw{*elementSet, *material, {}, {}, {}, block.location}; - for (std::size_t property = 0U; property < 5U; ++property) { - if (!parseModelDouble( - block.data[0].fields[property], raw.properties[property], - block.data[0].location, block.canonicalName, - "invalid-beam-property", - "Beam section properties must be finite.")) { - return; - } - } - for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { - if (!parseModelDouble( - block.data[1].fields[coordinate], raw.firstAxis[coordinate], - block.data[1].location, block.canonicalName, - "invalid-beam-guide-vector", - "The beam guide vector must be finite.")) { - return; - } - } - part.sections.push_back(std::move(raw)); - pendingSection_ = part.sections.size() - 1U; + FinalizeMaterials(); + if (failure_) { + return; } - - void parseSectionPoints(const KeywordBlock& block, RawPart& part) { - if (!pendingSection_ || !validateParameters(block, {}) || - block.data.empty()) { - if (!failure_) { - inputFailure( - "invalid-keyword-location", block.location, - block.canonicalName, "", - "SECTION POINTS must immediately follow a general section."); - } - return; - } - RawSection& section = part.sections[*pendingSection_]; - for (const auto& row : block.data) { - if (row.fields.size() != 2U) { - inputFailure( - "invalid-data-arity", row.location, - block.canonicalName, section.elementSetName, - "Section-point rows require x1 and x2."); - return; - } - std::array point{}; - if (!parseDouble( - row.fields[0], point[0], row.location, - block.canonicalName) || - !parseDouble( - row.fields[1], point[1], row.location, - block.canonicalName)) { - return; - } - if (std::find( - section.sectionPoints.begin(), - section.sectionPoints.end(), point) != - section.sectionPoints.end()) { - inputFailure( - "duplicate-entity", row.location, - block.canonicalName, section.elementSetName, - "Section points must be unique within a section."); - return; - } - section.sectionPoints.push_back(point); - } - pendingSection_.reset(); + FinalizePartsAndSections(); + if (failure_) { + return; } - - void parseAssemblyStart(const KeywordBlock& block) { - if (assemblySeen_) { - inputFailure( - "unsupported-nested-assembly", block.location, - block.canonicalName, "", - "V0 accepts exactly one non-nested assembly."); - return; - } - if (parts_.empty() || !materials_.empty() || modelBoundarySeen_) { - invalidKeywordLocation( - block, - "The sole assembly follows all part blocks and precedes model data."); - return; - } - if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) { - return; - } - if (requiredParameterValue(block, "NAME") == nullptr) { - return; - } - assemblySeen_ = true; - inAssembly_ = true; - assemblySetSeen_ = false; + ExpandInstances(); + if (failure_) { + return; } + if (!definition_.shell_elements.empty()) { + auto geometry = + PreprocessShellGeometry(definition_.nodes, definition_.shell_elements, + definition_.shell_sections); + if (!geometry.HasValue()) { + const auto& status = geometry.GetStatus(); + failure_ = + MappingFailure{status.Category().value_or(FailureCategory::kModel), + status.Diagnostics().front()}; + return; + } + definition_.shell_node_initial_frames = + std::move(geometry.Value().nodal_frames); + } + ExpandAssemblySets(); + if (failure_) { + return; + } + FinalizeStep(); + } - void parseAssemblyBlock(const KeywordBlock& block) { - if (block.canonicalName == "END ASSEMBLY") { - if (!validateParameters(block, {}) || !requireNoData(block)) { - return; - } - if (instances_.empty()) { - invalidKeywordLocation( - block, - "The assembly requires at least one identity instance."); - return; - } - inAssembly_ = false; - return; + void FinalizeMaterials() { + for (const auto& material : materials_) { + if (!material.has_elastic) { + InputFailure("unresolved-reference", material.location, "MATERIAL", + material.name, + "A material must own exactly one ELASTIC row."); + return; + } + if (model_element_family_ == ElementFamily::kShell) { + if (!(material.youngs_modulus > 0.0) || + !(material.poisson_ratio > -1.0) || + !(material.poisson_ratio < 0.5)) { + ModelFailure( + "invalid-shell-material", material.elastic_location, "ELASTIC", + material.name, + "Shell isotropic elasticity requires E>0 and -1 0.0) || !std::isfinite(shear_modulus) || + !(shear_modulus > 0.0)) { + ModelFailure("invalid-beam-property", material.elastic_location, + "ELASTIC", material.name, + "E and the derived G=E/(2*(1+nu)) must be positive."); + return; + } + definition_.materials.push_back({material.name, material.youngs_modulus, + material.poisson_ratio, + material.location}); + } + } + + void FinalizePartsAndSections() { + part_section_assignments_.resize(parts_.size()); + part_shell_section_assignments_.resize(parts_.size()); + const bool shell_model = model_element_family_ == ElementFamily::kShell; + for (std::size_t part_index = 0U; part_index < parts_.size(); + ++part_index) { + const RawPart& part = parts_[part_index]; + if (part.nodes.empty() || part.elements.empty() || + (!shell_model && part.sections.empty())) { + InputFailure("invalid-model-cardinality", part.location, "PART", + part.name, + "A part requires nodes, elements, and its approved " + "section form."); + return; + } + + PartDefinition part_definition{}; + part_definition.name = part.name; + part_definition.location = part.location; + for (const auto& node : part.nodes) { + part_definition.node_source_labels.push_back(node.label); + } + for (const auto& element : part.elements) { + part_definition.element_source_labels.push_back(element.label); + for (const auto node_label : element.node_labels) { + if (FindNode(part, node_label) == nullptr) { + InputFailure(shell_model ? "invalid-shell-connectivity" + : "unresolved-reference", + element.location, "ELEMENT", element.label_text, + "Element connectivity must resolve within its part."); + return; + } + } + } + for (const auto& set : part.node_sets) { + part_definition.node_set_names.push_back(set.name); + for (const auto label : set.members) { + if (FindNode(part, label) == nullptr) { + InputFailure("unresolved-reference", set.location, "NSET", set.name, + "Every node-set member must resolve within its part."); + return; + } + } + } + for (const auto& set : part.element_sets) { + part_definition.element_set_names.push_back(set.name); + for (const auto label : set.members) { + if (FindElement(part, label) == nullptr) { + InputFailure( + "unresolved-reference", set.location, "ELSET", set.name, + "Every element-set member must resolve within its part."); + return; + } + } + } + definition_.parts.push_back(std::move(part_definition)); + + if (shell_model) { + auto& assignments = part_shell_section_assignments_[part_index]; + for (const auto& section : part.shell_sections) { + const auto* element_set = + FindSet(part.element_sets, section.element_set_name); + const auto material_index = FindMaterialIndex(section.material_name); + if (element_set == nullptr || !material_index) { + InputFailure("unresolved-shell-section", section.location, + "SHELL SECTION", section.element_set_name, + "Shell section ELSET and MATERIAL references " + "must resolve."); + return; + } + const EntityIndex section_index = + static_cast(definition_.shell_sections.size()); + definition_.shell_sections.push_back( + {section.element_set_name, section.thickness, *material_index, + section.location}); + for (const auto element_label : element_set->members) { + if (!assignments + .emplace(element_label, + std::make_pair(section_index, *material_index)) + .second) { + InputFailure("invalid-shell-section-assignment", section.location, + "SHELL SECTION", std::to_string(element_label), + "A shell element cannot receive multiple " + "section assignments."); + return; } - parseInstanceStart(block); - } else if (block.canonicalName == "NSET") { - if (instances_.empty()) { - invalidKeywordLocation( - block, - "Assembly sets follow at least one identity instance."); - return; - } - assemblySetSeen_ = true; - parseAssemblySet(block, true); - } else if (block.canonicalName == "ELSET") { - if (instances_.empty()) { - invalidKeywordLocation( - block, - "Assembly sets follow at least one identity instance."); - return; - } - assemblySetSeen_ = true; - parseAssemblySet(block, false); + } + } + for (const auto& element : part.elements) { + if (assignments.find(element.label) == assignments.end()) { + InputFailure("invalid-shell-section-assignment", element.location, + "ELEMENT", element.label_text, + "Every shell element requires exactly one " + "resolved section assignment."); + return; + } + } + continue; + } + + auto& assignments = part_section_assignments_[part_index]; + for (const auto& section : part.sections) { + const auto* element_set = + FindSet(part.element_sets, section.element_set_name); + const auto material_index = FindMaterialIndex(section.material_name); + if (element_set == nullptr || !material_index) { + InputFailure("unresolved-reference", section.location, + "BEAM GENERAL SECTION", section.element_set_name, + "Section ELSET and MATERIAL references must resolve."); + return; + } + if (section.properties[2] != 0.0) { + ModelFailure("unsupported-coupled-section", section.location, + "BEAM GENERAL SECTION", section.element_set_name, + "V0 requires exact I12=0."); + return; + } + if (!(section.properties[0] > 0.0) || !(section.properties[1] > 0.0) || + !(section.properties[3] > 0.0) || !(section.properties[4] > 0.0)) { + ModelFailure("invalid-beam-property", section.location, + "BEAM GENERAL SECTION", section.element_set_name, + "A, I11, I22, and J must be positive."); + return; + } + const EntityIndex section_index = + static_cast(definition_.sections.size()); + definition_.sections.push_back( + {section.element_set_name, section.properties[0], + section.properties[1], section.properties[2], + section.properties[3], section.properties[4], section.first_axis, + section.section_points, section.location}); + for (const auto element_label : element_set->members) { + if (!assignments + .emplace(element_label, + std::make_pair(section_index, *material_index)) + .second) { + InputFailure("duplicate-entity", section.location, + "BEAM GENERAL SECTION", std::to_string(element_label), + "An element cannot receive multiple section " + "assignments."); + return; + } + } + } + for (const auto& element : part.elements) { + if (assignments.find(element.label) == assignments.end()) { + InputFailure( + "unresolved-reference", element.location, "ELEMENT", + element.label_text, + "Every B33 element requires a resolved section assignment."); + return; + } + } + } + } + + std::size_t PartIndex(const RawPart& part) const { + return static_cast(&part - parts_.data()); + } + + bool ValidateGeometry(const RawElement& raw, const Node& first, + const Node& second, const GeneralBeamSection& section) { + const auto norm = [](const std::array& vector) { + return std::hypot(vector[0], vector[1], vector[2]); + }; + const auto maximum_absolute = [](const std::array& vector) { + return std::max( + {std::abs(vector[0]), std::abs(vector[1]), std::abs(vector[2])}); + }; + + // Compare both approved inequalities after a common scaling. This + // preserves the exact ratios while avoiding overflow in x*x and in + // subtraction between large finite coordinates. + const double global_coordinate_scale = + std::max({1.0, maximum_absolute(first.coordinates), + maximum_absolute(second.coordinates)}); + std::array first_scaled{}; + std::array second_scaled{}; + std::array delta_scaled{}; + for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { + first_scaled[coordinate] = + first.coordinates[coordinate] / global_coordinate_scale; + second_scaled[coordinate] = + second.coordinates[coordinate] / global_coordinate_scale; + delta_scaled[coordinate] = + second_scaled[coordinate] - first_scaled[coordinate]; + } + const double length_ratio = norm(delta_scaled); + const double coordinate_norm_ratio = + std::max({1.0 / global_coordinate_scale, norm(first_scaled), + norm(second_scaled)}); + if (!(length_ratio > 1.0e-12 * coordinate_norm_ratio)) { + return ModelFailure( + "invalid-beam-length", raw.location, "ELEMENT", raw.label_text, + "Beam length fails the approved scale-aware threshold."); + } + std::array tangent{delta_scaled[0] / length_ratio, + delta_scaled[1] / length_ratio, + delta_scaled[2] / length_ratio}; + + const double global_guide_scale = + std::max(1.0, maximum_absolute(section.first_axis)); + std::array guide_scaled{}; + for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { + guide_scaled[coordinate] = + section.first_axis[coordinate] / global_guide_scale; + } + const double projection = guide_scaled[0] * tangent[0] + + guide_scaled[1] * tangent[1] + + guide_scaled[2] * tangent[2]; + std::array perpendicular{ + guide_scaled[0] - projection * tangent[0], + guide_scaled[1] - projection * tangent[1], + guide_scaled[2] - projection * tangent[2]}; + const double guide_norm_ratio = + std::max(1.0 / global_guide_scale, norm(guide_scaled)); + if (!(norm(perpendicular) > 1.0e-12 * guide_norm_ratio)) { + return ModelFailure( + "invalid-beam-guide-vector", section.location, "BEAM GENERAL SECTION", + raw.label_text, + "The first section axis cannot be zero or tangent-parallel."); + } + return true; + } + + void ExpandInstances() { + for (const auto& raw_instance : instances_) { + const RawPart* part = FindPart(raw_instance.part_name); + if (part == nullptr) { + InputFailure("unresolved-reference", raw_instance.location, "INSTANCE", + raw_instance.name, + "INSTANCE PART must resolve case-insensitively."); + return; + } + InstanceDefinition instance{ + raw_instance.name, part->name, {}, {}, raw_instance.location}; + std::map node_indices; + std::map element_indices; + + // Instance order, followed by part-local declaration order, is the + // sole source of stable expanded internal IDs. + for (const auto& raw_node : part->nodes) { + if (definition_.nodes.size() > + std::numeric_limits::max()) { + InputFailure("entity-index-overflow", raw_node.location, "NODE", + raw_node.label_text, + "Expanded node count exceeds EntityIndex capacity."); + return; + } + const EntityIndex index = + static_cast(definition_.nodes.size()); + definition_.nodes.push_back( + {{raw_instance.name, raw_node.label, raw_node.label_text}, + raw_node.coordinates, + raw_node.location}); + node_indices.emplace(raw_node.label, index); + instance.node_mappings.push_back({raw_node.label, index}); + } + + for (const auto& raw_element : part->elements) { + EntityIndex index = 0U; + if (raw_element.type == RawElement::Type::kB33) { + const auto first = node_indices.find(raw_element.node_labels[0]); + const auto second = node_indices.find(raw_element.node_labels[1]); + const auto& assignments = part_section_assignments_[PartIndex(*part)]; + const auto assignment = assignments.find(raw_element.label); + if (first == node_indices.end() || second == node_indices.end() || + assignment == assignments.end()) { + InputFailure( + "unresolved-reference", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded connectivity and section assignment must resolve."); + return; + } + if (definition_.elements.size() > + std::numeric_limits::max()) { + InputFailure( + "entity-index-overflow", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded element count exceeds EntityIndex capacity."); + return; + } + index = static_cast(definition_.elements.size()); + const auto& section = definition_.sections[assignment->second.first]; + if (!ValidateGeometry(raw_element, definition_.nodes[first->second], + definition_.nodes[second->second], section)) { + return; + } + definition_.elements.push_back( + {{raw_instance.name, raw_element.label, raw_element.label_text}, + {first->second, second->second}, + assignment->second.second, + assignment->second.first, + raw_element.location}); } else { - rejectUnknown(block); + std::array connected_nodes{}; + for (std::size_t node = 0U; node < connected_nodes.size(); ++node) { + const auto found = node_indices.find(raw_element.node_labels[node]); + if (found == node_indices.end()) { + InputFailure("invalid-shell-connectivity", raw_element.location, + "ELEMENT", raw_element.label_text, + "Expanded shell connectivity must resolve."); + return; + } + connected_nodes[node] = found->second; + } + const auto& assignments = + part_shell_section_assignments_[PartIndex(*part)]; + const auto assignment = assignments.find(raw_element.label); + if (assignment == assignments.end()) { + InputFailure( + "invalid-shell-section-assignment", raw_element.location, + "ELEMENT", raw_element.label_text, + "Expanded shell section assignment must resolve exactly once."); + return; + } + if (definition_.shell_elements.size() > + std::numeric_limits::max()) { + InputFailure( + "entity-index-overflow", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded shell element count exceeds EntityIndex capacity."); + return; + } + index = static_cast(definition_.shell_elements.size()); + definition_.shell_elements.push_back( + {{raw_instance.name, raw_element.label, raw_element.label_text}, + raw_element.type == RawElement::Type::kS4 + ? ShellSourceElementType::kS4 + : ShellSourceElementType::kS4r, + connected_nodes, + assignment->second.second, + assignment->second.first, + raw_element.location}); } + element_indices.emplace(raw_element.label, index); + instance.element_mappings.push_back({raw_element.label, index}); + } + + for (const auto& raw_set : part->node_sets) { + NodeSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto found = node_indices.find(member); + if (found == node_indices.end()) { + InputFailure("unresolved-reference", raw_set.location, "NSET", + raw_set.name, "Part node-set expansion failed."); + return; + } + set.node_indices.push_back(found->second); + } + definition_.node_sets.push_back(std::move(set)); + } + for (const auto& raw_set : part->element_sets) { + ElementSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto found = element_indices.find(member); + if (found == element_indices.end()) { + InputFailure("unresolved-reference", raw_set.location, "ELSET", + raw_set.name, "Part element-set expansion failed."); + return; + } + set.element_indices.push_back(found->second); + } + definition_.element_sets.push_back(std::move(set)); + } + definition_.instances.push_back(std::move(instance)); + } + } + + void ExpandAssemblySets() { + for (const auto& raw_set : assembly_sets_) { + const RawInstance* raw_instance = FindInstance(raw_set.instance_name); + if (raw_instance == nullptr) { + InputFailure("unresolved-reference", raw_set.location, + raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name, + "Assembly set INSTANCE must resolve."); + return; + } + const auto definition_instance = std::find_if( + definition_.instances.begin(), definition_.instances.end(), + [&raw_instance](const InstanceDefinition& instance) { + return EqualName(instance.name, raw_instance->name); + }); + if (definition_instance == definition_.instances.end()) { + InputFailure("unresolved-reference", raw_set.location, + raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name, + "Assembly set instance expansion is unavailable."); + return; + } + if (raw_set.is_node_set) { + definition_.node_sets.erase( + std::remove_if(definition_.node_sets.begin(), + definition_.node_sets.end(), + [&raw_set](const NodeSet& set) { + return EqualName(set.name, raw_set.name); + }), + definition_.node_sets.end()); + NodeSet set{ + raw_set.name, definition_instance->name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto mapping = + std::find_if(definition_instance->node_mappings.begin(), + definition_instance->node_mappings.end(), + [member](const SourceIndexMapping& value) { + return value.source_label == member; + }); + if (mapping == definition_instance->node_mappings.end()) { + InputFailure("unresolved-reference", raw_set.location, "NSET", + raw_set.name, + "Assembly node-set member must resolve in its " + "instance."); + return; + } + set.node_indices.push_back(mapping->internal_index); + } + definition_.node_sets.push_back(std::move(set)); + } else { + definition_.element_sets.erase( + std::remove_if(definition_.element_sets.begin(), + definition_.element_sets.end(), + [&raw_set](const ElementSet& set) { + return EqualName(set.name, raw_set.name); + }), + definition_.element_sets.end()); + ElementSet set{ + raw_set.name, definition_instance->name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto mapping = + std::find_if(definition_instance->element_mappings.begin(), + definition_instance->element_mappings.end(), + [member](const SourceIndexMapping& value) { + return value.source_label == member; + }); + if (mapping == definition_instance->element_mappings.end()) { + InputFailure("unresolved-reference", raw_set.location, "ELSET", + raw_set.name, + "Assembly element-set member must resolve in " + "its instance."); + return; + } + set.element_indices.push_back(mapping->internal_index); + } + definition_.element_sets.push_back(std::move(set)); + } + } + } + + std::optional> ResolveNodeTarget( + const std::string& target, const SourceLocation& location, + const std::string& keyword) { + std::vector matching_sets; + for (const auto& set : definition_.node_sets) { + if (EqualName(set.name, target)) { + matching_sets.push_back(&set); + } } - void parseInstanceStart(const KeywordBlock& block) { - if (parameter(block, "DEPENDENT") != nullptr || - parameter(block, "INDEPENDENT") != nullptr) { - inputFailure( - "unsupported-instance-mesh-semantics", block.location, - block.canonicalName, "", - "Dependent and independent instance mesh semantics are unsupported."); - return; + std::vector matching_nodes; + std::int64_t label = 0; + if (TryPositiveInteger(target, label)) { + for (std::size_t index = 0U; index < definition_.nodes.size(); ++index) { + if (definition_.nodes[index].source_id.source_label == label) { + matching_nodes.push_back(static_cast(index)); } - if (!validateParameters(block, {"NAME", "PART"})) { - return; - } - const auto* name = requiredParameterValue(block, "NAME"); - const auto* part = requiredParameterValue(block, "PART"); - if (name == nullptr || part == nullptr) { - return; - } - if (!block.data.empty()) { - inputFailure( - "unsupported-instance-transform", block.data.front().location, - block.canonicalName, *name, - "Instance translation or rotation data is unsupported."); - return; - } - if (containsName(instances_, *name)) { - inputFailure( - "duplicate-entity", block.location, block.canonicalName, - *name, "Instance names are globally unique."); - return; - } - instances_.push_back({*name, *part, block.location}); - inInstance_ = true; + } } - void parseInstanceBlock(const KeywordBlock& block) { - if (block.canonicalName == "END INSTANCE") { - if (!validateParameters(block, {}) || !requireNoData(block)) { - return; - } - inInstance_ = false; + // A token is resolved only after both approved interpretations have + // been considered; declaration order never gives a node set priority + // over an equally valid direct source-label target. + if (matching_sets.size() > 1U) { + InputFailure( + "unresolved-reference", location, keyword, target, + "The node-set target is ambiguous across identity instances."); + return std::nullopt; + } + if (matching_nodes.size() > 1U) { + InputFailure("unresolved-reference", location, keyword, target, + "The direct node-label target is ambiguous across identity " + "instances."); + return std::nullopt; + } + if (matching_sets.size() == 1U && matching_nodes.size() == 1U) { + InputFailure("unresolved-reference", location, keyword, target, + "The target is ambiguous between a node-set name and a " + "direct node label."); + return std::nullopt; + } + if (matching_sets.size() == 1U) { + return matching_sets.front()->node_indices; + } + if (matching_nodes.size() == 1U) { + return matching_nodes; + } + InputFailure( + "unresolved-reference", location, keyword, target, + "The boundary or load target must resolve to one node or node set."); + return std::nullopt; + } + + void FinalizeStep() { + std::vector boundaries = model_boundaries_; + boundaries.insert(boundaries.end(), step_.boundaries.begin(), + step_.boundaries.end()); + + std::map, double> prescribed_values; + for (const auto& boundary : boundaries) { + auto target = + ResolveNodeTarget(boundary.target, boundary.location, "BOUNDARY"); + if (!target) { + return; + } + for (const auto node : *target) { + for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) { + const auto key = std::make_pair(node, dof); + const auto existing = prescribed_values.find(key); + if (existing != prescribed_values.end() && + existing->second != boundary.value) { + InputFailure("conflicting-boundary-condition", boundary.location, + "BOUNDARY", boundary.target, + "Expanded boundary rows prescribe different values to " + "one node/DOF."); return; + } + prescribed_values[key] = boundary.value; } - if (block.canonicalName == "ASSEMBLY") { - inputFailure( - "unsupported-nested-assembly", block.location, - block.canonicalName, "", - "An assembly cannot be nested in an instance."); - return; - } - inputFailure( - "unsupported-instance-mesh-semantics", block.location, - block.canonicalName, instances_.back().name, - "Instance-local mesh definitions are unsupported."); + } + } + for (const auto& load : step_.loads) { + if (!ResolveNodeTarget(load.target, load.location, "CLOAD")) { + return; + } } - void parseAssemblySet(const KeywordBlock& block, bool nodeSet) { - const std::string_view nameParameter = nodeSet ? "NSET" : "ELSET"; - if (!validateParameters( - block, {nameParameter, "INSTANCE", "GENERATE"})) { - return; - } - const auto* name = requiredParameterValue(block, nameParameter); - const auto* instance = requiredParameterValue(block, "INSTANCE"); - if (name == nullptr || instance == nullptr) { - return; - } - const auto* generateParameter = parameter(block, "GENERATE"); - if (generateParameter != nullptr && generateParameter->value) { - inputFailure( - "invalid-keyword-parameter", block.location, - block.canonicalName, "GENERATE", - "GENERATE is a valueless flag."); - return; - } - if (containsAssemblySetName(nodeSet, *name)) { - inputFailure( - "duplicate-entity", block.location, block.canonicalName, - *name, - "Assembly set names are unique within their node-set or element-set namespace."); - return; - } - RawAssemblySet set{nodeSet, *name, *instance, {}, block.location}; - if (!parseSetMembers(block, generateParameter != nullptr, set.members)) { - return; - } - assemblySets_.push_back(std::move(set)); - } + // Static time-control values are provenance only: V0 creates exactly + // the canonical final frame 0 and performs no increment loop here. + definition_.steps.push_back({"Step-1", std::move(boundaries), step_.loads, + step_.static_values[0], step_.static_values[1], + step_.static_values[2], step_.static_values[3], + step_.location}); + } - void parseMaterial(const KeywordBlock& block) { - if (!assemblySeen_ || modelBoundarySeen_) { - invalidKeywordLocation( - block, - "Material definitions follow the assembly and precede model boundaries."); - return; - } - if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) { - return; - } - const auto* name = requiredParameterValue(block, "NAME"); - if (name == nullptr) { - return; - } - if (containsName(materials_, *name)) { - inputFailure( - "duplicate-entity", block.location, block.canonicalName, - *name, "Material names are globally unique."); - return; - } - materials_.push_back({*name, 0.0, 0.0, false, block.location, {}}); - materialEligible_ = materials_.size() - 1U; - } + const ParsedInput& input_; + ModelDefinition definition_{}; + std::optional failure_; - void parseElastic(const KeywordBlock& block) { - if (!materialEligible_) { - inputFailure( - "invalid-keyword-location", block.location, - block.canonicalName, "", - "ELASTIC must immediately follow MATERIAL."); - return; - } - if (!validateParameters(block, {}) || block.data.size() != 1U || - block.data[0].fields.size() != 2U) { - if (!failure_) { - inputFailure( - "invalid-data-arity", block.location, - block.canonicalName, materials_[*materialEligible_].name, - "ELASTIC requires exactly one E, nu row."); - } - return; - } - RawMaterial& material = materials_[*materialEligible_]; - if (material.hasElastic) { - inputFailure( - "duplicate-entity", block.location, block.canonicalName, - material.name, "A material accepts one ELASTIC definition."); - return; - } - if (!parseModelDouble( - block.data[0].fields[0], material.youngsModulus, - block.data[0].location, block.canonicalName, - modelElementFamily_ == ElementFamily::shell - ? "invalid-shell-material" - : "invalid-beam-property", - "Elastic material values must be finite.") || - !parseModelDouble( - block.data[0].fields[1], material.poissonRatio, - block.data[0].location, block.canonicalName, - modelElementFamily_ == ElementFamily::shell - ? "invalid-shell-material" - : "invalid-beam-property", - "Elastic material values must be finite.")) { - return; - } - material.hasElastic = true; - material.elasticLocation = block.data[0].location; - } + std::vector parts_; + std::vector instances_; + std::vector assembly_sets_; + std::vector materials_; + std::vector model_boundaries_; + RawStep step_{}; + std::vector>> + part_section_assignments_; + std::vector>> + part_shell_section_assignments_; - void parseBoundary( - const KeywordBlock& block, - std::vector& destination) { - if (!validateParameters(block, {}) || block.data.empty()) { - if (!failure_) { - inputFailure( - "invalid-data-arity", block.location, - block.canonicalName, "", - "BOUNDARY requires one or more data rows."); - } - return; - } - for (const auto& row : block.data) { - if (row.fields.size() != 3U && row.fields.size() != 4U) { - inputFailure( - "invalid-data-arity", row.location, - block.canonicalName, "", - "BOUNDARY rows require target, first DOF, last DOF, and optional value."); - return; - } - if (row.fields[0].empty()) { - inputFailure( - "unresolved-reference", row.location, - block.canonicalName, "", - "A boundary target cannot be empty."); - return; - } - std::int64_t first = 0; - std::int64_t last = 0; - if (!parseInteger( - row.fields[1], first, row.location, - block.canonicalName, true) || - !parseInteger( - row.fields[2], last, row.location, - block.canonicalName, true)) { - return; - } - if (first < 1 || first > 6 || last < first || last > 6) { - inputFailure( - "invalid-dof", row.location, block.canonicalName, - row.fields[0], "Boundary DOFs must be an ordered range in 1..6."); - return; - } - double value = 0.0; - if (row.fields.size() == 4U && - !parseDouble( - row.fields[3], value, row.location, - block.canonicalName)) { - return; - } - destination.push_back({ - row.fields[0], - static_cast(first), - static_cast(last), - value, - row.location}); - } - } - - void parseCload(const KeywordBlock& block, std::vector& loads) { - if (!validateParameters(block, {}) || block.data.empty()) { - if (!failure_) { - inputFailure( - "invalid-data-arity", block.location, - block.canonicalName, "", "CLOAD requires one or more rows."); - } - return; - } - for (const auto& row : block.data) { - if (row.fields.size() != 3U || row.fields[0].empty()) { - inputFailure( - "invalid-data-arity", row.location, - block.canonicalName, "", - "CLOAD rows require target, DOF, magnitude."); - return; - } - std::int64_t dof = 0; - double magnitude = 0.0; - if (!parseInteger( - row.fields[1], dof, row.location, - block.canonicalName, true) || - !parseDouble( - row.fields[2], magnitude, row.location, - block.canonicalName)) { - return; - } - if (dof < 1 || dof > 6) { - inputFailure( - "invalid-dof", row.location, block.canonicalName, - row.fields[0], "CLOAD DOF must be in 1..6."); - return; - } - loads.push_back({ - row.fields[0], static_cast(dof), magnitude, row.location}); - } - } - - void parseStepStart(const KeywordBlock& block) { - if (stepSeen_) { - inputFailure( - "unsupported-multiple-step", block.location, - block.canonicalName, "", - "V0 accepts exactly one analysis step."); - return; - } - if (!assemblySeen_ || materials_.empty()) { - invalidKeywordLocation( - block, - "The sole step follows the complete assembly and material definitions."); - return; - } - if (!validateParameters(block, {"NAME", "NLGEOM"}) || - !requireNoData(block)) { - return; - } - const auto* nlgeom = parameter(block, "NLGEOM"); - if (nlgeom != nullptr) { - if (!nlgeom->value || nlgeom->value->empty()) { - inputFailure( - "invalid-keyword-parameter", block.location, - block.canonicalName, "NLGEOM", - "NLGEOM requires NO in the approved subset."); - return; - } - if (!equalName(*nlgeom->value, "NO")) { - if (modelElementFamily_ == ElementFamily::shell) { - inputFailure( - "unsupported-nonlinear-geometry", block.location, - block.canonicalName, *nlgeom->value, - "Only absent NLGEOM or NLGEOM=NO is supported."); - } else { - modelFailure( - "unsupported-nonlinear-geometry", block.location, - block.canonicalName, *nlgeom->value, - "Only absent NLGEOM or NLGEOM=NO is supported."); - } - return; - } - } - stepSeen_ = true; - inStep_ = true; - step_ = RawStep{block.location, false, {}, {}, {}}; - stepLoadSeen_ = false; - stepNoOpSeen_ = false; - } - - void parseStepBlock(const KeywordBlock& block) { - if (block.canonicalName == "END STEP") { - activeOutput_ = false; - if (!validateParameters(block, {}) || !requireNoData(block)) { - return; - } - if (!step_.hasStatic) { - invalidKeywordLocation( - block, - "The sole step requires exactly one leading STATIC procedure."); - return; - } - inStep_ = false; - return; - } - if (block.canonicalName == "STEP") { - inputFailure( - "unsupported-multiple-step", block.location, - block.canonicalName, "", - "A second or nested step is unsupported."); - return; - } - if (block.canonicalName == "STATIC") { - activeOutput_ = false; - if (!step_.boundaries.empty() || !step_.loads.empty() || - stepNoOpSeen_) { - invalidKeywordLocation( - block, - "STATIC must be the first keyword in the sole step."); - return; - } - parseStatic(block); - } else if (block.canonicalName == "BOUNDARY") { - activeOutput_ = false; - if (!step_.hasStatic || stepLoadSeen_ || stepNoOpSeen_) { - invalidKeywordLocation( - block, - "Step boundaries follow STATIC and precede loads and no-op requests."); - return; - } - parseBoundary(block, step_.boundaries); - } else if (block.canonicalName == "CLOAD") { - activeOutput_ = false; - if (!step_.hasStatic || stepNoOpSeen_) { - invalidKeywordLocation( - block, - "CLOAD follows STATIC and boundaries and precedes no-op requests."); - return; - } - parseCload(block, step_.loads); - stepLoadSeen_ = !failure_; - } else if (block.canonicalName == "RESTART") { - activeOutput_ = false; - if (!step_.hasStatic) { - invalidKeywordLocation( - block, - "Step no-op requests follow STATIC, boundaries, and loads."); - return; - } - stepNoOpSeen_ = true; - if (requireNoData(block)) { - addIgnoredWarning(block); - } - } else if (block.canonicalName == "OUTPUT") { - if (!step_.hasStatic) { - invalidKeywordLocation( - block, - "Step no-op requests follow STATIC, boundaries, and loads."); - return; - } - stepNoOpSeen_ = true; - parseOutputRoot(block); - } else if (block.canonicalName == "NODE OUTPUT" || - block.canonicalName == "ELEMENT OUTPUT" || - block.canonicalName == "CONTACT OUTPUT") { - parseOutputChild(block); - } else { - activeOutput_ = false; - rejectUnknown(block); - } - } - - void parseStatic(const KeywordBlock& block) { - if (step_.hasStatic || !validateParameters(block, {}) || - block.data.size() != 1U || block.data[0].fields.size() != 4U) { - if (!failure_) { - inputFailure( - "invalid-static-data", block.location, - block.canonicalName, "", - "STATIC requires exactly one row of four values."); - } - return; - } - for (std::size_t field = 0U; field < 4U; ++field) { - if (!parseDouble( - block.data[0].fields[field], step_.staticValues[field], - block.data[0].location, block.canonicalName)) { - return; - } - if (step_.staticValues[field] <= 0.0) { - inputFailure( - "invalid-static-data", block.data[0].location, - block.canonicalName, "", - "All four STATIC fields must be positive."); - return; - } - } - if (step_.staticValues[2] > step_.staticValues[3]) { - inputFailure( - "invalid-static-data", block.data[0].location, - block.canonicalName, "", - "STATIC minimum increment cannot exceed maximum increment."); - return; - } - step_.hasStatic = true; - } - - void parseOutputRoot(const KeywordBlock& block) { - const auto* field = parameter(block, "FIELD"); - const auto* history = parameter(block, "HISTORY"); - if ((field == nullptr) == (history == nullptr) || - (field != nullptr && field->value) || - (history != nullptr && history->value)) { - inputFailure( - "unsupported-keyword", block.location, - block.canonicalName, "", - "OUTPUT must select exactly FIELD or HISTORY."); - return; - } - activeOutput_ = true; - addIgnoredWarning(block); - } - - void parseOutputChild(const KeywordBlock& block) { - if (!activeOutput_) { - inputFailure( - "invalid-keyword-location", block.location, - block.canonicalName, "", - "Output variable keywords require an active OUTPUT request."); - return; - } - addIgnoredWarning(block); - } - - void addIgnoredWarning(const KeywordBlock& block) { - // One warning per allowlisted keyword keeps no-op provenance stable; - // subordinate variable rows remain attached to that keyword record. - definition_.warnings.push_back({ - Severity::kWarning, - "ignored-input-keyword", - block.location, - block.canonicalName, - "", - "The allowlisted Abaqus keyword is ignored without semantic effect."}); - } - - void rejectUnknown(const KeywordBlock& block) { - if (block.canonicalName == "DLOAD" && - modelElementFamily_ == ElementFamily::shell) { - inputFailure( - "unsupported-distributed-load", block.location, - block.canonicalName, "", - "Distributed, pressure, gravity, body, edge, and follower loads are unsupported."); - return; - } - inputFailure( - "unsupported-keyword", block.location, - block.canonicalName, "", - "The keyword is outside the approved Abaqus subset."); - } - - const RawPart* findPart(const std::string& name) const { - const auto found = std::find_if( - parts_.begin(), parts_.end(), [&name](const RawPart& part) { - return equalName(part.name, name); - }); - return found == parts_.end() ? nullptr : &*found; - } - - const RawInstance* findInstance(const std::string& name) const { - const auto found = std::find_if( - instances_.begin(), instances_.end(), - [&name](const RawInstance& instance) { - return equalName(instance.name, name); - }); - return found == instances_.end() ? nullptr : &*found; - } - - std::optional findMaterialIndex(const std::string& name) const { - for (std::size_t index = 0U; index < materials_.size(); ++index) { - if (equalName(materials_[index].name, name)) { - return static_cast(index); - } - } - return std::nullopt; - } - - const RawSet* findSet( - const std::vector& sets, - const std::string& name) const { - const auto found = std::find_if( - sets.begin(), sets.end(), [&name](const RawSet& set) { - return equalName(set.name, name); - }); - return found == sets.end() ? nullptr : &*found; - } - - const RawNode* findNode(const RawPart& part, std::int64_t label) const { - const auto found = std::find_if( - part.nodes.begin(), part.nodes.end(), - [label](const RawNode& node) { return node.label == label; }); - return found == part.nodes.end() ? nullptr : &*found; - } - - const RawElement* findElement( - const RawPart& part, - std::int64_t label) const { - const auto found = std::find_if( - part.elements.begin(), part.elements.end(), - [label](const RawElement& element) { return element.label == label; }); - return found == part.elements.end() ? nullptr : &*found; - } - - void finalizeModel() { - if (parts_.empty() || !assemblySeen_ || instances_.empty() || - materials_.empty() || !stepSeen_ || !step_.hasStatic) { - const auto location = input_.blocks.empty() - ? SourceLocation{input_.sourcePath, 0U} - : input_.blocks.back().location; - inputFailure( - "invalid-model-cardinality", location, "", "", - "The model requires part, assembly, identity instance, material, and one STATIC step."); - return; - } - - finalizeMaterials(); - if (failure_) { - return; - } - finalizePartsAndSections(); - if (failure_) { - return; - } - expandInstances(); - if (failure_) { - return; - } - if (!definition_.shell_elements.empty()) { - auto geometry = PreprocessShellGeometry( - definition_.nodes, - definition_.shell_elements, - definition_.shell_sections); - if (!geometry.HasValue()) { - const auto& status = geometry.GetStatus(); - failure_ = MappingFailure{ - status.Category().value_or(FailureCategory::kModel), - status.Diagnostics().front()}; - return; - } - definition_.shell_node_initial_frames = - std::move(geometry.Value().nodal_frames); - } - expandAssemblySets(); - if (failure_) { - return; - } - finalizeStep(); - } - - void finalizeMaterials() { - for (const auto& material : materials_) { - if (!material.hasElastic) { - inputFailure( - "unresolved-reference", material.location, - "MATERIAL", material.name, - "A material must own exactly one ELASTIC row."); - return; - } - if (modelElementFamily_ == ElementFamily::shell) { - if (!(material.youngsModulus > 0.0) || - !(material.poissonRatio > -1.0) || - !(material.poissonRatio < 0.5)) { - modelFailure( - "invalid-shell-material", material.elasticLocation, - "ELASTIC", material.name, - "Shell isotropic elasticity requires E>0 and -1 0.0) || - !std::isfinite(shearModulus) || !(shearModulus > 0.0)) { - modelFailure( - "invalid-beam-property", material.elasticLocation, - "ELASTIC", material.name, - "E and the derived G=E/(2*(1+nu)) must be positive."); - return; - } - definition_.materials.push_back({ - material.name, - material.youngsModulus, - material.poissonRatio, - material.location}); - } - } - - void finalizePartsAndSections() { - partSectionAssignments_.resize(parts_.size()); - partShellSectionAssignments_.resize(parts_.size()); - const bool shellModel = modelElementFamily_ == ElementFamily::shell; - for (std::size_t partIndex = 0U; partIndex < parts_.size(); ++partIndex) { - const RawPart& part = parts_[partIndex]; - if (part.nodes.empty() || part.elements.empty() || - (!shellModel && part.sections.empty())) { - inputFailure( - "invalid-model-cardinality", part.location, - "PART", part.name, - "A part requires nodes, elements, and its approved section form."); - return; - } - - PartDefinition partDefinition{}; - partDefinition.name = part.name; - partDefinition.location = part.location; - for (const auto& node : part.nodes) { - partDefinition.node_source_labels.push_back(node.label); - } - for (const auto& element : part.elements) { - partDefinition.element_source_labels.push_back(element.label); - for (const auto nodeLabel : element.nodeLabels) { - if (findNode(part, nodeLabel) == nullptr) { - inputFailure( - shellModel - ? "invalid-shell-connectivity" - : "unresolved-reference", - element.location, "ELEMENT", element.labelText, - "Element connectivity must resolve within its part."); - return; - } - } - } - for (const auto& set : part.nodeSets) { - partDefinition.node_set_names.push_back(set.name); - for (const auto label : set.members) { - if (findNode(part, label) == nullptr) { - inputFailure( - "unresolved-reference", set.location, - "NSET", set.name, - "Every node-set member must resolve within its part."); - return; - } - } - } - for (const auto& set : part.elementSets) { - partDefinition.element_set_names.push_back(set.name); - for (const auto label : set.members) { - if (findElement(part, label) == nullptr) { - inputFailure( - "unresolved-reference", set.location, - "ELSET", set.name, - "Every element-set member must resolve within its part."); - return; - } - } - } - definition_.parts.push_back(std::move(partDefinition)); - - if (shellModel) { - auto& assignments = partShellSectionAssignments_[partIndex]; - for (const auto& section : part.shellSections) { - const auto* elementSet = findSet( - part.elementSets, section.elementSetName); - const auto materialIndex = findMaterialIndex(section.materialName); - if (elementSet == nullptr || !materialIndex) { - inputFailure( - "unresolved-shell-section", section.location, - "SHELL SECTION", section.elementSetName, - "Shell section ELSET and MATERIAL references must resolve."); - return; - } - const EntityIndex sectionIndex = - static_cast(definition_.shell_sections.size()); - definition_.shell_sections.push_back({ - section.elementSetName, - section.thickness, - *materialIndex, - section.location}); - for (const auto elementLabel : elementSet->members) { - if (!assignments.emplace( - elementLabel, - std::make_pair( - sectionIndex, *materialIndex)).second) { - inputFailure( - "invalid-shell-section-assignment", - section.location, "SHELL SECTION", - std::to_string(elementLabel), - "A shell element cannot receive multiple section assignments."); - return; - } - } - } - for (const auto& element : part.elements) { - if (assignments.find(element.label) == assignments.end()) { - inputFailure( - "invalid-shell-section-assignment", element.location, - "ELEMENT", element.labelText, - "Every shell element requires exactly one resolved section assignment."); - return; - } - } - continue; - } - - auto& assignments = partSectionAssignments_[partIndex]; - for (const auto& section : part.sections) { - const auto* elementSet = findSet( - part.elementSets, section.elementSetName); - const auto materialIndex = findMaterialIndex(section.materialName); - if (elementSet == nullptr || !materialIndex) { - inputFailure( - "unresolved-reference", section.location, - "BEAM GENERAL SECTION", section.elementSetName, - "Section ELSET and MATERIAL references must resolve."); - return; - } - if (section.properties[2] != 0.0) { - modelFailure( - "unsupported-coupled-section", section.location, - "BEAM GENERAL SECTION", section.elementSetName, - "V0 requires exact I12=0."); - return; - } - if (!(section.properties[0] > 0.0) || - !(section.properties[1] > 0.0) || - !(section.properties[3] > 0.0) || - !(section.properties[4] > 0.0)) { - modelFailure( - "invalid-beam-property", section.location, - "BEAM GENERAL SECTION", section.elementSetName, - "A, I11, I22, and J must be positive."); - return; - } - const EntityIndex sectionIndex = - static_cast(definition_.sections.size()); - definition_.sections.push_back({ - section.elementSetName, - section.properties[0], - section.properties[1], - section.properties[2], - section.properties[3], - section.properties[4], - section.firstAxis, - section.sectionPoints, - section.location}); - for (const auto elementLabel : elementSet->members) { - if (!assignments.emplace( - elementLabel, - std::make_pair(sectionIndex, *materialIndex)).second) { - inputFailure( - "duplicate-entity", section.location, - "BEAM GENERAL SECTION", - std::to_string(elementLabel), - "An element cannot receive multiple section assignments."); - return; - } - } - } - for (const auto& element : part.elements) { - if (assignments.find(element.label) == assignments.end()) { - inputFailure( - "unresolved-reference", element.location, - "ELEMENT", element.labelText, - "Every B33 element requires a resolved section assignment."); - return; - } - } - } - } - - std::size_t partIndex(const RawPart& part) const { - return static_cast(&part - parts_.data()); - } - - bool validateGeometry( - const RawElement& raw, - const Node& first, - const Node& second, - const GeneralBeamSection& section) { - const auto norm = [](const std::array& vector) { - return std::hypot(vector[0], vector[1], vector[2]); - }; - const auto maximumAbsolute = [](const std::array& vector) { - return std::max({ - std::abs(vector[0]), - std::abs(vector[1]), - std::abs(vector[2])}); - }; - - // Compare both approved inequalities after a common scaling. This - // preserves the exact ratios while avoiding overflow in x*x and in - // subtraction between large finite coordinates. - const double globalCoordinateScale = std::max({ - 1.0, - maximumAbsolute(first.coordinates), - maximumAbsolute(second.coordinates)}); - std::array firstScaled{}; - std::array secondScaled{}; - std::array deltaScaled{}; - for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { - firstScaled[coordinate] = - first.coordinates[coordinate] / globalCoordinateScale; - secondScaled[coordinate] = - second.coordinates[coordinate] / globalCoordinateScale; - deltaScaled[coordinate] = - secondScaled[coordinate] - firstScaled[coordinate]; - } - const double lengthRatio = norm(deltaScaled); - const double coordinateNormRatio = std::max({ - 1.0 / globalCoordinateScale, - norm(firstScaled), - norm(secondScaled)}); - if (!(lengthRatio > 1.0e-12 * coordinateNormRatio)) { - return modelFailure( - "invalid-beam-length", raw.location, "ELEMENT", - raw.labelText, - "Beam length fails the approved scale-aware threshold."); - } - std::array tangent{ - deltaScaled[0] / lengthRatio, - deltaScaled[1] / lengthRatio, - deltaScaled[2] / lengthRatio}; - - const double globalGuideScale = - std::max(1.0, maximumAbsolute(section.first_axis)); - std::array guideScaled{}; - for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { - guideScaled[coordinate] = - section.first_axis[coordinate] / globalGuideScale; - } - const double projection = - guideScaled[0] * tangent[0] + - guideScaled[1] * tangent[1] + - guideScaled[2] * tangent[2]; - std::array perpendicular{ - guideScaled[0] - projection * tangent[0], - guideScaled[1] - projection * tangent[1], - guideScaled[2] - projection * tangent[2]}; - const double guideNormRatio = std::max( - 1.0 / globalGuideScale, - norm(guideScaled)); - if (!(norm(perpendicular) > 1.0e-12 * guideNormRatio)) { - return modelFailure( - "invalid-beam-guide-vector", section.location, - "BEAM GENERAL SECTION", raw.labelText, - "The first section axis cannot be zero or tangent-parallel."); - } - return true; - } - - void expandInstances() { - for (const auto& rawInstance : instances_) { - const RawPart* part = findPart(rawInstance.partName); - if (part == nullptr) { - inputFailure( - "unresolved-reference", rawInstance.location, - "INSTANCE", rawInstance.name, - "INSTANCE PART must resolve case-insensitively."); - return; - } - InstanceDefinition instance{ - rawInstance.name, part->name, {}, {}, rawInstance.location}; - std::map nodeIndices; - std::map elementIndices; - - // Instance order, followed by part-local declaration order, is the - // sole source of stable expanded internal IDs. - for (const auto& rawNode : part->nodes) { - if (definition_.nodes.size() > - std::numeric_limits::max()) { - inputFailure( - "entity-index-overflow", rawNode.location, - "NODE", rawNode.labelText, - "Expanded node count exceeds EntityIndex capacity."); - return; - } - const EntityIndex index = - static_cast(definition_.nodes.size()); - definition_.nodes.push_back({ - {rawInstance.name, rawNode.label, rawNode.labelText}, - rawNode.coordinates, - rawNode.location}); - nodeIndices.emplace(rawNode.label, index); - instance.node_mappings.push_back({rawNode.label, index}); - } - - for (const auto& rawElement : part->elements) { - EntityIndex index = 0U; - if (rawElement.type == RawElement::Type::b33) { - const auto first = nodeIndices.find(rawElement.nodeLabels[0]); - const auto second = nodeIndices.find(rawElement.nodeLabels[1]); - const auto& assignments = - partSectionAssignments_[partIndex(*part)]; - const auto assignment = assignments.find(rawElement.label); - if (first == nodeIndices.end() || second == nodeIndices.end() || - assignment == assignments.end()) { - inputFailure( - "unresolved-reference", rawElement.location, - "ELEMENT", rawElement.labelText, - "Expanded connectivity and section assignment must resolve."); - return; - } - if (definition_.elements.size() > - std::numeric_limits::max()) { - inputFailure( - "entity-index-overflow", rawElement.location, - "ELEMENT", rawElement.labelText, - "Expanded element count exceeds EntityIndex capacity."); - return; - } - index = static_cast(definition_.elements.size()); - const auto& section = - definition_.sections[assignment->second.first]; - if (!validateGeometry( - rawElement, - definition_.nodes[first->second], - definition_.nodes[second->second], - section)) { - return; - } - definition_.elements.push_back({ - {rawInstance.name, rawElement.label, rawElement.labelText}, - {first->second, second->second}, - assignment->second.second, - assignment->second.first, - rawElement.location}); - } else { - std::array connectedNodes{}; - for (std::size_t node = 0U; - node < connectedNodes.size(); - ++node) { - const auto found = - nodeIndices.find(rawElement.nodeLabels[node]); - if (found == nodeIndices.end()) { - inputFailure( - "invalid-shell-connectivity", rawElement.location, - "ELEMENT", rawElement.labelText, - "Expanded shell connectivity must resolve."); - return; - } - connectedNodes[node] = found->second; - } - const auto& assignments = - partShellSectionAssignments_[partIndex(*part)]; - const auto assignment = assignments.find(rawElement.label); - if (assignment == assignments.end()) { - inputFailure( - "invalid-shell-section-assignment", - rawElement.location, "ELEMENT", rawElement.labelText, - "Expanded shell section assignment must resolve exactly once."); - return; - } - if (definition_.shell_elements.size() > - std::numeric_limits::max()) { - inputFailure( - "entity-index-overflow", rawElement.location, - "ELEMENT", rawElement.labelText, - "Expanded shell element count exceeds EntityIndex capacity."); - return; - } - index = - static_cast(definition_.shell_elements.size()); - definition_.shell_elements.push_back({ - {rawInstance.name, rawElement.label, rawElement.labelText}, - rawElement.type == RawElement::Type::s4 - ? ShellSourceElementType::kS4 - : ShellSourceElementType::kS4r, - connectedNodes, - assignment->second.second, - assignment->second.first, - rawElement.location}); - } - elementIndices.emplace(rawElement.label, index); - instance.element_mappings.push_back({rawElement.label, index}); - } - - for (const auto& rawSet : part->nodeSets) { - NodeSet set{rawSet.name, rawInstance.name, {}, rawSet.location}; - for (const auto member : rawSet.members) { - const auto found = nodeIndices.find(member); - if (found == nodeIndices.end()) { - inputFailure( - "unresolved-reference", rawSet.location, - "NSET", rawSet.name, - "Part node-set expansion failed."); - return; - } - set.node_indices.push_back(found->second); - } - definition_.node_sets.push_back(std::move(set)); - } - for (const auto& rawSet : part->elementSets) { - ElementSet set{ - rawSet.name, rawInstance.name, {}, rawSet.location}; - for (const auto member : rawSet.members) { - const auto found = elementIndices.find(member); - if (found == elementIndices.end()) { - inputFailure( - "unresolved-reference", rawSet.location, - "ELSET", rawSet.name, - "Part element-set expansion failed."); - return; - } - set.element_indices.push_back(found->second); - } - definition_.element_sets.push_back(std::move(set)); - } - definition_.instances.push_back(std::move(instance)); - } - } - - void expandAssemblySets() { - for (const auto& rawSet : assemblySets_) { - const RawInstance* rawInstance = findInstance(rawSet.instanceName); - if (rawInstance == nullptr) { - inputFailure( - "unresolved-reference", rawSet.location, - rawSet.isNodeSet ? "NSET" : "ELSET", rawSet.name, - "Assembly set INSTANCE must resolve."); - return; - } - const auto definitionInstance = std::find_if( - definition_.instances.begin(), definition_.instances.end(), - [&rawInstance](const InstanceDefinition& instance) { - return equalName(instance.name, rawInstance->name); - }); - if (definitionInstance == definition_.instances.end()) { - inputFailure( - "unresolved-reference", rawSet.location, - rawSet.isNodeSet ? "NSET" : "ELSET", rawSet.name, - "Assembly set instance expansion is unavailable."); - return; - } - if (rawSet.isNodeSet) { - definition_.node_sets.erase( - std::remove_if( - definition_.node_sets.begin(), - definition_.node_sets.end(), - [&rawSet](const NodeSet& set) { - return equalName(set.name, rawSet.name); - }), - definition_.node_sets.end()); - NodeSet set{ - rawSet.name, definitionInstance->name, {}, rawSet.location}; - for (const auto member : rawSet.members) { - const auto mapping = std::find_if( - definitionInstance->node_mappings.begin(), - definitionInstance->node_mappings.end(), - [member](const SourceIndexMapping& value) { - return value.source_label == member; - }); - if (mapping == definitionInstance->node_mappings.end()) { - inputFailure( - "unresolved-reference", rawSet.location, - "NSET", rawSet.name, - "Assembly node-set member must resolve in its instance."); - return; - } - set.node_indices.push_back(mapping->internal_index); - } - definition_.node_sets.push_back(std::move(set)); - } else { - definition_.element_sets.erase( - std::remove_if( - definition_.element_sets.begin(), - definition_.element_sets.end(), - [&rawSet](const ElementSet& set) { - return equalName(set.name, rawSet.name); - }), - definition_.element_sets.end()); - ElementSet set{ - rawSet.name, definitionInstance->name, {}, rawSet.location}; - for (const auto member : rawSet.members) { - const auto mapping = std::find_if( - definitionInstance->element_mappings.begin(), - definitionInstance->element_mappings.end(), - [member](const SourceIndexMapping& value) { - return value.source_label == member; - }); - if (mapping == definitionInstance->element_mappings.end()) { - inputFailure( - "unresolved-reference", rawSet.location, - "ELSET", rawSet.name, - "Assembly element-set member must resolve in its instance."); - return; - } - set.element_indices.push_back(mapping->internal_index); - } - definition_.element_sets.push_back(std::move(set)); - } - } - } - - std::optional> resolveNodeTarget( - const std::string& target, - const SourceLocation& location, - const std::string& keyword) { - std::vector matchingSets; - for (const auto& set : definition_.node_sets) { - if (equalName(set.name, target)) { - matchingSets.push_back(&set); - } - } - - std::vector matchingNodes; - std::int64_t label = 0; - if (tryPositiveInteger(target, label)) { - for (std::size_t index = 0U; - index < definition_.nodes.size(); - ++index) { - if (definition_.nodes[index].source_id.source_label == label) { - matchingNodes.push_back(static_cast(index)); - } - } - } - - // A token is resolved only after both approved interpretations have - // been considered; declaration order never gives a node set priority - // over an equally valid direct source-label target. - if (matchingSets.size() > 1U) { - inputFailure( - "unresolved-reference", location, keyword, target, - "The node-set target is ambiguous across identity instances."); - return std::nullopt; - } - if (matchingNodes.size() > 1U) { - inputFailure( - "unresolved-reference", location, keyword, target, - "The direct node-label target is ambiguous across identity instances."); - return std::nullopt; - } - if (matchingSets.size() == 1U && matchingNodes.size() == 1U) { - inputFailure( - "unresolved-reference", location, keyword, target, - "The target is ambiguous between a node-set name and a direct node label."); - return std::nullopt; - } - if (matchingSets.size() == 1U) { - return matchingSets.front()->node_indices; - } - if (matchingNodes.size() == 1U) { - return matchingNodes; - } - inputFailure( - "unresolved-reference", location, keyword, target, - "The boundary or load target must resolve to one node or node set."); - return std::nullopt; - } - - void finalizeStep() { - std::vector boundaries = modelBoundaries_; - boundaries.insert( - boundaries.end(), step_.boundaries.begin(), step_.boundaries.end()); - - std::map, double> prescribedValues; - for (const auto& boundary : boundaries) { - auto target = resolveNodeTarget( - boundary.target, boundary.location, "BOUNDARY"); - if (!target) { - return; - } - for (const auto node : *target) { - for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) { - const auto key = std::make_pair(node, dof); - const auto existing = prescribedValues.find(key); - if (existing != prescribedValues.end() && - existing->second != boundary.value) { - inputFailure( - "conflicting-boundary-condition", - boundary.location, - "BOUNDARY", - boundary.target, - "Expanded boundary rows prescribe different values to one node/DOF."); - return; - } - prescribedValues[key] = boundary.value; - } - } - } - for (const auto& load : step_.loads) { - if (!resolveNodeTarget(load.target, load.location, "CLOAD")) { - return; - } - } - - // Static time-control values are provenance only: V0 creates exactly - // the canonical final frame 0 and performs no increment loop here. - definition_.steps.push_back({ - "Step-1", - std::move(boundaries), - step_.loads, - step_.staticValues[0], - step_.staticValues[1], - step_.staticValues[2], - step_.staticValues[3], - step_.location}); - } - - const ParsedInput& input_; - ModelDefinition definition_{}; - std::optional failure_; - - std::vector parts_; - std::vector instances_; - std::vector assemblySets_; - std::vector materials_; - std::vector modelBoundaries_; - RawStep step_{}; - std::vector>> partSectionAssignments_; - std::vector>> partShellSectionAssignments_; - - std::optional currentPart_; - std::optional pendingSection_; - std::optional materialEligible_; - std::optional modelElementFamily_; - bool headingSeen_{false}; - bool partElementsSeen_{false}; - bool partSetsSeen_{false}; - bool partSectionsSeen_{false}; - bool beamSectionContextActive_{false}; - bool assemblySeen_{false}; - bool assemblySetSeen_{false}; - bool modelBoundarySeen_{false}; - bool inAssembly_{false}; - bool inInstance_{false}; - bool stepSeen_{false}; - bool inStep_{false}; - bool stepLoadSeen_{false}; - bool stepNoOpSeen_{false}; - bool activeOutput_{false}; + std::optional current_part_; + std::optional pending_section_; + std::optional material_eligible_; + std::optional model_element_family_; + bool heading_seen_{false}; + bool part_elements_seen_{false}; + bool part_sets_seen_{false}; + bool part_sections_seen_{false}; + bool beam_section_context_active_{false}; + bool assembly_seen_{false}; + bool assembly_set_seen_{false}; + bool model_boundary_seen_{false}; + bool in_assembly_{false}; + bool in_instance_{false}; + bool step_seen_{false}; + bool in_step_{false}; + bool step_load_seen_{false}; + bool step_no_op_seen_{false}; + bool active_output_{false}; }; -} // namespace +} // namespace -Result AbaqusDomainMapper::map(const ParsedInput& input) const { - return MappingContext{input}.run(); +Result AbaqusDomainMapper::Map(const ParsedInput& input) const { + return MappingContext{input}.Run(); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/io/abaqus/input_reader.cpp b/src/fesa/io/abaqus/input_reader.cpp index d3568f6..61af931 100644 --- a/src/fesa/io/abaqus/input_reader.cpp +++ b/src/fesa/io/abaqus/input_reader.cpp @@ -1,4 +1,4 @@ -#include "fesa/io/abaqus/input_reader.hpp" +#include "fesa/io/abaqus/input_reader.h" #include #include @@ -15,202 +15,176 @@ namespace fesa { namespace { -std::filesystem::path normalizedPath(const std::filesystem::path& path) { - std::error_code error; - const auto absolute = std::filesystem::absolute(path, error); - return (error ? path : absolute).lexically_normal(); +std::filesystem::path NormalizedPath(const std::filesystem::path& path) { + std::error_code error; + const auto absolute = std::filesystem::absolute(path, error); + return (error ? path : absolute).lexically_normal(); } -bool isAsciiWhitespace(char value) noexcept { - return value == ' ' || value == '\t' || value == '\r' || - value == '\n' || value == '\f' || value == '\v'; +bool IsAsciiWhitespace(char value) noexcept { + return value == ' ' || value == '\t' || value == '\r' || value == '\n' || + value == '\f' || value == '\v'; } -std::string trim(std::string_view text) { - while (!text.empty() && isAsciiWhitespace(text.front())) { - text.remove_prefix(1U); +std::string Trim(std::string_view text) { + while (!text.empty() && IsAsciiWhitespace(text.front())) { + text.remove_prefix(1U); + } + while (!text.empty() && IsAsciiWhitespace(text.back())) { + text.remove_suffix(1U); + } + return std::string{text}; +} + +std::string UppercaseAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](char character) { + if (character >= 'a' && character <= 'z') { + return static_cast(character - 'a' + 'A'); } - while (!text.empty() && isAsciiWhitespace(text.back())) { - text.remove_suffix(1U); - } - return std::string{text}; + return character; + }); + return value; } -std::string uppercaseAscii(std::string value) { - std::transform( - value.begin(), value.end(), value.begin(), [](char character) { - if (character >= 'a' && character <= 'z') { - return static_cast(character - 'a' + 'A'); - } - return character; - }); - return value; +std::vector SplitFields(std::string_view line) { + std::vector fields; + std::size_t field_start = 0U; + while (true) { + const std::size_t separator = line.find(',', field_start); + if (separator == std::string_view::npos) { + fields.push_back(Trim(line.substr(field_start))); + break; + } + fields.push_back(Trim(line.substr(field_start, separator - field_start))); + field_start = separator + 1U; + } + return fields; } -std::vector splitFields(std::string_view line) { - std::vector fields; - std::size_t fieldStart = 0U; - while (true) { - const std::size_t separator = line.find(',', fieldStart); - if (separator == std::string_view::npos) { - fields.push_back(trim(line.substr(fieldStart))); - break; - } - fields.push_back(trim(line.substr(fieldStart, separator - fieldStart))); - fieldStart = separator + 1U; - } - return fields; +/// @brief Computes the stable identity of the exact source bytes. +/// @note This runs before line-ending handling so parser provenance is not +/// affected by text normalization. +std::string ContentIdentity(const std::string& bytes) { + constexpr std::uint64_t kOffsetBasis = 14695981039346656037ULL; + constexpr std::uint64_t kPrime = 1099511628211ULL; + std::uint64_t hash = kOffsetBasis; + // Hash the binary input before CRLF handling so provenance follows the + // exact file bytes rather than a normalized text representation. + for (const unsigned char byte : bytes) { + hash ^= static_cast(byte); + hash *= kPrime; + } + + std::ostringstream formatted; + formatted << "fnv1a64:" << std::hex << std::setfill('0') << std::setw(16) + << hash; + return formatted.str(); } -std::string contentIdentity(const std::string& bytes) { - constexpr std::uint64_t offsetBasis = 14695981039346656037ULL; - constexpr std::uint64_t prime = 1099511628211ULL; - std::uint64_t hash = offsetBasis; - // Hash the binary input before CRLF handling so provenance follows the - // exact file bytes rather than a normalized text representation. - for (const unsigned char byte : bytes) { - hash ^= static_cast(byte); - hash *= prime; - } - - std::ostringstream formatted; - formatted << "fnv1a64:" << std::hex << std::setfill('0') - << std::setw(16) << hash; - return formatted.str(); -} - -Result failure( - const std::filesystem::path& sourcePath, - std::size_t line, - std::string code, - std::string keyword, - std::string message) { - Diagnostic diagnostic{ - Severity::kError, - std::move(code), - {sourcePath, line}, - std::move(keyword), - "", - std::move(message)}; - return Result::Failure(Status::Failure( - FailureCategory::kInput, {std::move(diagnostic)})); -} - -} // namespace - -Result AbaqusInputReader::read( - const std::filesystem::path& inputPath) const { - const auto sourcePath = normalizedPath(inputPath); - std::ifstream stream{sourcePath, std::ios::binary}; - if (!stream) { - return failure( - sourcePath, - 0U, - "input-file-unreadable", - "", - "The Abaqus input file could not be opened for reading."); - } - - const std::string bytes{ - std::istreambuf_iterator{stream}, - std::istreambuf_iterator{}}; - if (stream.bad()) { - return failure( - sourcePath, - 0U, - "input-file-unreadable", - "", - "The Abaqus input file could not be read completely."); - } - - ParsedInput parsed{sourcePath, contentIdentity(bytes), {}}; - std::size_t lineStart = 0U; - std::size_t lineNumber = 1U; - while (lineStart < bytes.size()) { - const std::size_t newline = bytes.find('\n', lineStart); - const std::size_t lineEnd = - newline == std::string::npos ? bytes.size() : newline; - std::string originalLine = bytes.substr(lineStart, lineEnd - lineStart); - if (!originalLine.empty() && originalLine.back() == '\r') { - originalLine.pop_back(); - } - - const std::string trimmedLine = trim(originalLine); - if (!trimmedLine.empty() && trimmedLine.rfind("**", 0U) != 0U) { - if (trimmedLine.front() == '*') { - const auto fields = splitFields(trimmedLine); - const std::string keywordText = - fields.empty() ? std::string{} : trim( - std::string_view{fields[0]}.substr(1U)); - if (keywordText.empty()) { - return failure( - sourcePath, - lineNumber, - "malformed-keyword", - trimmedLine, - "A keyword line requires a non-empty keyword name."); - } - - KeywordBlock block{ - uppercaseAscii(keywordText), - originalLine, - {}, - {}, - {sourcePath, lineNumber}}; - for (std::size_t index = 1U; index < fields.size(); ++index) { - const std::string& field = fields[index]; - if (field.empty()) { - return failure( - sourcePath, - lineNumber, - "malformed-keyword", - block.canonicalName, - "A keyword parameter name cannot be empty."); - } - - const std::size_t equals = field.find('='); - const std::string parameterName = trim(std::string_view{field}.substr( - 0U, equals)); - if (parameterName.empty()) { - return failure( - sourcePath, - lineNumber, - "malformed-keyword", - block.canonicalName, - "A keyword parameter name cannot be empty."); - } - - KeywordParameter parameter{ - uppercaseAscii(parameterName), std::nullopt}; - if (equals != std::string::npos) { - parameter.value = trim( - std::string_view{field}.substr(equals + 1U)); - } - block.parameters.push_back(std::move(parameter)); - } - parsed.blocks.push_back(std::move(block)); - } else { - if (parsed.blocks.empty()) { - return failure( - sourcePath, - lineNumber, - "orphan-data-line", +Result Failure(const std::filesystem::path& source_path, + std::size_t line, std::string code, + std::string keyword, std::string message) { + Diagnostic diagnostic{Severity::kError, + std::move(code), + {source_path, line}, + std::move(keyword), "", - "A data line must follow a keyword line."); - } - parsed.blocks.back().data.push_back( - {splitFields(originalLine), {sourcePath, lineNumber}}); - } - } - - if (newline == std::string::npos) { - break; - } - lineStart = newline + 1U; - ++lineNumber; - } - - return Result::Success(std::move(parsed)); + std::move(message)}; + return Result::Failure( + Status::Failure(FailureCategory::kInput, {std::move(diagnostic)})); } -} // namespace fesa +} // namespace + +Result AbaqusInputReader::Read( + const std::filesystem::path& input_path) const { + const auto source_path = NormalizedPath(input_path); + std::ifstream stream{source_path, std::ios::binary}; + if (!stream) { + return Failure(source_path, 0U, "input-file-unreadable", "", + "The Abaqus input file could not be opened for reading."); + } + + const std::string bytes{std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; + if (stream.bad()) { + return Failure(source_path, 0U, "input-file-unreadable", "", + "The Abaqus input file could not be read completely."); + } + + ParsedInput parsed{source_path, ContentIdentity(bytes), {}}; + std::size_t line_start = 0U; + std::size_t line_number = 1U; + while (line_start < bytes.size()) { + const std::size_t newline = bytes.find('\n', line_start); + const std::size_t line_end = + newline == std::string::npos ? bytes.size() : newline; + std::string original_line = bytes.substr(line_start, line_end - line_start); + if (!original_line.empty() && original_line.back() == '\r') { + original_line.pop_back(); + } + + const std::string trimmed_line = Trim(original_line); + if (!trimmed_line.empty() && trimmed_line.rfind("**", 0U) != 0U) { + if (trimmed_line.front() == '*') { + const auto fields = SplitFields(trimmed_line); + const std::string keyword_text = + fields.empty() ? std::string{} + : Trim(std::string_view{fields[0]}.substr(1U)); + if (keyword_text.empty()) { + return Failure(source_path, line_number, "malformed-keyword", + trimmed_line, + "A keyword line requires a non-empty keyword name."); + } + + KeywordBlock block{UppercaseAscii(keyword_text), + original_line, + {}, + {}, + {source_path, line_number}}; + for (std::size_t index = 1U; index < fields.size(); ++index) { + const std::string& field = fields[index]; + if (field.empty()) { + return Failure(source_path, line_number, "malformed-keyword", + block.canonical_name, + "A keyword parameter name cannot be empty."); + } + + const std::size_t equals = field.find('='); + const std::string parameter_name = + Trim(std::string_view{field}.substr(0U, equals)); + if (parameter_name.empty()) { + return Failure(source_path, line_number, "malformed-keyword", + block.canonical_name, + "A keyword parameter name cannot be empty."); + } + + KeywordParameter parameter{UppercaseAscii(parameter_name), + std::nullopt}; + if (equals != std::string::npos) { + parameter.value = Trim(std::string_view{field}.substr(equals + 1U)); + } + block.parameters.push_back(std::move(parameter)); + } + parsed.blocks.push_back(std::move(block)); + } else { + if (parsed.blocks.empty()) { + return Failure(source_path, line_number, "orphan-data-line", "", + "A data line must follow a keyword line."); + } + parsed.blocks.back().data.push_back( + {SplitFields(original_line), {source_path, line_number}}); + } + } + + if (newline == std::string::npos) { + break; + } + line_start = newline + 1U; + ++line_number; + } + + return Result::Success(std::move(parsed)); +} + +} // namespace fesa diff --git a/src/fesa/io/hdf5/hdf5_results_writer.cpp b/src/fesa/io/hdf5/hdf5_results_writer.cpp index 33ffaa4..c04794b 100644 --- a/src/fesa/io/hdf5/hdf5_results_writer.cpp +++ b/src/fesa/io/hdf5/hdf5_results_writer.cpp @@ -1,12 +1,7 @@ #define NOMINMAX +#include "fesa/io/hdf5/hdf5_results_writer.h" + #include - -#include "fesa/io/hdf5/hdf5_results_writer.hpp" - -#include "fesa/analysis/analysis_model.h" -#include "fesa/build_info.h" -#include "fesa/fem/dof_manager.h" - #include #include @@ -22,6 +17,10 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/build_info.h" +#include "fesa/fem/dof_manager.h" + namespace fesa { namespace { @@ -39,2563 +38,2289 @@ constexpr const char* kStepName = "Step-1"; constexpr std::size_t kFrameIndex = 0U; constexpr const char* kStepRoot = "/steps/Step-1/frames/0"; +/// @brief Owns one HDF5 backend identifier and its matching close operation. +/// @note Move-only lifetime keeps HDF5 handles inside the writer +/// implementation. class Hdf5Handle { -public: - using Closer = herr_t (*)(hid_t); + public: + using Closer = herr_t (*)(hid_t); - Hdf5Handle() = default; - Hdf5Handle(const hid_t value, Closer closer) - : value_{value}, closer_{closer} {} - Hdf5Handle(const Hdf5Handle&) = delete; - Hdf5Handle& operator=(const Hdf5Handle&) = delete; - Hdf5Handle(Hdf5Handle&& other) noexcept - : value_{other.value_}, closer_{other.closer_} { - other.value_ = -1; - other.closer_ = nullptr; + Hdf5Handle() = default; + Hdf5Handle(const hid_t value, Closer closer) + : value_{value}, closer_{closer} {} + Hdf5Handle(const Hdf5Handle&) = delete; + Hdf5Handle& operator=(const Hdf5Handle&) = delete; + Hdf5Handle(Hdf5Handle&& other) noexcept + : value_{other.value_}, closer_{other.closer_} { + other.value_ = -1; + other.closer_ = nullptr; + } + Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { + if (this != &other) { + Reset(); + value_ = other.value_; + closer_ = other.closer_; + other.value_ = -1; + other.closer_ = nullptr; } - Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { - if (this != &other) { - reset(); - value_ = other.value_; - closer_ = other.closer_; - other.value_ = -1; - other.closer_ = nullptr; - } - return *this; - } - ~Hdf5Handle() { reset(); } + return *this; + } + ~Hdf5Handle() { Reset(); } - hid_t get() const noexcept { return value_; } - herr_t closeChecked() noexcept { - if (value_ < 0 || closer_ == nullptr) { - return 0; - } - const hid_t value = value_; - const Closer closer = closer_; - value_ = -1; - closer_ = nullptr; - return closer(value); + hid_t Get() const noexcept { return value_; } + herr_t CloseChecked() noexcept { + if (value_ < 0 || closer_ == nullptr) { + return 0; } + const hid_t value = value_; + const Closer closer = closer_; + value_ = -1; + closer_ = nullptr; + return closer(value); + } -private: - void reset() noexcept { - if (value_ >= 0 && closer_ != nullptr) { - (void)closer_(value_); - } - value_ = -1; - closer_ = nullptr; + private: + void Reset() noexcept { + if (value_ >= 0 && closer_ != nullptr) { + (void)closer_(value_); } + value_ = -1; + closer_ = nullptr; + } - hid_t value_{-1}; - Closer closer_{nullptr}; + hid_t value_{-1}; + Closer closer_{nullptr}; }; -// Expected backend failures become one structured FESA diagnostic, not an -// HDF5 error-stack dump mixed into deterministic CLI output. +/// @brief Temporarily suppresses HDF5's process-global automatic error printer. +/// @note Expected backend failures become one structured FESA diagnostic, not +/// an HDF5 error-stack dump mixed into deterministic CLI output. class Hdf5ErrorSilencer { -public: - Hdf5ErrorSilencer() { - if (H5Eget_auto2(H5E_DEFAULT, &callback_, &clientData_) >= 0 && - H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { - active_ = true; - } + public: + Hdf5ErrorSilencer() { + if (H5Eget_auto2(H5E_DEFAULT, &callback_, &client_data_) >= 0 && + H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { + active_ = true; } - Hdf5ErrorSilencer(const Hdf5ErrorSilencer&) = delete; - Hdf5ErrorSilencer& operator=(const Hdf5ErrorSilencer&) = delete; - ~Hdf5ErrorSilencer() { - if (active_) { - (void)H5Eset_auto2(H5E_DEFAULT, callback_, clientData_); - } + } + Hdf5ErrorSilencer(const Hdf5ErrorSilencer&) = delete; + Hdf5ErrorSilencer& operator=(const Hdf5ErrorSilencer&) = delete; + ~Hdf5ErrorSilencer() { + if (active_) { + (void)H5Eset_auto2(H5E_DEFAULT, callback_, client_data_); } + } -private: - H5E_auto2_t callback_{nullptr}; - void* clientData_{nullptr}; - bool active_{false}; + private: + H5E_auto2_t callback_{nullptr}; + void* client_data_{nullptr}; + bool active_{false}; }; +/// @brief Removes an uncommitted temporary result file on every failure path. +/// @note Release is called only after the atomic finalization operation +/// succeeds. class TemporaryFileGuard { -public: - explicit TemporaryFileGuard(std::filesystem::path path) - : path_{std::move(path)} {} - TemporaryFileGuard(const TemporaryFileGuard&) = delete; - TemporaryFileGuard& operator=(const TemporaryFileGuard&) = delete; - ~TemporaryFileGuard() { - if (active_) { - std::error_code ignored; - (void)std::filesystem::remove(path_, ignored); - } + public: + explicit TemporaryFileGuard(std::filesystem::path path) + : path_{std::move(path)} {} + TemporaryFileGuard(const TemporaryFileGuard&) = delete; + TemporaryFileGuard& operator=(const TemporaryFileGuard&) = delete; + ~TemporaryFileGuard() { + if (active_) { + std::error_code ignored; + (void)std::filesystem::remove(path_, ignored); } - void release() noexcept { active_ = false; } + } + void Release() noexcept { active_ = false; } -private: - std::filesystem::path path_; - bool active_{true}; + private: + std::filesystem::path path_; + bool active_{true}; }; class Hdf5Failure final : public std::runtime_error { -public: - explicit Hdf5Failure(const std::string& message) - : std::runtime_error{message} {} + public: + explicit Hdf5Failure(const std::string& message) + : std::runtime_error{message} {} }; -void requireHdf5(const herr_t result, const char* message) { - if (result < 0) { - throw Hdf5Failure{message}; +void RequireHdf5(const herr_t result, const char* message) { + if (result < 0) { + throw Hdf5Failure{message}; + } +} + +hid_t RequireHdf5Id(const hid_t result, const char* message) { + if (result < 0) { + throw Hdf5Failure{message}; + } + return result; +} + +Status OutputFailure(const std::string& code, const std::string& message) { + return Status::Failure(FailureCategory::kOutput, + {{Severity::kError, code, {}, "", "", message}}); +} + +bool IsFinite(const std::array& values) { + return std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); }); +} + +template +bool IsFinite(const std::array& values) { + return std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); }); +} + +bool IsValidUtf8(const std::string& value) { + const auto* bytes = reinterpret_cast(value.data()); + std::size_t index = 0U; + while (index < value.size()) { + const unsigned char first = bytes[index]; + if (first <= 0x7fU) { + ++index; + continue; } -} -hid_t requireHdf5Id(const hid_t result, const char* message) { - if (result < 0) { - throw Hdf5Failure{message}; + std::size_t continuation_count = 0U; + std::uint32_t code_point = 0U; + if (first >= 0xc2U && first <= 0xdfU) { + continuation_count = 1U; + code_point = first & 0x1fU; + } else if (first >= 0xe0U && first <= 0xefU) { + continuation_count = 2U; + code_point = first & 0x0fU; + } else if (first >= 0xf0U && first <= 0xf4U) { + continuation_count = 3U; + code_point = first & 0x07U; + } else { + return false; } - return result; -} - -Status outputFailure(const std::string& code, const std::string& message) { - return Status::Failure( - FailureCategory::kOutput, - {{Severity::kError, code, {}, "", "", message}}); -} - -bool isFinite(const std::array& values) { - return std::all_of(values.begin(), values.end(), [](const double value) { - return std::isfinite(value); - }); -} - -template -bool isFinite(const std::array& values) { - return std::all_of(values.begin(), values.end(), [](const double value) { - return std::isfinite(value); - }); -} - -bool isValidUtf8(const std::string& value) { - const auto* bytes = reinterpret_cast(value.data()); - std::size_t index = 0U; - while (index < value.size()) { - const unsigned char first = bytes[index]; - if (first <= 0x7fU) { - ++index; - continue; - } - - std::size_t continuationCount = 0U; - std::uint32_t codePoint = 0U; - if (first >= 0xc2U && first <= 0xdfU) { - continuationCount = 1U; - codePoint = first & 0x1fU; - } else if (first >= 0xe0U && first <= 0xefU) { - continuationCount = 2U; - codePoint = first & 0x0fU; - } else if (first >= 0xf0U && first <= 0xf4U) { - continuationCount = 3U; - codePoint = first & 0x07U; - } else { - return false; - } - if (index + continuationCount >= value.size()) { - return false; - } - for (std::size_t offset = 1U; offset <= continuationCount; ++offset) { - const unsigned char next = bytes[index + offset]; - if ((next & 0xc0U) != 0x80U) { - return false; - } - codePoint = (codePoint << 6U) | (next & 0x3fU); - } - if ((continuationCount == 2U && codePoint < 0x800U) || - (continuationCount == 3U && codePoint < 0x10000U) || - (codePoint >= 0xd800U && codePoint <= 0xdfffU) || - codePoint > 0x10ffffU) { - return false; - } - index += continuationCount + 1U; + if (index + continuation_count >= value.size()) { + return false; } - return true; + for (std::size_t offset = 1U; offset <= continuation_count; ++offset) { + const unsigned char next = bytes[index + offset]; + if ((next & 0xc0U) != 0x80U) { + return false; + } + code_point = (code_point << 6U) | (next & 0x3fU); + } + if ((continuation_count == 2U && code_point < 0x800U) || + (continuation_count == 3U && code_point < 0x10000U) || + (code_point >= 0xd800U && code_point <= 0xdfffU) || + code_point > 0x10ffffU) { + return false; + } + index += continuation_count + 1U; + } + return true; } -bool sameIdentity(const SourceEntityId& left, const SourceEntityId& right) { - return left.instance_name == right.instance_name && - left.source_label == right.source_label && - left.source_label_text == right.source_label_text; +bool SameIdentity(const SourceEntityId& left, const SourceEntityId& right) { + return left.instance_name == right.instance_name && + left.source_label == right.source_label && + left.source_label_text == right.source_label_text; } using AxisSet = std::array; struct WriterModelData { - std::vector beamLocalAxes; - std::vector constraintMask; - std::vector prescribedDisplacement; + std::vector beam_local_axes; + std::vector constraint_mask; + std::vector prescribed_displacement; }; -bool isShellDomain(const Domain& domain) noexcept { - return !domain.ShellElements().empty(); +bool IsShellDomain(const Domain& domain) noexcept { + return !domain.ShellElements().empty(); } -const char* shellSourceTypeName(const ShellSourceElementType type) { - return type == ShellSourceElementType::kS4 ? "S4" : "S4R"; +const char* ShellSourceTypeName(const ShellSourceElementType type) { + return type == ShellSourceElementType::kS4 ? "S4" : "S4R"; } -bool isOrthonormalRightHanded( +bool IsOrthonormalRightHanded( const std::array, 3>& frame) { - constexpr double tolerance = 1.0e-12; - const auto dot = [](const std::array& left, - const std::array& right) { - return left[0U] * right[0U] + left[1U] * right[1U] + - left[2U] * right[2U]; - }; - for (const auto& axis : frame) { - if (!isFinite(axis) || std::abs(dot(axis, axis) - 1.0) > tolerance) { - return false; - } + constexpr double kTolerance = 1.0e-12; + const auto dot = [](const std::array& left, + const std::array& right) { + return left[0U] * right[0U] + left[1U] * right[1U] + left[2U] * right[2U]; + }; + for (const auto& axis : frame) { + if (!IsFinite(axis) || std::abs(dot(axis, axis) - 1.0) > kTolerance) { + return false; } - if (std::abs(dot(frame[0U], frame[1U])) > tolerance || - std::abs(dot(frame[0U], frame[2U])) > tolerance || - std::abs(dot(frame[1U], frame[2U])) > tolerance) { - return false; - } - const std::array cross = { - frame[0U][1U] * frame[1U][2U] - frame[0U][2U] * frame[1U][1U], - frame[0U][2U] * frame[1U][0U] - frame[0U][0U] * frame[1U][2U], - frame[0U][0U] * frame[1U][1U] - frame[0U][1U] * frame[1U][0U]}; - return dot(cross, frame[2U]) > 0.0 && - std::abs(dot(cross, frame[2U]) - 1.0) <= tolerance; + } + if (std::abs(dot(frame[0U], frame[1U])) > kTolerance || + std::abs(dot(frame[0U], frame[2U])) > kTolerance || + std::abs(dot(frame[1U], frame[2U])) > kTolerance) { + return false; + } + const std::array cross = { + frame[0U][1U] * frame[1U][2U] - frame[0U][2U] * frame[1U][1U], + frame[0U][2U] * frame[1U][0U] - frame[0U][0U] * frame[1U][2U], + frame[0U][0U] * frame[1U][1U] - frame[0U][1U] * frame[1U][0U]}; + return dot(cross, frame[2U]) > 0.0 && + std::abs(dot(cross, frame[2U]) - 1.0) <= kTolerance; } -bool computeLocalAxes( - const Domain& domain, const EulerBeam3DDefinition& element, AxisSet& axes) { - if (element.node_indices[0U] >= domain.Nodes().size() || - element.node_indices[1U] >= domain.Nodes().size() || - element.section_index >= domain.Sections().size()) { - return false; - } - const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; - const auto& second = domain.Nodes()[element.node_indices[1U]].coordinates; - const auto& guide = domain.Sections()[element.section_index].first_axis; - const std::array delta = { - second[0U] - first[0U], - second[1U] - first[1U], - second[2U] - first[2U]}; - const double length = std::hypot(delta[0U], delta[1U], delta[2U]); - if (!isFinite(first) || !isFinite(second) || !isFinite(guide) || - !isFinite(delta) || !std::isfinite(length) || !(length > 0.0)) { - return false; - } - const std::array x = { - delta[0U] / length, delta[1U] / length, delta[2U] / length}; - const double projection = - guide[0U] * x[0U] + guide[1U] * x[1U] + guide[2U] * x[2U]; - const std::array yTrial = { - guide[0U] - projection * x[0U], - guide[1U] - projection * x[1U], - guide[2U] - projection * x[2U]}; - const double yNorm = std::hypot(yTrial[0U], yTrial[1U], yTrial[2U]); - if (!isFinite(yTrial) || !std::isfinite(yNorm) || !(yNorm > 0.0)) { - return false; - } - const std::array y = { - yTrial[0U] / yNorm, yTrial[1U] / yNorm, yTrial[2U] / yNorm}; - const std::array z = { - x[1U] * y[2U] - x[2U] * y[1U], - x[2U] * y[0U] - x[0U] * y[2U], - x[0U] * y[1U] - x[1U] * y[0U]}; - axes = { - x[0U], x[1U], x[2U], - y[0U], y[1U], y[2U], - z[0U], z[1U], z[2U]}; - return isFinite(axes); +bool ComputeLocalAxes(const Domain& domain, + const EulerBeam3DDefinition& element, AxisSet& axes) { + if (element.node_indices[0U] >= domain.Nodes().size() || + element.node_indices[1U] >= domain.Nodes().size() || + element.section_index >= domain.Sections().size()) { + return false; + } + const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; + const auto& second = domain.Nodes()[element.node_indices[1U]].coordinates; + const auto& guide = domain.Sections()[element.section_index].first_axis; + const std::array delta = { + second[0U] - first[0U], second[1U] - first[1U], second[2U] - first[2U]}; + const double length = std::hypot(delta[0U], delta[1U], delta[2U]); + if (!IsFinite(first) || !IsFinite(second) || !IsFinite(guide) || + !IsFinite(delta) || !std::isfinite(length) || !(length > 0.0)) { + return false; + } + const std::array x = {delta[0U] / length, delta[1U] / length, + delta[2U] / length}; + const double projection = + guide[0U] * x[0U] + guide[1U] * x[1U] + guide[2U] * x[2U]; + const std::array y_trial = {guide[0U] - projection * x[0U], + guide[1U] - projection * x[1U], + guide[2U] - projection * x[2U]}; + const double y_norm = std::hypot(y_trial[0U], y_trial[1U], y_trial[2U]); + if (!IsFinite(y_trial) || !std::isfinite(y_norm) || !(y_norm > 0.0)) { + return false; + } + const std::array y = {y_trial[0U] / y_norm, y_trial[1U] / y_norm, + y_trial[2U] / y_norm}; + const std::array z = {x[1U] * y[2U] - x[2U] * y[1U], + x[2U] * y[0U] - x[0U] * y[2U], + x[0U] * y[1U] - x[1U] * y[0U]}; + axes = {x[0U], x[1U], x[2U], y[0U], y[1U], y[2U], z[0U], z[1U], z[2U]}; + return IsFinite(axes); } -bool sizeProductFits( - const std::size_t left, const std::size_t right, std::size_t& product) { - if (right != 0U && left > (std::numeric_limits::max)() / right) { - return false; - } - product = left * right; - return true; +bool SizeProductFits(const std::size_t left, const std::size_t right, + std::size_t& product) { + if (right != 0U && left > (std::numeric_limits::max)() / right) { + return false; + } + product = left * right; + return true; } -Status validateShellWriterInput( - const Domain& domain, const AnalysisState& state) { - if (!domain.Elements().empty()) { - return outputFailure( - "invalid-result-identity", - "Schema v0 does not combine B33 and FESA-MITC4 element inventories."); +Status ValidateShellWriterInput(const Domain& domain, + const AnalysisState& state) { + if (!domain.Elements().empty()) { + return OutputFailure( + "invalid-result-identity", + "Schema v0 does not combine B33 and FESA-MITC4 element inventories."); + } + for (const auto& material : domain.Materials()) { + if (material.name.empty() || !IsValidUtf8(material.name) || + !std::isfinite(material.youngs_modulus) || + !std::isfinite(material.poisson_ratio) || + !(material.youngs_modulus > 0.0)) { + return OutputFailure("invalid-result-identity", + "Every shell material requires a UTF-8 name and " + "finite constitutive data."); } - for (const auto& material : domain.Materials()) { - if (material.name.empty() || !isValidUtf8(material.name) || - !std::isfinite(material.youngs_modulus) || - !std::isfinite(material.poisson_ratio) || - !(material.youngs_modulus > 0.0)) { - return outputFailure( - "invalid-result-identity", - "Every shell material requires a UTF-8 name and finite constitutive data."); - } + } + for (const auto& section : domain.ShellSections()) { + if (section.name.empty() || !IsValidUtf8(section.name) || + section.material_index >= domain.Materials().size() || + !std::isfinite(section.thickness) || !(section.thickness > 0.0)) { + return OutputFailure("invalid-result-identity", + "Every shell section requires stable material " + "identity and positive finite thickness."); } - for (const auto& section : domain.ShellSections()) { - if (section.name.empty() || !isValidUtf8(section.name) || - section.material_index >= domain.Materials().size() || - !std::isfinite(section.thickness) || !(section.thickness > 0.0)) { - return outputFailure( - "invalid-result-identity", - "Every shell section requires stable material identity and positive finite thickness."); - } + } + for (const auto& element : domain.ShellElements()) { + if ((element.source_type != ShellSourceElementType::kS4 && + element.source_type != ShellSourceElementType::kS4r) || + element.source_id.source_label <= 0 || + element.source_id.source_label_text.empty() || + !IsValidUtf8(element.source_id.instance_name) || + !IsValidUtf8(element.source_id.source_label_text) || + element.material_index >= domain.Materials().size() || + element.section_index >= domain.ShellSections().size() || + domain.ShellSections()[element.section_index].material_index != + element.material_index) { + return OutputFailure("invalid-result-identity", + "Every shell element requires stable source, " + "material, and section identity."); } - for (const auto& element : domain.ShellElements()) { - if ((element.source_type != ShellSourceElementType::kS4 && - element.source_type != ShellSourceElementType::kS4r) || - element.source_id.source_label <= 0 || - element.source_id.source_label_text.empty() || - !isValidUtf8(element.source_id.instance_name) || - !isValidUtf8(element.source_id.source_label_text) || - element.material_index >= domain.Materials().size() || - element.section_index >= domain.ShellSections().size() || - domain.ShellSections()[element.section_index].material_index != - element.material_index) { - return outputFailure( - "invalid-result-identity", - "Every shell element requires stable source, material, and section identity."); - } - std::array sortedNodes = element.node_indices; - std::sort(sortedNodes.begin(), sortedNodes.end()); - if (sortedNodes.back() >= domain.Nodes().size() || - std::adjacent_find(sortedNodes.begin(), sortedNodes.end()) != - sortedNodes.end()) { - return outputFailure( - "invalid-result-identity", - "Every shell element requires four distinct valid node identities."); - } + std::array sorted_nodes = + element.node_indices; + std::sort(sorted_nodes.begin(), sorted_nodes.end()); + if (sorted_nodes.back() >= domain.Nodes().size() || + std::adjacent_find(sorted_nodes.begin(), sorted_nodes.end()) != + sorted_nodes.end()) { + return OutputFailure( + "invalid-result-identity", + "Every shell element requires four distinct valid node identities."); } - if (domain.ShellNodeInitialFrames().size() != domain.Nodes().size()) { - return outputFailure( - "invalid-result-identity", - "Shell output requires one initial director/frame per source-ordered node."); - } - for (std::size_t node = 0U; - node < domain.ShellNodeInitialFrames().size(); - ++node) { - const auto& source = domain.ShellNodeInitialFrames()[node]; - const std::array, 3> frame{ - source.tangent_a, source.tangent_b, source.director}; - if (source.node_index != node || !isOrthonormalRightHanded(frame)) { - return outputFailure( - "invalid-result-identity", - "Shell initial frames must be finite, orthonormal, right-handed, and node ordered."); - } + } + if (domain.ShellNodeInitialFrames().size() != domain.Nodes().size()) { + return OutputFailure("invalid-result-identity", + "Shell output requires one initial director/frame per " + "source-ordered node."); + } + for (std::size_t node = 0U; node < domain.ShellNodeInitialFrames().size(); + ++node) { + const auto& source = domain.ShellNodeInitialFrames()[node]; + const std::array, 3> frame{ + source.tangent_a, source.tangent_b, source.director}; + if (source.node_index != node || !IsOrthonormalRightHanded(frame)) { + return OutputFailure("invalid-result-identity", + "Shell initial frames must be finite, orthonormal, " + "right-handed, and node ordered."); } + } - std::size_t expectedRows = 0U; - if (!sizeProductFits( - domain.ShellElements().size(), kShellLocationCount, expectedRows) || - state.ShellResults().size() != expectedRows) { - return outputFailure( - "invalid-result-rows", - "Shell output requires exactly GP1 through GP4 for every shell element."); + std::size_t expected_rows = 0U; + if (!SizeProductFits(domain.ShellElements().size(), kShellLocationCount, + expected_rows) || + state.ShellResults().size() != expected_rows) { + return OutputFailure("invalid-result-rows", + "Shell output requires exactly GP1 through GP4 for " + "every shell element."); + } + const double gauss = 1.0 / std::sqrt(3.0); + const std::array locations{ + ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2, + ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4}; + const std::array, kShellLocationCount> coordinates{ + {{-gauss, -gauss}, {gauss, -gauss}, {gauss, gauss}, {-gauss, gauss}}}; + const std::array positions{ + ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle, + ShellSectionPosition::kTop}; + constexpr std::array kZeta{-1.0, 0.0, + 1.0}; + for (std::size_t row_index = 0U; row_index < state.ShellResults().size(); + ++row_index) { + const auto& row = state.ShellResults()[row_index]; + const std::size_t element = row_index / kShellLocationCount; + const std::size_t location = row_index % kShellLocationCount; + if (row.element != element || row.location != locations[location] || + row.natural_coordinates != coordinates[location] || + !IsOrthonormalRightHanded(row.local_frame) || + !IsFinite(row.generalized_strain) || !IsFinite(row.section_resultant)) { + return OutputFailure("invalid-result-rows", + "Shell result rows must be finite and preserve " + "element/GP/frame identity."); } - const double gauss = 1.0 / std::sqrt(3.0); - const std::array locations{ - ShellMidsurfaceLocation::kGp1, - ShellMidsurfaceLocation::kGp2, - ShellMidsurfaceLocation::kGp3, - ShellMidsurfaceLocation::kGp4}; - const std::array, kShellLocationCount> coordinates{{ - {-gauss, -gauss}, - {gauss, -gauss}, - {gauss, gauss}, - {-gauss, gauss}}}; - const std::array positions{ - ShellSectionPosition::kBottom, - ShellSectionPosition::kMiddle, - ShellSectionPosition::kTop}; - constexpr std::array zeta{-1.0, 0.0, 1.0}; - for (std::size_t rowIndex = 0U; - rowIndex < state.ShellResults().size(); - ++rowIndex) { - const auto& row = state.ShellResults()[rowIndex]; - const std::size_t element = rowIndex / kShellLocationCount; - const std::size_t location = rowIndex % kShellLocationCount; - if (row.element != element || row.location != locations[location] || - row.natural_coordinates != coordinates[location] || - !isOrthonormalRightHanded(row.local_frame) || - !isFinite(row.generalized_strain) || - !isFinite(row.section_resultant)) { - return outputFailure( - "invalid-result-rows", - "Shell result rows must be finite and preserve element/GP/frame identity."); - } - for (std::size_t position = 0U; - position < kShellSectionPositionCount; - ++position) { - if (row.stress[position].position != positions[position] || - row.stress[position].zeta != zeta[position] || - !isFinite(row.stress[position].components)) { - return outputFailure( - "invalid-result-rows", - "Shell stress rows must preserve BOTTOM, MIDDLE, TOP identity."); - } - } + for (std::size_t position = 0U; position < kShellSectionPositionCount; + ++position) { + if (row.stress[position].position != positions[position] || + row.stress[position].zeta != kZeta[position] || + !IsFinite(row.stress[position].components)) { + return OutputFailure("invalid-result-rows", + "Shell stress rows must preserve BOTTOM, " + "MIDDLE, TOP identity."); + } } - if (!std::isfinite(state.PhysicalStrainEnergy()) || - !isFinite(state.Equilibrium()) || - !isFinite(state.VerificationMetrics())) { - return outputFailure( - "invalid-result-rows", - "Shell energy, equilibrium, and verification metrics must be finite."); - } - return Status::Ok(); + } + if (!std::isfinite(state.PhysicalStrainEnergy()) || + !IsFinite(state.Equilibrium()) || + !IsFinite(state.VerificationMetrics())) { + return OutputFailure( + "invalid-result-rows", + "Shell energy, equilibrium, and verification metrics must be finite."); + } + return Status::Ok(); } -Status validateWriterInput( - const std::filesystem::path& outputPath, - const Domain& domain, - const AnalysisState& state, - const std::vector& diagnostics, - WriterModelData& modelData) { - if (outputPath.empty() || outputPath.filename().empty()) { - return outputFailure( - "invalid-output-path", "The HDF5 output path must name a file."); - } - if (state.Identity().step_name != kStepName || - state.Identity().frame_index != kFrameIndex) { - return outputFailure( - "invalid-result-state", - "Schema v0 requires literal Step-1 and frame index 0."); - } +Status ValidateWriterInput(const std::filesystem::path& output_path, + const Domain& domain, const AnalysisState& state, + const std::vector& diagnostics, + WriterModelData& model_data) { + if (output_path.empty() || output_path.filename().empty()) { + return OutputFailure("invalid-output-path", + "The HDF5 output path must name a file."); + } + if (state.Identity().step_name != kStepName || + state.Identity().frame_index != kFrameIndex) { + return OutputFailure( + "invalid-result-state", + "Schema v0 requires literal Step-1 and frame index 0."); + } - const bool shell = isShellDomain(domain); - if (shell) { - const Status shellValidation = validateShellWriterInput(domain, state); - if (!shellValidation.IsOk()) { - return shellValidation; + const bool shell = IsShellDomain(domain); + if (shell) { + const Status shell_validation = ValidateShellWriterInput(domain, state); + if (!shell_validation.IsOk()) { + return shell_validation; + } + } else if (!state.ShellResults().empty()) { + return OutputFailure("invalid-result-rows", + "Beam output cannot contain shell recovery rows."); + } + + std::size_t full_dof_count = 0U; + if (!SizeProductFits(domain.Nodes().size(), kDofsPerNode, full_dof_count)) { + return OutputFailure("invalid-result-state", + "The nodal result shape overflows size_t."); + } + const std::array vectors = { + &state.Displacement(), &state.ExternalForce(), &state.InternalForce(), + &state.Residual(), &state.Reaction()}; + for (const Vector* vector : vectors) { + if (vector->Size() != full_dof_count) { + return OutputFailure( + "invalid-result-state", + "Every V0 analysis vector must have node_count*6 values."); + } + for (std::size_t index = 0U; index < vector->Size(); ++index) { + if (!std::isfinite((*vector)[index])) { + return OutputFailure("invalid-result-state", + "Every V0 analysis vector value must be finite."); + } + } + } + + if (domain.SourcePath().empty() || domain.SourceContentIdentity().empty() || + !IsValidUtf8(domain.SourceContentIdentity())) { + return OutputFailure( + "invalid-result-identity", + "Source path and UTF-8 content identity are required."); + } + for (const Node& node : domain.Nodes()) { + if (node.source_id.source_label <= 0 || + node.source_id.source_label_text.empty() || + !IsValidUtf8(node.source_id.instance_name) || + !IsValidUtf8(node.source_id.source_label_text) || + !IsFinite(node.coordinates)) { + return OutputFailure( + "invalid-result-identity", + "Every node requires finite coordinates and UTF-8 source identity."); + } + } + + model_data.beam_local_axes.clear(); + model_data.beam_local_axes.reserve(domain.Elements().size()); + for (const EulerBeam3DDefinition& element : domain.Elements()) { + AxisSet axes{}; + if (element.source_id.source_label <= 0 || + element.source_id.source_label_text.empty() || + !IsValidUtf8(element.source_id.instance_name) || + !IsValidUtf8(element.source_id.source_label_text) || + element.node_indices[0U] == element.node_indices[1U] || + element.material_index >= domain.Materials().size() || + !ComputeLocalAxes(domain, element, axes)) { + return OutputFailure("invalid-result-identity", + "Every element requires valid source, connectivity, " + "property, and local-axis identity."); + } + model_data.beam_local_axes.push_back(axes); + } + + std::size_t endpoint_count = 0U; + std::size_t gauss_count = 0U; + if (!SizeProductFits(domain.Elements().size(), kEndpointCount, + endpoint_count) || + !SizeProductFits(domain.Elements().size(), kGaussPointCount, + gauss_count) || + state.EndpointResults().size() != endpoint_count || + state.GaussResults().size() != gauss_count) { + return OutputFailure( + "invalid-result-rows", + "Endpoint and Gauss row counts must match every element and location."); + } + for (std::size_t row_index = 0U; row_index < state.EndpointResults().size(); + ++row_index) { + const EntityIndex expected_element = + static_cast(row_index / kEndpointCount); + const int expected_endpoint = static_cast(row_index % kEndpointCount); + const EndpointResultRow& row = state.EndpointResults()[row_index]; + const auto& element = domain.Elements()[expected_element]; + const auto& expected_node = + domain.Nodes()[element.node_indices[static_cast( + expected_endpoint)]]; + if (row.element != expected_element || row.endpoint != expected_endpoint || + !SameIdentity(row.node, expected_node.source_id) || + !IsFinite(row.end_action) || !IsFinite(row.section_resultant)) { + return OutputFailure("invalid-result-rows", + "Endpoint result rows must follow " + "element/endpoint order and identity."); + } + } + for (std::size_t row_index = 0U; row_index < state.GaussResults().size(); + ++row_index) { + const EntityIndex expected_element = + static_cast(row_index / kGaussPointCount); + const int expected_gauss_point = + static_cast(row_index % kGaussPointCount) + 1; + const GaussResultRow& row = state.GaussResults()[row_index]; + if (row.element != expected_element || + row.gauss_point != expected_gauss_point || + !IsFinite(row.generalized_strain) || + !IsFinite(row.generalized_resultant)) { + return OutputFailure( + "invalid-result-rows", + "Gauss result rows must follow element/Gauss order and identity."); + } + } + + std::size_t stress_index = 0U; + for (std::size_t element_index = 0U; element_index < domain.Elements().size(); + ++element_index) { + const auto& element = domain.Elements()[element_index]; + const auto& section_points = + domain.Sections()[element.section_index].section_points; + for (std::size_t gauss = 0U; gauss < kGaussPointCount; ++gauss) { + const std::size_t count = + section_points.empty() ? 1U : section_points.size(); + for (std::size_t point = 0U; point < count; ++point) { + if (stress_index >= state.StressResults().size()) { + return OutputFailure("invalid-result-rows", + "Axial stress rows are missing required " + "element/Gauss/section locations."); } - } else if (!state.ShellResults().empty()) { - return outputFailure( - "invalid-result-rows", - "Beam output cannot contain shell recovery rows."); - } - - std::size_t fullDofCount = 0U; - if (!sizeProductFits(domain.Nodes().size(), kDofsPerNode, fullDofCount)) { - return outputFailure( - "invalid-result-state", "The nodal result shape overflows size_t."); - } - const std::array vectors = { - &state.Displacement(), - &state.ExternalForce(), - &state.InternalForce(), - &state.Residual(), - &state.Reaction()}; - for (const Vector* vector : vectors) { - if (vector->Size() != fullDofCount) { - return outputFailure( - "invalid-result-state", - "Every V0 analysis vector must have node_count*6 values."); - } - for (std::size_t index = 0U; index < vector->Size(); ++index) { - if (!std::isfinite((*vector)[index])) { - return outputFailure( - "invalid-result-state", - "Every V0 analysis vector value must be finite."); - } + const StressS11Row& row = state.StressResults()[stress_index++]; + const std::size_t expected_point = + section_points.empty() ? 0U : point + 1U; + const double expected_x1 = + section_points.empty() ? 0.0 : section_points[point][0U]; + const double expected_x2 = + section_points.empty() ? 0.0 : section_points[point][1U]; + const char* expected_source = + section_points.empty() ? "fesa-default" : "input"; + if (row.element != static_cast(element_index) || + row.gauss_point != static_cast(gauss + 1U) || + row.section_point != expected_point || row.x1 != expected_x1 || + row.x2 != expected_x2 || row.source != expected_source || + !IsValidUtf8(row.source) || !std::isfinite(row.x1) || + !std::isfinite(row.x2) || !std::isfinite(row.s11)) { + return OutputFailure("invalid-result-rows", + "Axial stress rows must follow exact " + "element/Gauss/section identity."); } + } } + } + if (stress_index != state.StressResults().size()) { + return OutputFailure("invalid-result-rows", + "Axial stress output contains extra rows."); + } - if (domain.SourcePath().empty() || domain.SourceContentIdentity().empty() || - !isValidUtf8(domain.SourceContentIdentity())) { - return outputFailure( - "invalid-result-identity", - "Source path and UTF-8 content identity are required."); - } - for (const Node& node : domain.Nodes()) { - if (node.source_id.source_label <= 0 || - node.source_id.source_label_text.empty() || - !isValidUtf8(node.source_id.instance_name) || - !isValidUtf8(node.source_id.source_label_text) || - !isFinite(node.coordinates)) { - return outputFailure( - "invalid-result-identity", - "Every node requires finite coordinates and UTF-8 source identity."); - } + for (const Diagnostic& diagnostic : diagnostics) { + if (!IsValidUtf8(diagnostic.code) || !IsValidUtf8(diagnostic.keyword) || + !IsValidUtf8(diagnostic.entity_identity) || + !IsValidUtf8(diagnostic.message)) { + return OutputFailure("invalid-result-diagnostic", + "Diagnostic text must be valid UTF-8."); } + } - modelData.beamLocalAxes.clear(); - modelData.beamLocalAxes.reserve(domain.Elements().size()); - for (const EulerBeam3DDefinition& element : domain.Elements()) { - AxisSet axes{}; - if (element.source_id.source_label <= 0 || - element.source_id.source_label_text.empty() || - !isValidUtf8(element.source_id.instance_name) || - !isValidUtf8(element.source_id.source_label_text) || - element.node_indices[0U] == element.node_indices[1U] || - element.material_index >= domain.Materials().size() || - !computeLocalAxes(domain, element, axes)) { - return outputFailure( - "invalid-result-identity", - "Every element requires valid source, connectivity, property, and local-axis identity."); - } - modelData.beamLocalAxes.push_back(axes); + auto analysis_model_result = AnalysisModel::Create(domain); + if (!analysis_model_result.HasValue()) { + return OutputFailure( + "invalid-result-state", + "The HDF5 writer could not reconstruct the active model view."); + } + const AnalysisModel analysis_model = std::move(analysis_model_result.Value()); + auto dof_result = DofManager::Create(analysis_model); + if (!dof_result.HasValue()) { + return OutputFailure( + "invalid-result-state", + "The HDF5 writer could not reconstruct stable constraint identity."); + } + const DofManager dofs = std::move(dof_result.Value()); + model_data.constraint_mask.assign(full_dof_count, 0U); + model_data.prescribed_displacement.assign(full_dof_count, 0.0); + if (dofs.ConstrainedDofs().size() != dofs.PrescribedValues().Size()) { + return OutputFailure( + "invalid-result-state", + "Constraint identities and prescribed values have inconsistent sizes."); + } + for (std::size_t index = 0U; index < dofs.ConstrainedDofs().size(); ++index) { + const std::size_t full_dof = dofs.ConstrainedDofs()[index]; + const double prescribed = dofs.PrescribedValues()[index]; + if (full_dof >= full_dof_count || !std::isfinite(prescribed)) { + return OutputFailure("invalid-result-state", + "Constraint identity and prescribed values must " + "be finite and in range."); } - - std::size_t endpointCount = 0U; - std::size_t gaussCount = 0U; - if (!sizeProductFits(domain.Elements().size(), kEndpointCount, endpointCount) || - !sizeProductFits(domain.Elements().size(), kGaussPointCount, gaussCount) || - state.EndpointResults().size() != endpointCount || - state.GaussResults().size() != gaussCount) { - return outputFailure( - "invalid-result-rows", - "Endpoint and Gauss row counts must match every element and location."); - } - for (std::size_t rowIndex = 0U; - rowIndex < state.EndpointResults().size(); - ++rowIndex) { - const EntityIndex expectedElement = - static_cast(rowIndex / kEndpointCount); - const int expectedEndpoint = static_cast(rowIndex % kEndpointCount); - const EndpointResultRow& row = state.EndpointResults()[rowIndex]; - const auto& element = domain.Elements()[expectedElement]; - const auto& expectedNode = - domain.Nodes()[element.node_indices[static_cast(expectedEndpoint)]]; - if (row.element != expectedElement || row.endpoint != expectedEndpoint || - !sameIdentity(row.node, expectedNode.source_id) || - !isFinite(row.end_action) || !isFinite(row.section_resultant)) { - return outputFailure( - "invalid-result-rows", - "Endpoint result rows must follow element/endpoint order and identity."); - } - } - for (std::size_t rowIndex = 0U; - rowIndex < state.GaussResults().size(); - ++rowIndex) { - const EntityIndex expectedElement = - static_cast(rowIndex / kGaussPointCount); - const int expectedGaussPoint = - static_cast(rowIndex % kGaussPointCount) + 1; - const GaussResultRow& row = state.GaussResults()[rowIndex]; - if (row.element != expectedElement || - row.gauss_point != expectedGaussPoint || - !isFinite(row.generalized_strain) || - !isFinite(row.generalized_resultant)) { - return outputFailure( - "invalid-result-rows", - "Gauss result rows must follow element/Gauss order and identity."); - } - } - - std::size_t stressIndex = 0U; - for (std::size_t elementIndex = 0U; - elementIndex < domain.Elements().size(); - ++elementIndex) { - const auto& element = domain.Elements()[elementIndex]; - const auto& sectionPoints = - domain.Sections()[element.section_index].section_points; - for (std::size_t gauss = 0U; gauss < kGaussPointCount; ++gauss) { - const std::size_t count = sectionPoints.empty() ? 1U : sectionPoints.size(); - for (std::size_t point = 0U; point < count; ++point) { - if (stressIndex >= state.StressResults().size()) { - return outputFailure( - "invalid-result-rows", - "Axial stress rows are missing required element/Gauss/section locations."); - } - const StressS11Row& row = state.StressResults()[stressIndex++]; - const std::size_t expectedPoint = sectionPoints.empty() ? 0U : point + 1U; - const double expectedX1 = sectionPoints.empty() ? 0.0 : sectionPoints[point][0U]; - const double expectedX2 = sectionPoints.empty() ? 0.0 : sectionPoints[point][1U]; - const char* expectedSource = sectionPoints.empty() ? "fesa-default" : "input"; - if (row.element != static_cast(elementIndex) || - row.gauss_point != static_cast(gauss + 1U) || - row.section_point != expectedPoint || - row.x1 != expectedX1 || row.x2 != expectedX2 || - row.source != expectedSource || !isValidUtf8(row.source) || - !std::isfinite(row.x1) || !std::isfinite(row.x2) || - !std::isfinite(row.s11)) { - return outputFailure( - "invalid-result-rows", - "Axial stress rows must follow exact element/Gauss/section identity."); - } - } - } - } - if (stressIndex != state.StressResults().size()) { - return outputFailure( - "invalid-result-rows", "Axial stress output contains extra rows."); - } - - for (const Diagnostic& diagnostic : diagnostics) { - if (!isValidUtf8(diagnostic.code) || - !isValidUtf8(diagnostic.keyword) || - !isValidUtf8(diagnostic.entity_identity) || - !isValidUtf8(diagnostic.message)) { - return outputFailure( - "invalid-result-diagnostic", - "Diagnostic text must be valid UTF-8."); - } - } - - - auto analysisModelResult = AnalysisModel::Create(domain); - if (!analysisModelResult.HasValue()) { - return outputFailure( - "invalid-result-state", - "The HDF5 writer could not reconstruct the active model view."); - } - const AnalysisModel analysisModel = - std::move(analysisModelResult.Value()); - auto dofResult = DofManager::Create(analysisModel); - if (!dofResult.HasValue()) { - return outputFailure( - "invalid-result-state", - "The HDF5 writer could not reconstruct stable constraint identity."); - } - const DofManager dofs = std::move(dofResult.Value()); - modelData.constraintMask.assign(fullDofCount, 0U); - modelData.prescribedDisplacement.assign(fullDofCount, 0.0); - if (dofs.ConstrainedDofs().size() != dofs.PrescribedValues().Size()) { - return outputFailure( - "invalid-result-state", - "Constraint identities and prescribed values have inconsistent sizes."); - } - for (std::size_t index = 0U; - index < dofs.ConstrainedDofs().size(); - ++index) { - const std::size_t fullDof = dofs.ConstrainedDofs()[index]; - const double prescribed = dofs.PrescribedValues()[index]; - if (fullDof >= fullDofCount || !std::isfinite(prescribed)) { - return outputFailure( - "invalid-result-state", - "Constraint identity and prescribed values must be finite and in range."); - } - modelData.constraintMask[fullDof] = 1U; - modelData.prescribedDisplacement[fullDof] = prescribed; - } - return Status::Ok(); + model_data.constraint_mask[full_dof] = 1U; + model_data.prescribed_displacement[full_dof] = prescribed; + } + return Status::Ok(); } -std::string normalizedPathString(const std::filesystem::path& path) { - if (path.empty()) { - return {}; - } - return std::filesystem::absolute(path).lexically_normal().generic_u8string(); +std::string NormalizedPathString(const std::filesystem::path& path) { + if (path.empty()) { + return {}; + } + return std::filesystem::absolute(path).lexically_normal().generic_u8string(); } -std::string sourceInputIdentity(const Domain& domain) { - return "path=" + normalizedPathString(domain.SourcePath()) + - ";content_identity=" + domain.SourceContentIdentity(); +std::string SourceInputIdentity(const Domain& domain) { + return "path=" + NormalizedPathString(domain.SourcePath()) + + ";content_identity=" + domain.SourceContentIdentity(); } -Hdf5Handle makeUtf8StringType() { - Hdf5Handle type{ - requireHdf5Id(H5Tcopy(H5T_C_S1), "Unable to copy an HDF5 string type."), - H5Tclose}; - requireHdf5( - H5Tset_size(type.get(), H5T_VARIABLE), - "Unable to configure a variable-length HDF5 string."); - requireHdf5( - H5Tset_cset(type.get(), H5T_CSET_UTF8), - "Unable to configure UTF-8 HDF5 strings."); - requireHdf5( - H5Tset_strpad(type.get(), H5T_STR_NULLTERM), - "Unable to configure HDF5 string padding."); - return type; +Hdf5Handle MakeUtf8StringType() { + Hdf5Handle type{ + RequireHdf5Id(H5Tcopy(H5T_C_S1), "Unable to copy an HDF5 string type."), + H5Tclose}; + RequireHdf5(H5Tset_size(type.Get(), H5T_VARIABLE), + "Unable to configure a variable-length HDF5 string."); + RequireHdf5(H5Tset_cset(type.Get(), H5T_CSET_UTF8), + "Unable to configure UTF-8 HDF5 strings."); + RequireHdf5(H5Tset_strpad(type.Get(), H5T_STR_NULLTERM), + "Unable to configure HDF5 string padding."); + return type; } -void writeStringAttribute( - const hid_t object, const char* name, const std::string& value) { - auto type = makeUtf8StringType(); - Hdf5Handle space{ - requireHdf5Id(H5Screate(H5S_SCALAR), "Unable to create an attribute space."), - H5Sclose}; - Hdf5Handle attribute{ - requireHdf5Id( - H5Acreate2(object, name, type.get(), space.get(), H5P_DEFAULT, H5P_DEFAULT), - "Unable to create an HDF5 string attribute."), - H5Aclose}; - const char* raw = value.c_str(); - requireHdf5( - H5Awrite(attribute.get(), type.get(), &raw), - "Unable to write an HDF5 string attribute."); +void WriteStringAttribute(const hid_t object, const char* name, + const std::string& value) { + auto type = MakeUtf8StringType(); + Hdf5Handle space{RequireHdf5Id(H5Screate(H5S_SCALAR), + "Unable to create an attribute space."), + H5Sclose}; + Hdf5Handle attribute{ + RequireHdf5Id(H5Acreate2(object, name, type.Get(), space.Get(), + H5P_DEFAULT, H5P_DEFAULT), + "Unable to create an HDF5 string attribute."), + H5Aclose}; + const char* raw = value.c_str(); + RequireHdf5(H5Awrite(attribute.Get(), type.Get(), &raw), + "Unable to write an HDF5 string attribute."); } -void writeUint64Attribute( - const hid_t object, const char* name, const std::uint64_t value) { - Hdf5Handle space{ - requireHdf5Id(H5Screate(H5S_SCALAR), "Unable to create an attribute space."), - H5Sclose}; - Hdf5Handle attribute{ - requireHdf5Id( - H5Acreate2(object, name, H5T_STD_U64LE, space.get(), H5P_DEFAULT, H5P_DEFAULT), - "Unable to create an HDF5 integer attribute."), - H5Aclose}; - requireHdf5( - H5Awrite(attribute.get(), H5T_NATIVE_UINT64, &value), - "Unable to write an HDF5 integer attribute."); +void WriteUint64Attribute(const hid_t object, const char* name, + const std::uint64_t value) { + Hdf5Handle space{RequireHdf5Id(H5Screate(H5S_SCALAR), + "Unable to create an attribute space."), + H5Sclose}; + Hdf5Handle attribute{ + RequireHdf5Id(H5Acreate2(object, name, H5T_STD_U64LE, space.Get(), + H5P_DEFAULT, H5P_DEFAULT), + "Unable to create an HDF5 integer attribute."), + H5Aclose}; + RequireHdf5(H5Awrite(attribute.Get(), H5T_NATIVE_UINT64, &value), + "Unable to write an HDF5 integer attribute."); } -Hdf5Handle createGroup(const hid_t file, const char* path) { - Hdf5Handle linkProperties{ - requireHdf5Id(H5Pcreate(H5P_LINK_CREATE), "Unable to create link properties."), - H5Pclose}; - requireHdf5( - H5Pset_create_intermediate_group(linkProperties.get(), 1U), - "Unable to enable intermediate HDF5 groups."); - return Hdf5Handle{ - requireHdf5Id( - H5Gcreate2(file, path, linkProperties.get(), H5P_DEFAULT, H5P_DEFAULT), - "Unable to create an HDF5 group."), - H5Gclose}; +Hdf5Handle CreateGroup(const hid_t file, const char* path) { + Hdf5Handle link_properties{RequireHdf5Id(H5Pcreate(H5P_LINK_CREATE), + "Unable to create link properties."), + H5Pclose}; + RequireHdf5(H5Pset_create_intermediate_group(link_properties.Get(), 1U), + "Unable to enable intermediate HDF5 groups."); + return Hdf5Handle{RequireHdf5Id(H5Gcreate2(file, path, link_properties.Get(), + H5P_DEFAULT, H5P_DEFAULT), + "Unable to create an HDF5 group."), + H5Gclose}; } -Hdf5Handle createDatasetSpace(const std::vector& dimensions) { - return Hdf5Handle{ - requireHdf5Id( - H5Screate_simple( - static_cast(dimensions.size()), dimensions.data(), nullptr), - "Unable to create an HDF5 dataset space."), - H5Sclose}; +Hdf5Handle CreateDatasetSpace(const std::vector& dimensions) { + return Hdf5Handle{ + RequireHdf5Id(H5Screate_simple(static_cast(dimensions.size()), + dimensions.data(), nullptr), + "Unable to create an HDF5 dataset space."), + H5Sclose}; } -void writeResultAttributes( - const hid_t dataset, - const std::string& componentNames, - const std::string& componentUnits, - const std::string& coordinateSystem, - const std::string& location) { - writeStringAttribute(dataset, "component_names", componentNames); - writeStringAttribute( - dataset, "component_unit_dimensions", componentUnits); - writeStringAttribute(dataset, "coordinate_system", coordinateSystem); - writeStringAttribute(dataset, "location", location); - writeStringAttribute(dataset, "step_name", kStepName); - writeUint64Attribute(dataset, "frame_index", 0U); +void WriteResultAttributes(const hid_t dataset, + const std::string& component_names, + const std::string& component_units, + const std::string& coordinate_system, + const std::string& location) { + WriteStringAttribute(dataset, "component_names", component_names); + WriteStringAttribute(dataset, "component_unit_dimensions", component_units); + WriteStringAttribute(dataset, "coordinate_system", coordinate_system); + WriteStringAttribute(dataset, "location", location); + WriteStringAttribute(dataset, "step_name", kStepName); + WriteUint64Attribute(dataset, "frame_index", 0U); } -Hdf5Handle writeDoubleValues( - const hid_t file, - const std::string& path, - const std::vector& dimensions, - const double* values, - const std::size_t valueCount) { - auto space = createDatasetSpace(dimensions); - Hdf5Handle dataset{ - requireHdf5Id( - H5Dcreate2( - file, path.c_str(), H5T_IEEE_F64LE, space.get(), - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), - "Unable to create a floating-point HDF5 dataset."), - H5Dclose}; - if (valueCount != 0U) { - requireHdf5( - H5Dwrite( - dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values), - "Unable to write a floating-point HDF5 dataset."); - } - return dataset; +Hdf5Handle WriteDoubleValues(const hid_t file, const std::string& path, + const std::vector& dimensions, + const double* values, + const std::size_t value_count) { + auto space = CreateDatasetSpace(dimensions); + Hdf5Handle dataset{ + RequireHdf5Id(H5Dcreate2(file, path.c_str(), H5T_IEEE_F64LE, space.Get(), + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), + "Unable to create a floating-point HDF5 dataset."), + H5Dclose}; + if (value_count != 0U) { + RequireHdf5(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, + H5P_DEFAULT, values), + "Unable to write a floating-point HDF5 dataset."); + } + return dataset; } -void writeDoubleDataset( - const hid_t file, - const std::string& path, - const std::vector& dimensions, - const double* values, - const std::size_t valueCount, - const std::string& componentNames, - const std::string& componentUnits, - const std::string& coordinateSystem, - const std::string& location) { - auto dataset = writeDoubleValues( - file, path, dimensions, values, valueCount); - writeResultAttributes( - dataset.get(), componentNames, componentUnits, coordinateSystem, location); +void WriteDoubleDataset(const hid_t file, const std::string& path, + const std::vector& dimensions, + const double* values, const std::size_t value_count, + const std::string& component_names, + const std::string& component_units, + const std::string& coordinate_system, + const std::string& location) { + auto dataset = WriteDoubleValues(file, path, dimensions, values, value_count); + WriteResultAttributes(dataset.Get(), component_names, component_units, + coordinate_system, location); } -void writeModelDoubleDataset( - const hid_t file, - const std::string& path, - const std::vector& dimensions, - const double* values, - const std::size_t valueCount, - const std::string& componentNames, - const std::string& componentUnits, - const std::string& coordinateSystem, - const std::string& location) { - auto dataset = writeDoubleValues( - file, path, dimensions, values, valueCount); - writeStringAttribute(dataset.get(), "component_names", componentNames); - writeStringAttribute( - dataset.get(), "component_unit_dimensions", componentUnits); - writeStringAttribute(dataset.get(), "coordinate_system", coordinateSystem); - writeStringAttribute(dataset.get(), "location", location); +void WriteModelDoubleDataset(const hid_t file, const std::string& path, + const std::vector& dimensions, + const double* values, + const std::size_t value_count, + const std::string& component_names, + const std::string& component_units, + const std::string& coordinate_system, + const std::string& location) { + auto dataset = WriteDoubleValues(file, path, dimensions, values, value_count); + WriteStringAttribute(dataset.Get(), "component_names", component_names); + WriteStringAttribute(dataset.Get(), "component_unit_dimensions", + component_units); + WriteStringAttribute(dataset.Get(), "coordinate_system", coordinate_system); + WriteStringAttribute(dataset.Get(), "location", location); } -void writeUint8Dataset( - const hid_t file, - const std::string& path, - const std::vector& dimensions, - const std::uint8_t* values, - const std::size_t valueCount) { - auto space = createDatasetSpace(dimensions); - Hdf5Handle dataset{ - requireHdf5Id( - H5Dcreate2( - file, path.c_str(), H5T_STD_U8LE, space.get(), - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), - "Unable to create a uint8 HDF5 dataset."), - H5Dclose}; - if (valueCount != 0U) { - requireHdf5( - H5Dwrite( - dataset.get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values), - "Unable to write a uint8 HDF5 dataset."); - } - writeStringAttribute(dataset.get(), "component_names", "UX,UY,UZ,URX,URY,URZ"); - writeStringAttribute(dataset.get(), "value_meaning", "0=free,1=constrained"); +void WriteUint8Dataset(const hid_t file, const std::string& path, + const std::vector& dimensions, + const std::uint8_t* values, + const std::size_t value_count) { + auto space = CreateDatasetSpace(dimensions); + Hdf5Handle dataset{ + RequireHdf5Id(H5Dcreate2(file, path.c_str(), H5T_STD_U8LE, space.Get(), + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), + "Unable to create a uint8 HDF5 dataset."), + H5Dclose}; + if (value_count != 0U) { + RequireHdf5(H5Dwrite(dataset.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, + H5P_DEFAULT, values), + "Unable to write a uint8 HDF5 dataset."); + } + WriteStringAttribute(dataset.Get(), "component_names", + "UX,UY,UZ,URX,URY,URZ"); + WriteStringAttribute(dataset.Get(), "value_meaning", "0=free,1=constrained"); } struct NodeWriteRow { - std::uint64_t internalNodeId; - const char* instanceName; - const char* sourceLabel; - double coordinates[3]; + std::uint64_t internal_node_id; + const char* instance_name; + const char* source_label; + double coordinates[3]; }; struct ElementWriteRow { - std::uint64_t internalElementId; - const char* instanceName; - const char* sourceLabel; - std::uint64_t nodeInternalIds[2]; - double localAxes[9]; + std::uint64_t internal_element_id; + const char* instance_name; + const char* source_label; + std::uint64_t node_internal_ids[2]; + double local_axes[9]; }; struct ShellElementWriteRow { - std::uint64_t internalElementId; - const char* instanceName; - const char* sourceLabel; - const char* sourceElementType; - const char* internalFormulation; - std::uint64_t nodeInternalIds[4]; - std::uint64_t shellSectionInternalId; - std::uint64_t materialInternalId; + std::uint64_t internal_element_id; + const char* instance_name; + const char* source_label; + const char* source_element_type; + const char* internal_formulation; + std::uint64_t node_internal_ids[4]; + std::uint64_t shell_section_internal_id; + std::uint64_t material_internal_id; }; struct ShellMaterialWriteRow { - std::uint64_t internalMaterialId; - const char* name; - double youngsModulus; - double poissonRatio; + std::uint64_t internal_material_id; + const char* name; + double youngs_modulus; + double poisson_ratio; }; struct ShellSectionWriteRow { - std::uint64_t internalSectionId; - const char* sourceFile; - std::uint64_t sourceLine; - const char* sourceElset; - std::uint64_t materialInternalId; - double thickness; + std::uint64_t internal_section_id; + const char* source_file; + std::uint64_t source_line; + const char* source_elset; + std::uint64_t material_internal_id; + double thickness; }; struct StressWriteRow { - std::uint64_t internalElementId; - std::uint64_t gaussPointIndex; - std::uint64_t sectionPointIndex; - double x1; - double x2; - const char* source; - double s11; + std::uint64_t internal_element_id; + std::uint64_t gauss_point_index; + std::uint64_t section_point_index; + double x1; + double x2; + const char* source; + double s11; }; struct DiagnosticWriteRow { - const char* severity; - const char* code; - const char* file; - std::uint64_t line; - const char* keyword; - const char* entityIdentity; - const char* message; + const char* severity; + const char* code; + const char* file; + std::uint64_t line; + const char* keyword; + const char* entity_identity; + const char* message; }; -Hdf5Handle writeCompoundDataset( - const hid_t file, - const char* path, - const std::size_t rowCount, - const hid_t fileType, - const hid_t memoryType, - const void* rows) { - const std::vector dimensions = { - static_cast(rowCount)}; - auto space = createDatasetSpace(dimensions); +Hdf5Handle WriteCompoundDataset(const hid_t file, const char* path, + const std::size_t row_count, + const hid_t file_type, const hid_t memory_type, + const void* rows) { + const std::vector dimensions = {static_cast(row_count)}; + auto space = CreateDatasetSpace(dimensions); + Hdf5Handle dataset{ + RequireHdf5Id(H5Dcreate2(file, path, file_type, space.Get(), H5P_DEFAULT, + H5P_DEFAULT, H5P_DEFAULT), + "Unable to create a compound HDF5 dataset."), + H5Dclose}; + if (row_count != 0U) { + RequireHdf5(H5Dwrite(dataset.Get(), memory_type, H5S_ALL, H5S_ALL, + H5P_DEFAULT, rows), + "Unable to write a compound HDF5 dataset."); + } + return dataset; +} + +void WriteMetadata(const hid_t file, const Domain& domain) { + auto metadata = CreateGroup(file, "/metadata"); + WriteUint64Attribute(metadata.Get(), "schema_version", 0U); + WriteStringAttribute(metadata.Get(), "solver_version", + std::string{SolverVersion()}); + WriteStringAttribute(metadata.Get(), "source_input_identity", + SourceInputIdentity(domain)); + WriteStringAttribute(metadata.Get(), "unit_system_label", + "user-consistent-unspecified"); + if (IsShellDomain(domain)) { + WriteStringAttribute(metadata.Get(), "feature_id", + "linear-static-mitc4-shell"); + WriteStringAttribute( + metadata.Get(), "coordinate_convention", + "global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta"); + WriteStringAttribute(metadata.Get(), "internal_formulation", "FESA-MITC4"); + WriteStringAttribute(metadata.Get(), "integration_rule", + "2x2x2-gauss; mitc4-edge-midpoint-shear"); + } else { + WriteStringAttribute(metadata.Get(), "feature_id", + "linear-static-3d-euler-beam"); + WriteStringAttribute(metadata.Get(), "coordinate_convention", + "global-cartesian; beam-local=(t,n1,t-cross-n1)"); + WriteStringAttribute(metadata.Get(), "element_formulation", + "B33-3D-Euler-Bernoulli"); + } + WriteStringAttribute(metadata.Get(), "step_name", kStepName); + WriteUint64Attribute(metadata.Get(), "frame_index", 0U); +} + +void WriteNodes(const hid_t file, const Domain& domain) { + std::vector rows; + rows.reserve(domain.Nodes().size()); + for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) { + const Node& node = domain.Nodes()[index]; + rows.push_back( + {static_cast(index), + node.source_id.instance_name.c_str(), + node.source_id.source_label_text.c_str(), + {node.coordinates[0U], node.coordinates[1U], node.coordinates[2U]}}); + } + + auto string_type = MakeUtf8StringType(); + const hsize_t coordinate_dimensions[] = {3U}; + Hdf5Handle file_coordinates{ + RequireHdf5Id(H5Tarray_create2(H5T_IEEE_F64LE, 1, coordinate_dimensions), + "Unable to create the node coordinate file type."), + H5Tclose}; + Hdf5Handle memory_coordinates{ + RequireHdf5Id( + H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), + "Unable to create the node coordinate memory type."), + H5Tclose}; + Hdf5Handle file_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), + "Unable to create the node file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), + "Unable to create the node memory type."), + H5Tclose}; + RequireHdf5(H5Tinsert(file_type.Get(), "internal_node_id", + HOFFSET(NodeWriteRow, internal_node_id), H5T_STD_U64LE), + "Unable to define the node internal ID field."); + RequireHdf5( + H5Tinsert(file_type.Get(), "instance_name", + HOFFSET(NodeWriteRow, instance_name), string_type.Get()), + "Unable to define the node instance field."); + RequireHdf5(H5Tinsert(file_type.Get(), "source_label", + HOFFSET(NodeWriteRow, source_label), string_type.Get()), + "Unable to define the node source-label field."); + RequireHdf5( + H5Tinsert(file_type.Get(), "coordinates", + HOFFSET(NodeWriteRow, coordinates), file_coordinates.Get()), + "Unable to define the node coordinate field."); + RequireHdf5( + H5Tinsert(memory_type.Get(), "internal_node_id", + HOFFSET(NodeWriteRow, internal_node_id), H5T_NATIVE_UINT64), + "Unable to define the node internal ID memory field."); + RequireHdf5( + H5Tinsert(memory_type.Get(), "instance_name", + HOFFSET(NodeWriteRow, instance_name), string_type.Get()), + "Unable to define the node instance memory field."); + RequireHdf5(H5Tinsert(memory_type.Get(), "source_label", + HOFFSET(NodeWriteRow, source_label), string_type.Get()), + "Unable to define the node source-label memory field."); + RequireHdf5( + H5Tinsert(memory_type.Get(), "coordinates", + HOFFSET(NodeWriteRow, coordinates), memory_coordinates.Get()), + "Unable to define the node coordinate memory field."); + + auto dataset = + WriteCompoundDataset(file, "/model/nodes", rows.size(), file_type.Get(), + memory_type.Get(), rows.data()); + WriteStringAttribute(dataset.Get(), "coordinate_system", "global-cartesian"); + WriteStringAttribute(dataset.Get(), "units_label", "length"); +} + +void WriteBeamElements(const hid_t file, const Domain& domain, + const std::vector& axes) { + std::vector rows; + rows.reserve(domain.Elements().size()); + for (std::size_t index = 0U; index < domain.Elements().size(); ++index) { + const auto& element = domain.Elements()[index]; + ElementWriteRow row{static_cast(index), + element.source_id.instance_name.c_str(), + element.source_id.source_label_text.c_str(), + {static_cast(element.node_indices[0U]), + static_cast(element.node_indices[1U])}, + {}}; + std::copy(axes[index].begin(), axes[index].end(), row.local_axes); + rows.push_back(row); + } + + auto string_type = MakeUtf8StringType(); + const hsize_t node_dimensions[] = {2U}; + const hsize_t axis_dimensions[] = {3U, 3U}; + Hdf5Handle file_nodes{ + RequireHdf5Id(H5Tarray_create2(H5T_STD_U64LE, 1, node_dimensions), + "Unable to create the element connectivity file type."), + H5Tclose}; + Hdf5Handle memory_nodes{ + RequireHdf5Id(H5Tarray_create2(H5T_NATIVE_UINT64, 1, node_dimensions), + "Unable to create the element connectivity memory type."), + H5Tclose}; + Hdf5Handle file_axes{ + RequireHdf5Id(H5Tarray_create2(H5T_IEEE_F64LE, 2, axis_dimensions), + "Unable to create the local-axis file type."), + H5Tclose}; + Hdf5Handle memory_axes{ + RequireHdf5Id(H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axis_dimensions), + "Unable to create the local-axis memory type."), + H5Tclose}; + Hdf5Handle file_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), + "Unable to create the element file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), + "Unable to create the element memory type."), + H5Tclose}; + const auto insert_fields = [&](const hid_t type, const hid_t integer_type, + const hid_t node_type, const hid_t axes_type) { + RequireHdf5( + H5Tinsert(type, "internal_element_id", + HOFFSET(ElementWriteRow, internal_element_id), integer_type), + "Unable to define the element internal ID field."); + RequireHdf5( + H5Tinsert(type, "instance_name", + HOFFSET(ElementWriteRow, instance_name), string_type.Get()), + "Unable to define the element instance field."); + RequireHdf5( + H5Tinsert(type, "source_label", HOFFSET(ElementWriteRow, source_label), + string_type.Get()), + "Unable to define the element source-label field."); + RequireHdf5( + H5Tinsert(type, "node_internal_ids", + HOFFSET(ElementWriteRow, node_internal_ids), node_type), + "Unable to define the element connectivity field."); + RequireHdf5(H5Tinsert(type, "local_axes", + HOFFSET(ElementWriteRow, local_axes), axes_type), + "Unable to define the element local-axis field."); + }; + insert_fields(file_type.Get(), H5T_STD_U64LE, file_nodes.Get(), + file_axes.Get()); + insert_fields(memory_type.Get(), H5T_NATIVE_UINT64, memory_nodes.Get(), + memory_axes.Get()); + auto dataset = + WriteCompoundDataset(file, "/model/elements", rows.size(), + file_type.Get(), memory_type.Get(), rows.data()); + WriteStringAttribute(dataset.Get(), "formulation", "B33-3D-Euler-Bernoulli"); +} + +void WriteShellElements(const hid_t file, const Domain& domain) { + std::vector rows; + rows.reserve(domain.ShellElements().size()); + for (std::size_t index = 0U; index < domain.ShellElements().size(); ++index) { + const auto& element = domain.ShellElements()[index]; + rows.push_back({static_cast(index), + element.source_id.instance_name.c_str(), + element.source_id.source_label_text.c_str(), + ShellSourceTypeName(element.source_type), + kMitc4InternalFormulation.data(), + {static_cast(element.node_indices[0U]), + static_cast(element.node_indices[1U]), + static_cast(element.node_indices[2U]), + static_cast(element.node_indices[3U])}, + static_cast(element.section_index), + static_cast(element.material_index)}); + } + + auto string_type = MakeUtf8StringType(); + const hsize_t node_dimensions[] = {kShellNodeCount}; + Hdf5Handle file_nodes{ + RequireHdf5Id(H5Tarray_create2(H5T_STD_U64LE, 1, node_dimensions), + "Unable to create shell connectivity file type."), + H5Tclose}; + Hdf5Handle memory_nodes{ + RequireHdf5Id(H5Tarray_create2(H5T_NATIVE_UINT64, 1, node_dimensions), + "Unable to create shell connectivity memory type."), + H5Tclose}; + Hdf5Handle file_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ShellElementWriteRow)), + "Unable to create shell element file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ShellElementWriteRow)), + "Unable to create shell element memory type."), + H5Tclose}; + const auto insert_fields = [&](const hid_t type, const hid_t integer_type, + const hid_t node_type) { + RequireHdf5(H5Tinsert(type, "internal_element_id", + HOFFSET(ShellElementWriteRow, internal_element_id), + integer_type), + "Unable to define shell element ID field."); + RequireHdf5(H5Tinsert(type, "instance_name", + HOFFSET(ShellElementWriteRow, instance_name), + string_type.Get()), + "Unable to define shell element instance field."); + RequireHdf5(H5Tinsert(type, "source_label", + HOFFSET(ShellElementWriteRow, source_label), + string_type.Get()), + "Unable to define shell element source-label field."); + RequireHdf5(H5Tinsert(type, "source_element_type", + HOFFSET(ShellElementWriteRow, source_element_type), + string_type.Get()), + "Unable to define shell source-type field."); + RequireHdf5(H5Tinsert(type, "internal_formulation", + HOFFSET(ShellElementWriteRow, internal_formulation), + string_type.Get()), + "Unable to define shell formulation field."); + RequireHdf5( + H5Tinsert(type, "node_internal_ids", + HOFFSET(ShellElementWriteRow, node_internal_ids), node_type), + "Unable to define shell connectivity field."); + RequireHdf5( + H5Tinsert(type, "shell_section_internal_id", + HOFFSET(ShellElementWriteRow, shell_section_internal_id), + integer_type), + "Unable to define shell section ID field."); + RequireHdf5(H5Tinsert(type, "material_internal_id", + HOFFSET(ShellElementWriteRow, material_internal_id), + integer_type), + "Unable to define shell material ID field."); + }; + insert_fields(file_type.Get(), H5T_STD_U64LE, file_nodes.Get()); + insert_fields(memory_type.Get(), H5T_NATIVE_UINT64, memory_nodes.Get()); + auto dataset = + WriteCompoundDataset(file, "/model/elements", rows.size(), + file_type.Get(), memory_type.Get(), rows.data()); + WriteStringAttribute(dataset.Get(), "formulation", "FESA-MITC4"); +} + +void WriteShellMaterials(const hid_t file, const Domain& domain) { + std::vector rows; + rows.reserve(domain.Materials().size()); + for (std::size_t index = 0U; index < domain.Materials().size(); ++index) { + const auto& material = domain.Materials()[index]; + rows.push_back({static_cast(index), material.name.c_str(), + material.youngs_modulus, material.poisson_ratio}); + } + auto string_type = MakeUtf8StringType(); + Hdf5Handle file_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ShellMaterialWriteRow)), + "Unable to create shell material file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ShellMaterialWriteRow)), + "Unable to create shell material memory type."), + H5Tclose}; + const auto insert_fields = [&](const hid_t type, const hid_t integer_type, + const hid_t double_type) { + RequireHdf5(H5Tinsert(type, "internal_material_id", + HOFFSET(ShellMaterialWriteRow, internal_material_id), + integer_type), + "Unable to define shell material ID field."); + RequireHdf5(H5Tinsert(type, "name", HOFFSET(ShellMaterialWriteRow, name), + string_type.Get()), + "Unable to define shell material name field."); + RequireHdf5( + H5Tinsert(type, "E", HOFFSET(ShellMaterialWriteRow, youngs_modulus), + double_type), + "Unable to define shell material E field."); + RequireHdf5( + H5Tinsert(type, "nu", HOFFSET(ShellMaterialWriteRow, poisson_ratio), + double_type), + "Unable to define shell material nu field."); + }; + insert_fields(file_type.Get(), H5T_STD_U64LE, H5T_IEEE_F64LE); + insert_fields(memory_type.Get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); + auto dataset = + WriteCompoundDataset(file, "/model/shell/materials", rows.size(), + file_type.Get(), memory_type.Get(), rows.data()); + WriteStringAttribute(dataset.Get(), "component_unit_dimensions", + "force/length^2,1"); +} + +void WriteShellSections(const hid_t file, const Domain& domain) { + std::vector source_files; + source_files.reserve(domain.ShellSections().size()); + for (const auto& section : domain.ShellSections()) { + source_files.push_back(NormalizedPathString(section.location.file)); + } + std::vector rows; + rows.reserve(domain.ShellSections().size()); + for (std::size_t index = 0U; index < domain.ShellSections().size(); ++index) { + const auto& section = domain.ShellSections()[index]; + rows.push_back({static_cast(index), + source_files[index].c_str(), + static_cast(section.location.line), + section.name.c_str(), + static_cast(section.material_index), + section.thickness}); + } + auto string_type = MakeUtf8StringType(); + Hdf5Handle file_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ShellSectionWriteRow)), + "Unable to create shell section file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(ShellSectionWriteRow)), + "Unable to create shell section memory type."), + H5Tclose}; + const auto insert_fields = [&](const hid_t type, const hid_t integer_type, + const hid_t double_type) { + RequireHdf5(H5Tinsert(type, "internal_section_id", + HOFFSET(ShellSectionWriteRow, internal_section_id), + integer_type), + "Unable to define shell section ID field."); + RequireHdf5(H5Tinsert(type, "source_file", + HOFFSET(ShellSectionWriteRow, source_file), + string_type.Get()), + "Unable to define shell section source-file field."); + RequireHdf5( + H5Tinsert(type, "source_line", + HOFFSET(ShellSectionWriteRow, source_line), integer_type), + "Unable to define shell section source-line field."); + RequireHdf5(H5Tinsert(type, "source_elset", + HOFFSET(ShellSectionWriteRow, source_elset), + string_type.Get()), + "Unable to define shell section ELSET field."); + RequireHdf5(H5Tinsert(type, "material_internal_id", + HOFFSET(ShellSectionWriteRow, material_internal_id), + integer_type), + "Unable to define shell section material ID field."); + RequireHdf5( + H5Tinsert(type, "thickness", HOFFSET(ShellSectionWriteRow, thickness), + double_type), + "Unable to define shell section thickness field."); + }; + insert_fields(file_type.Get(), H5T_STD_U64LE, H5T_IEEE_F64LE); + insert_fields(memory_type.Get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); + auto dataset = + WriteCompoundDataset(file, "/model/shell/sections", rows.size(), + file_type.Get(), memory_type.Get(), rows.data()); + WriteStringAttribute(dataset.Get(), "thickness_unit_dimension", "length"); + WriteStringAttribute(dataset.Get(), "layering", "centered-single-layer"); +} + +void WriteShellModelData(const hid_t file, const Domain& domain, + const WriterModelData& model_data) { + std::vector directors; + directors.reserve(domain.Nodes().size() * 3U); + std::vector frames; + frames.reserve(domain.Nodes().size() * 9U); + for (const auto& frame : domain.ShellNodeInitialFrames()) { + directors.insert(directors.end(), frame.director.begin(), + frame.director.end()); + frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); + frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); + frames.insert(frames.end(), frame.director.begin(), frame.director.end()); + } + WriteModelDoubleDataset(file, "/model/shell/nodal_director", + {static_cast(domain.Nodes().size()), 3U}, + directors.data(), directors.size(), "D1,D2,D3", + "1,1,1", "global-cartesian", "nodal"); + WriteModelDoubleDataset(file, "/model/shell/nodal_frame", + {static_cast(domain.Nodes().size()), 3U, 3U}, + frames.data(), frames.size(), "X,Y,Z", "1,1,1", + "global-cartesian", "nodal-frame"); + { Hdf5Handle dataset{ - requireHdf5Id( - H5Dcreate2( - file, path, fileType, space.get(), H5P_DEFAULT, - H5P_DEFAULT, H5P_DEFAULT), - "Unable to create a compound HDF5 dataset."), + RequireHdf5Id(H5Dopen2(file, "/model/shell/nodal_frame", H5P_DEFAULT), + "Unable to reopen the nodal-frame dataset."), H5Dclose}; - if (rowCount != 0U) { - requireHdf5( - H5Dwrite( - dataset.get(), memoryType, H5S_ALL, H5S_ALL, - H5P_DEFAULT, rows), - "Unable to write a compound HDF5 dataset."); - } - return dataset; + WriteStringAttribute(dataset.Get(), "axis_names", "A,B,D"); + } + + WriteShellMaterials(file, domain); + WriteShellSections(file, domain); + const std::vector nodal_dimensions{ + static_cast(domain.Nodes().size()), kDofsPerNode}; + WriteUint8Dataset(file, "/model/nodal_constraint_mask", nodal_dimensions, + model_data.constraint_mask.data(), + model_data.constraint_mask.size()); + WriteModelDoubleDataset( + file, "/model/prescribed_displacement", nodal_dimensions, + model_data.prescribed_displacement.data(), + model_data.prescribed_displacement.size(), "UX,UY,UZ,URX,URY,URZ", + "length,length,length,radian,radian,radian", "global-cartesian", + "nodal-prescribed-value"); + + const double gauss = 1.0 / std::sqrt(3.0); + const std::array locations{-gauss, -gauss, gauss, -gauss, + gauss, gauss, -gauss, gauss}; + WriteModelDoubleDataset(file, "/model/shell/midsurface_locations", {4U, 2U}, + locations.data(), locations.size(), "XI,ETA", "1,1", + "shell-natural", "midsurface-location"); + const std::array section_positions{-1.0, 0.0, 1.0}; + WriteModelDoubleDataset(file, "/model/shell/section_positions", {3U}, + section_positions.data(), section_positions.size(), + "ZETA", "1", "shell-natural", "section-position"); + { + Hdf5Handle dataset{ + RequireHdf5Id( + H5Dopen2(file, "/model/shell/section_positions", H5P_DEFAULT), + "Unable to reopen shell section positions."), + H5Dclose}; + WriteStringAttribute(dataset.Get(), "position_names", "BOTTOM,MIDDLE,TOP"); + } } -void writeMetadata(const hid_t file, const Domain& domain) { - auto metadata = createGroup(file, "/metadata"); - writeUint64Attribute(metadata.get(), "schema_version", 0U); - writeStringAttribute( - metadata.get(), "solver_version", std::string{SolverVersion()}); - writeStringAttribute( - metadata.get(), "source_input_identity", sourceInputIdentity(domain)); - writeStringAttribute( - metadata.get(), "unit_system_label", "user-consistent-unspecified"); - if (isShellDomain(domain)) { - writeStringAttribute( - metadata.get(), "feature_id", "linear-static-mitc4-shell"); - writeStringAttribute( - metadata.get(), - "coordinate_convention", - "global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta"); - writeStringAttribute( - metadata.get(), "internal_formulation", "FESA-MITC4"); - writeStringAttribute( - metadata.get(), - "integration_rule", - "2x2x2-gauss; mitc4-edge-midpoint-shear"); +std::vector FlattenEndpointValues( + const std::vector& rows, const bool section_resultants) { + const std::size_t components = section_resultants ? kGeneralizedComponentCount + : kEndActionComponentCount; + std::vector values; + values.reserve(rows.size() * components); + for (const auto& row : rows) { + if (section_resultants) { + values.insert(values.end(), row.section_resultant.begin(), + row.section_resultant.end()); } else { - writeStringAttribute( - metadata.get(), "feature_id", "linear-static-3d-euler-beam"); - writeStringAttribute( - metadata.get(), - "coordinate_convention", - "global-cartesian; beam-local=(t,n1,t-cross-n1)"); - writeStringAttribute( - metadata.get(), "element_formulation", "B33-3D-Euler-Bernoulli"); + values.insert(values.end(), row.end_action.begin(), row.end_action.end()); } - writeStringAttribute(metadata.get(), "step_name", kStepName); - writeUint64Attribute(metadata.get(), "frame_index", 0U); + } + return values; } -void writeNodes(const hid_t file, const Domain& domain) { - std::vector rows; - rows.reserve(domain.Nodes().size()); - for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) { - const Node& node = domain.Nodes()[index]; - rows.push_back({ - static_cast(index), - node.source_id.instance_name.c_str(), - node.source_id.source_label_text.c_str(), - {node.coordinates[0U], node.coordinates[1U], node.coordinates[2U]}}); - } - - auto stringType = makeUtf8StringType(); - const hsize_t coordinateDimensions[] = {3U}; - Hdf5Handle fileCoordinates{ - requireHdf5Id( - H5Tarray_create2(H5T_IEEE_F64LE, 1, coordinateDimensions), - "Unable to create the node coordinate file type."), - H5Tclose}; - Hdf5Handle memoryCoordinates{ - requireHdf5Id( - H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), - "Unable to create the node coordinate memory type."), - H5Tclose}; - Hdf5Handle fileType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), - "Unable to create the node file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), - "Unable to create the node memory type."), - H5Tclose}; - requireHdf5( - H5Tinsert(fileType.get(), "internal_node_id", - HOFFSET(NodeWriteRow, internalNodeId), H5T_STD_U64LE), - "Unable to define the node internal ID field."); - requireHdf5( - H5Tinsert(fileType.get(), "instance_name", - HOFFSET(NodeWriteRow, instanceName), stringType.get()), - "Unable to define the node instance field."); - requireHdf5( - H5Tinsert(fileType.get(), "source_label", - HOFFSET(NodeWriteRow, sourceLabel), stringType.get()), - "Unable to define the node source-label field."); - requireHdf5( - H5Tinsert(fileType.get(), "coordinates", - HOFFSET(NodeWriteRow, coordinates), fileCoordinates.get()), - "Unable to define the node coordinate field."); - requireHdf5( - H5Tinsert(memoryType.get(), "internal_node_id", - HOFFSET(NodeWriteRow, internalNodeId), H5T_NATIVE_UINT64), - "Unable to define the node internal ID memory field."); - requireHdf5( - H5Tinsert(memoryType.get(), "instance_name", - HOFFSET(NodeWriteRow, instanceName), stringType.get()), - "Unable to define the node instance memory field."); - requireHdf5( - H5Tinsert(memoryType.get(), "source_label", - HOFFSET(NodeWriteRow, sourceLabel), stringType.get()), - "Unable to define the node source-label memory field."); - requireHdf5( - H5Tinsert(memoryType.get(), "coordinates", - HOFFSET(NodeWriteRow, coordinates), memoryCoordinates.get()), - "Unable to define the node coordinate memory field."); - - auto dataset = writeCompoundDataset( - file, "/model/nodes", rows.size(), fileType.get(), memoryType.get(), - rows.data()); - writeStringAttribute(dataset.get(), "coordinate_system", "global-cartesian"); - writeStringAttribute(dataset.get(), "units_label", "length"); +std::vector FlattenGaussValues(const std::vector& rows, + const bool resultants) { + std::vector values; + values.reserve(rows.size() * kGeneralizedComponentCount); + for (const auto& row : rows) { + const auto& row_values = + resultants ? row.generalized_resultant : row.generalized_strain; + values.insert(values.end(), row_values.begin(), row_values.end()); + } + return values; } -void writeBeamElements( - const hid_t file, const Domain& domain, const std::vector& axes) { - std::vector rows; - rows.reserve(domain.Elements().size()); - for (std::size_t index = 0U; index < domain.Elements().size(); ++index) { - const auto& element = domain.Elements()[index]; - ElementWriteRow row{ - static_cast(index), - element.source_id.instance_name.c_str(), - element.source_id.source_label_text.c_str(), - {static_cast(element.node_indices[0U]), - static_cast(element.node_indices[1U])}, - {}}; - std::copy(axes[index].begin(), axes[index].end(), row.localAxes); - rows.push_back(row); - } - - auto stringType = makeUtf8StringType(); - const hsize_t nodeDimensions[] = {2U}; - const hsize_t axisDimensions[] = {3U, 3U}; - Hdf5Handle fileNodes{ - requireHdf5Id( - H5Tarray_create2(H5T_STD_U64LE, 1, nodeDimensions), - "Unable to create the element connectivity file type."), - H5Tclose}; - Hdf5Handle memoryNodes{ - requireHdf5Id( - H5Tarray_create2(H5T_NATIVE_UINT64, 1, nodeDimensions), - "Unable to create the element connectivity memory type."), - H5Tclose}; - Hdf5Handle fileAxes{ - requireHdf5Id( - H5Tarray_create2(H5T_IEEE_F64LE, 2, axisDimensions), - "Unable to create the local-axis file type."), - H5Tclose}; - Hdf5Handle memoryAxes{ - requireHdf5Id( - H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axisDimensions), - "Unable to create the local-axis memory type."), - H5Tclose}; - Hdf5Handle fileType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), - "Unable to create the element file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), - "Unable to create the element memory type."), - H5Tclose}; - const auto insertFields = [&](const hid_t type, - const hid_t integerType, - const hid_t nodeType, - const hid_t axesType) { - requireHdf5( - H5Tinsert(type, "internal_element_id", - HOFFSET(ElementWriteRow, internalElementId), integerType), - "Unable to define the element internal ID field."); - requireHdf5( - H5Tinsert(type, "instance_name", - HOFFSET(ElementWriteRow, instanceName), stringType.get()), - "Unable to define the element instance field."); - requireHdf5( - H5Tinsert(type, "source_label", - HOFFSET(ElementWriteRow, sourceLabel), stringType.get()), - "Unable to define the element source-label field."); - requireHdf5( - H5Tinsert(type, "node_internal_ids", - HOFFSET(ElementWriteRow, nodeInternalIds), nodeType), - "Unable to define the element connectivity field."); - requireHdf5( - H5Tinsert(type, "local_axes", - HOFFSET(ElementWriteRow, localAxes), axesType), - "Unable to define the element local-axis field."); - }; - insertFields(fileType.get(), H5T_STD_U64LE, fileNodes.get(), fileAxes.get()); - insertFields( - memoryType.get(), H5T_NATIVE_UINT64, memoryNodes.get(), memoryAxes.get()); - auto dataset = writeCompoundDataset( - file, "/model/elements", rows.size(), fileType.get(), memoryType.get(), - rows.data()); - writeStringAttribute( - dataset.get(), "formulation", "B33-3D-Euler-Bernoulli"); +void WriteStress(const hid_t file, const AnalysisState& state) { + std::vector rows; + rows.reserve(state.StressResults().size()); + for (const auto& row : state.StressResults()) { + rows.push_back({static_cast(row.element), + static_cast(row.gauss_point), + static_cast(row.section_point), row.x1, + row.x2, row.source.c_str(), row.s11}); + } + auto string_type = MakeUtf8StringType(); + Hdf5Handle file_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(StressWriteRow)), + "Unable to create the stress file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(StressWriteRow)), + "Unable to create the stress memory type."), + H5Tclose}; + const auto insert_fields = [&](const hid_t type, const hid_t integer_type, + const hid_t double_type) { + RequireHdf5( + H5Tinsert(type, "internal_element_id", + HOFFSET(StressWriteRow, internal_element_id), integer_type), + "Unable to define the stress element field."); + RequireHdf5( + H5Tinsert(type, "gauss_point_index", + HOFFSET(StressWriteRow, gauss_point_index), integer_type), + "Unable to define the stress Gauss field."); + RequireHdf5( + H5Tinsert(type, "section_point_index", + HOFFSET(StressWriteRow, section_point_index), integer_type), + "Unable to define the stress section-point field."); + RequireHdf5(H5Tinsert(type, "x1", HOFFSET(StressWriteRow, x1), double_type), + "Unable to define the stress x1 field."); + RequireHdf5(H5Tinsert(type, "x2", HOFFSET(StressWriteRow, x2), double_type), + "Unable to define the stress x2 field."); + RequireHdf5(H5Tinsert(type, "source", HOFFSET(StressWriteRow, source), + string_type.Get()), + "Unable to define the stress source field."); + RequireHdf5( + H5Tinsert(type, "S11", HOFFSET(StressWriteRow, s11), double_type), + "Unable to define the stress S11 field."); + }; + insert_fields(file_type.Get(), H5T_STD_U64LE, H5T_IEEE_F64LE); + insert_fields(memory_type.Get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); + auto dataset = WriteCompoundDataset( + file, "/steps/Step-1/frames/0/element/stress_s11", rows.size(), + file_type.Get(), memory_type.Get(), rows.data()); + WriteResultAttributes(dataset.Get(), "S11", "force/length^2", "beam-local", + "section-point"); } -void writeShellElements(const hid_t file, const Domain& domain) { - std::vector rows; - rows.reserve(domain.ShellElements().size()); - for (std::size_t index = 0U; index < domain.ShellElements().size(); ++index) { - const auto& element = domain.ShellElements()[index]; - rows.push_back({ - static_cast(index), - element.source_id.instance_name.c_str(), - element.source_id.source_label_text.c_str(), - shellSourceTypeName(element.source_type), - kMitc4InternalFormulation.data(), - {static_cast(element.node_indices[0U]), - static_cast(element.node_indices[1U]), - static_cast(element.node_indices[2U]), - static_cast(element.node_indices[3U])}, - static_cast(element.section_index), - static_cast(element.material_index)}); - } - - auto stringType = makeUtf8StringType(); - const hsize_t nodeDimensions[] = {kShellNodeCount}; - Hdf5Handle fileNodes{ - requireHdf5Id( - H5Tarray_create2(H5T_STD_U64LE, 1, nodeDimensions), - "Unable to create shell connectivity file type."), - H5Tclose}; - Hdf5Handle memoryNodes{ - requireHdf5Id( - H5Tarray_create2(H5T_NATIVE_UINT64, 1, nodeDimensions), - "Unable to create shell connectivity memory type."), - H5Tclose}; - Hdf5Handle fileType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ShellElementWriteRow)), - "Unable to create shell element file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ShellElementWriteRow)), - "Unable to create shell element memory type."), - H5Tclose}; - const auto insertFields = [&](const hid_t type, - const hid_t integerType, - const hid_t nodeType) { - requireHdf5( - H5Tinsert(type, "internal_element_id", - HOFFSET(ShellElementWriteRow, internalElementId), integerType), - "Unable to define shell element ID field."); - requireHdf5( - H5Tinsert(type, "instance_name", - HOFFSET(ShellElementWriteRow, instanceName), stringType.get()), - "Unable to define shell element instance field."); - requireHdf5( - H5Tinsert(type, "source_label", - HOFFSET(ShellElementWriteRow, sourceLabel), stringType.get()), - "Unable to define shell element source-label field."); - requireHdf5( - H5Tinsert(type, "source_element_type", - HOFFSET(ShellElementWriteRow, sourceElementType), stringType.get()), - "Unable to define shell source-type field."); - requireHdf5( - H5Tinsert(type, "internal_formulation", - HOFFSET(ShellElementWriteRow, internalFormulation), stringType.get()), - "Unable to define shell formulation field."); - requireHdf5( - H5Tinsert(type, "node_internal_ids", - HOFFSET(ShellElementWriteRow, nodeInternalIds), nodeType), - "Unable to define shell connectivity field."); - requireHdf5( - H5Tinsert(type, "shell_section_internal_id", - HOFFSET(ShellElementWriteRow, shellSectionInternalId), integerType), - "Unable to define shell section ID field."); - requireHdf5( - H5Tinsert(type, "material_internal_id", - HOFFSET(ShellElementWriteRow, materialInternalId), integerType), - "Unable to define shell material ID field."); - }; - insertFields(fileType.get(), H5T_STD_U64LE, fileNodes.get()); - insertFields(memoryType.get(), H5T_NATIVE_UINT64, memoryNodes.get()); - auto dataset = writeCompoundDataset( - file, "/model/elements", rows.size(), fileType.get(), memoryType.get(), - rows.data()); - writeStringAttribute(dataset.get(), "formulation", "FESA-MITC4"); +void WriteShellResultIdentity(const hid_t file, const std::string& path, + const bool uses_section_positions) { + Hdf5Handle dataset{RequireHdf5Id(H5Dopen2(file, path.c_str(), H5P_DEFAULT), + "Unable to reopen a shell result dataset."), + H5Dclose}; + WriteStringAttribute(dataset.Get(), "source_element_type_dataset", + "/model/elements.source_element_type"); + WriteStringAttribute(dataset.Get(), "internal_formulation", "FESA-MITC4"); + WriteStringAttribute(dataset.Get(), "midsurface_location_dataset", + "/model/shell/midsurface_locations"); + WriteStringAttribute(dataset.Get(), "local_frame_dataset", + "/steps/Step-1/frames/0/element/shell/local_frame"); + if (uses_section_positions) { + WriteStringAttribute(dataset.Get(), "section_position_dataset", + "/model/shell/section_positions"); + } } -void writeShellMaterials(const hid_t file, const Domain& domain) { - std::vector rows; - rows.reserve(domain.Materials().size()); - for (std::size_t index = 0U; index < domain.Materials().size(); ++index) { - const auto& material = domain.Materials()[index]; - rows.push_back({ - static_cast(index), - material.name.c_str(), - material.youngs_modulus, - material.poisson_ratio}); +void WriteShellResultDatasets(const hid_t file, const Domain& domain, + const AnalysisState& state) { + std::vector local_frames; + std::vector generalized_strains; + std::vector section_resultants; + std::vector stresses; + local_frames.reserve(state.ShellResults().size() * 9U); + generalized_strains.reserve(state.ShellResults().size() * + kShellGeneralizedComponentCount); + section_resultants.reserve(state.ShellResults().size() * + kShellGeneralizedComponentCount); + stresses.reserve(state.ShellResults().size() * kShellSectionPositionCount * + kShellStressComponentCount); + for (const auto& row : state.ShellResults()) { + for (const auto& axis : row.local_frame) { + local_frames.insert(local_frames.end(), axis.begin(), axis.end()); } - auto stringType = makeUtf8StringType(); - Hdf5Handle fileType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ShellMaterialWriteRow)), - "Unable to create shell material file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ShellMaterialWriteRow)), - "Unable to create shell material memory type."), - H5Tclose}; - const auto insertFields = [&](const hid_t type, - const hid_t integerType, - const hid_t doubleType) { - requireHdf5( - H5Tinsert(type, "internal_material_id", - HOFFSET(ShellMaterialWriteRow, internalMaterialId), integerType), - "Unable to define shell material ID field."); - requireHdf5( - H5Tinsert(type, "name", - HOFFSET(ShellMaterialWriteRow, name), stringType.get()), - "Unable to define shell material name field."); - requireHdf5( - H5Tinsert(type, "E", HOFFSET(ShellMaterialWriteRow, youngsModulus), - doubleType), - "Unable to define shell material E field."); - requireHdf5( - H5Tinsert(type, "nu", HOFFSET(ShellMaterialWriteRow, poissonRatio), - doubleType), - "Unable to define shell material nu field."); - }; - insertFields(fileType.get(), H5T_STD_U64LE, H5T_IEEE_F64LE); - insertFields(memoryType.get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); - auto dataset = writeCompoundDataset( - file, "/model/shell/materials", rows.size(), fileType.get(), - memoryType.get(), rows.data()); - writeStringAttribute(dataset.get(), "component_unit_dimensions", "force/length^2,1"); + generalized_strains.insert(generalized_strains.end(), + row.generalized_strain.begin(), + row.generalized_strain.end()); + section_resultants.insert(section_resultants.end(), + row.section_resultant.begin(), + row.section_resultant.end()); + for (const auto& position : row.stress) { + stresses.insert(stresses.end(), position.components.begin(), + position.components.end()); + } + } + + const hsize_t element_count = + static_cast(domain.ShellElements().size()); + const std::string root = std::string{kStepRoot} + "/element/shell"; + const std::string frame_path = root + "/local_frame"; + WriteDoubleDataset(file, frame_path, {element_count, 4U, 3U, 3U}, + local_frames.data(), local_frames.size(), "X,Y,Z", "1,1,1", + "global-cartesian", "shell-local-frame"); + { + Hdf5Handle dataset{ + RequireHdf5Id(H5Dopen2(file, frame_path.c_str(), H5P_DEFAULT), + "Unable to reopen shell local-frame results."), + H5Dclose}; + WriteStringAttribute(dataset.Get(), "axis_names", "E1,E2,E3"); + WriteStringAttribute(dataset.Get(), "source_element_type_dataset", + "/model/elements.source_element_type"); + WriteStringAttribute(dataset.Get(), "internal_formulation", "FESA-MITC4"); + WriteStringAttribute(dataset.Get(), "midsurface_location_dataset", + "/model/shell/midsurface_locations"); + } + + const std::string strain_path = root + "/generalized_strain"; + WriteDoubleDataset( + file, strain_path, {element_count, 4U, 8U}, generalized_strains.data(), + generalized_strains.size(), "E11,E22,G12,K11,K22,K12,G13,G23", + "1,1,1,1/length,1/length,1/length,1,1", "shell-local", "midsurface"); + WriteShellResultIdentity(file, strain_path, false); + + const std::string resultant_path = root + "/section_resultant"; + WriteDoubleDataset(file, resultant_path, {element_count, 4U, 8U}, + section_resultants.data(), section_resultants.size(), + "N11,N22,N12,M11,M22,M12,Q13,Q23", + "force/length,force/length,force/" + "length,force,force,force,force/length,force/length", + "shell-local", "midsurface"); + WriteShellResultIdentity(file, resultant_path, false); + + const std::string stress_path = root + "/stress"; + WriteDoubleDataset(file, stress_path, {element_count, 4U, 3U, 3U}, + stresses.data(), stresses.size(), "S11,S22,S12", + "force/length^2,force/length^2,force/length^2", + "shell-local", "section-position"); + WriteShellResultIdentity(file, stress_path, true); + + const double energy = state.PhysicalStrainEnergy(); + WriteDoubleDataset(file, std::string{kStepRoot} + "/global/energy", {1U}, + &energy, 1U, "PHYSICAL_STRAIN_ENERGY", "force*length", + "global", "global"); + WriteDoubleDataset(file, std::string{kStepRoot} + "/global/equilibrium", {6U}, + state.Equilibrium().data(), state.Equilibrium().size(), + "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", + "force,force,force,force*length,force*length,force*length", + "global-cartesian", "global-origin"); + const std::string metrics_path = + std::string{kStepRoot} + "/global/verification_metrics"; + WriteDoubleDataset(file, metrics_path, {3U}, + state.VerificationMetrics().data(), + state.VerificationMetrics().size(), + "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED," + "MOMENT_BALANCE_NORMALIZED", + "1,1,1", "global", "verification"); + { + Hdf5Handle dataset{ + RequireHdf5Id(H5Dopen2(file, metrics_path.c_str(), H5P_DEFAULT), + "Unable to reopen shell verification metrics."), + H5Dclose}; + WriteStringAttribute( + dataset.Get(), "metric_definition_ids", + "free-residual-l2-over-max-free-force-l2,force-balance-l2-over-max-" + "force-sum,moment-balance-l2-over-max-moment-sum"); + WriteStringAttribute(dataset.Get(), "acceptance_thresholds", + "1e-10,1e-10,1e-10"); + } } -void writeShellSections(const hid_t file, const Domain& domain) { - std::vector sourceFiles; - sourceFiles.reserve(domain.ShellSections().size()); - for (const auto& section : domain.ShellSections()) { - sourceFiles.push_back(normalizedPathString(section.location.file)); - } - std::vector rows; - rows.reserve(domain.ShellSections().size()); - for (std::size_t index = 0U; index < domain.ShellSections().size(); ++index) { - const auto& section = domain.ShellSections()[index]; - rows.push_back({ - static_cast(index), - sourceFiles[index].c_str(), - static_cast(section.location.line), - section.name.c_str(), - static_cast(section.material_index), - section.thickness}); - } - auto stringType = makeUtf8StringType(); - Hdf5Handle fileType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ShellSectionWriteRow)), - "Unable to create shell section file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(ShellSectionWriteRow)), - "Unable to create shell section memory type."), - H5Tclose}; - const auto insertFields = [&](const hid_t type, - const hid_t integerType, - const hid_t doubleType) { - requireHdf5( - H5Tinsert(type, "internal_section_id", - HOFFSET(ShellSectionWriteRow, internalSectionId), integerType), - "Unable to define shell section ID field."); - requireHdf5( - H5Tinsert(type, "source_file", - HOFFSET(ShellSectionWriteRow, sourceFile), stringType.get()), - "Unable to define shell section source-file field."); - requireHdf5( - H5Tinsert(type, "source_line", - HOFFSET(ShellSectionWriteRow, sourceLine), integerType), - "Unable to define shell section source-line field."); - requireHdf5( - H5Tinsert(type, "source_elset", - HOFFSET(ShellSectionWriteRow, sourceElset), stringType.get()), - "Unable to define shell section ELSET field."); - requireHdf5( - H5Tinsert(type, "material_internal_id", - HOFFSET(ShellSectionWriteRow, materialInternalId), integerType), - "Unable to define shell section material ID field."); - requireHdf5( - H5Tinsert(type, "thickness", - HOFFSET(ShellSectionWriteRow, thickness), doubleType), - "Unable to define shell section thickness field."); - }; - insertFields(fileType.get(), H5T_STD_U64LE, H5T_IEEE_F64LE); - insertFields(memoryType.get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); - auto dataset = writeCompoundDataset( - file, "/model/shell/sections", rows.size(), fileType.get(), - memoryType.get(), rows.data()); - writeStringAttribute(dataset.get(), "thickness_unit_dimension", "length"); - writeStringAttribute(dataset.get(), "layering", "centered-single-layer"); +void WriteDiagnostics(const hid_t file, + const std::vector& input_diagnostics) { + std::vector diagnostics = input_diagnostics; + SortDiagnostics(diagnostics); + std::vector files; + files.reserve(diagnostics.size()); + for (const auto& diagnostic : diagnostics) { + files.push_back(NormalizedPathString(diagnostic.location.file)); + } + std::vector rows; + rows.reserve(diagnostics.size()); + for (std::size_t index = 0U; index < diagnostics.size(); ++index) { + const auto& diagnostic = diagnostics[index]; + rows.push_back( + {diagnostic.severity == Severity::kWarning ? "warning" : "error", + diagnostic.code.c_str(), files[index].c_str(), + static_cast(diagnostic.location.line), + diagnostic.keyword.c_str(), diagnostic.entity_identity.c_str(), + diagnostic.message.c_str()}); + } + + auto string_type = MakeUtf8StringType(); + Hdf5Handle file_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticWriteRow)), + "Unable to create the diagnostic file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticWriteRow)), + "Unable to create the diagnostic memory type."), + H5Tclose}; + const auto insert_fields = [&](const hid_t type, const hid_t integer_type) { + RequireHdf5( + H5Tinsert(type, "severity", HOFFSET(DiagnosticWriteRow, severity), + string_type.Get()), + "Unable to define diagnostic severity."); + RequireHdf5(H5Tinsert(type, "code", HOFFSET(DiagnosticWriteRow, code), + string_type.Get()), + "Unable to define diagnostic code."); + RequireHdf5(H5Tinsert(type, "file", HOFFSET(DiagnosticWriteRow, file), + string_type.Get()), + "Unable to define diagnostic file."); + RequireHdf5(H5Tinsert(type, "line", HOFFSET(DiagnosticWriteRow, line), + integer_type), + "Unable to define diagnostic line."); + RequireHdf5(H5Tinsert(type, "keyword", HOFFSET(DiagnosticWriteRow, keyword), + string_type.Get()), + "Unable to define diagnostic keyword."); + RequireHdf5(H5Tinsert(type, "entity_identity", + HOFFSET(DiagnosticWriteRow, entity_identity), + string_type.Get()), + "Unable to define diagnostic entity identity."); + RequireHdf5(H5Tinsert(type, "message", HOFFSET(DiagnosticWriteRow, message), + string_type.Get()), + "Unable to define diagnostic message."); + }; + insert_fields(file_type.Get(), H5T_STD_U64LE); + insert_fields(memory_type.Get(), H5T_NATIVE_UINT64); + (void)WriteCompoundDataset(file, "/diagnostics", rows.size(), file_type.Get(), + memory_type.Get(), rows.data()); } -void writeShellModelData( - const hid_t file, const Domain& domain, const WriterModelData& modelData) { +void WriteResultDatasets(const hid_t file, const Domain& domain, + const AnalysisState& state) { + const std::vector nodal_dimensions = { + static_cast(domain.Nodes().size()), kDofsPerNode}; + WriteDoubleDataset(file, std::string{kStepRoot} + "/nodal/displacement", + nodal_dimensions, state.Displacement().Data(), + state.Displacement().Size(), "UX,UY,UZ,URX,URY,URZ", + "length,length,length,radian,radian,radian", + "global-cartesian", "nodal"); + WriteDoubleDataset(file, std::string{kStepRoot} + "/nodal/reaction", + nodal_dimensions, state.Reaction().Data(), + state.Reaction().Size(), "RF1,RF2,RF3,RM1,RM2,RM3", + "force,force,force,force*length,force*length,force*length", + "global-cartesian", "nodal"); + + if (IsShellDomain(domain)) { + WriteShellResultDatasets(file, domain, state); + return; + } + + const std::vector endpoint_action_dimensions = { + static_cast(domain.Elements().size()), kEndpointCount, + kEndActionComponentCount}; + const std::vector generalized_dimensions = { + static_cast(domain.Elements().size()), kEndpointCount, + kGeneralizedComponentCount}; + const auto end_actions = + FlattenEndpointValues(state.EndpointResults(), false); + WriteDoubleDataset(file, std::string{kStepRoot} + "/element/end_force_local", + endpoint_action_dimensions, end_actions.data(), + end_actions.size(), "FX,FY,FZ,MX,MY,MZ", + "force,force,force,force*length,force*length,force*length", + "beam-local", "endpoint-outward-action"); + const auto section_resultants = + FlattenEndpointValues(state.EndpointResults(), true); + WriteDoubleDataset(file, + std::string{kStepRoot} + "/element/section_resultant", + generalized_dimensions, section_resultants.data(), + section_resultants.size(), "N,T,My,Mz", + "force,force*length,force*length,force*length", + "beam-local", "endpoint-positive-local-x-section-cut"); + const auto generalized_strains = + FlattenGaussValues(state.GaussResults(), false); + WriteDoubleDataset( + file, std::string{kStepRoot} + "/element/generalized_strain", + generalized_dimensions, generalized_strains.data(), + generalized_strains.size(), "epsilon0,kappa_x,kappa_y,kappa_z", + "1,1/length,1/length,1/length", "beam-local", "integration-point"); + const auto generalized_resultants = + FlattenGaussValues(state.GaussResults(), true); + WriteDoubleDataset(file, + std::string{kStepRoot} + "/element/generalized_resultant", + generalized_dimensions, generalized_resultants.data(), + generalized_resultants.size(), "N,T,My,Mz", + "force,force*length,force*length,force*length", + "beam-local", "integration-point"); + WriteStress(file, state); +} + +/// @brief Writes the complete candidate schema to a same-directory temporary. +/// @note The HDF5 file handle is closed before self-check and finalization. +void WriteFile(const std::filesystem::path& temporary_path, + const Domain& domain, const AnalysisState& state, + const std::vector& diagnostics, + const WriterModelData& model_data) { + Hdf5Handle file{ + RequireHdf5Id(H5Fcreate(temporary_path.string().c_str(), H5F_ACC_EXCL, + H5P_DEFAULT, H5P_DEFAULT), + "Unable to create the temporary HDF5 file."), + H5Fclose}; + WriteMetadata(file.Get(), domain); + (void)CreateGroup(file.Get(), "/model"); + (void)CreateGroup(file.Get(), "/steps/Step-1/frames/0/nodal"); + (void)CreateGroup(file.Get(), "/steps/Step-1/frames/0/element"); + if (IsShellDomain(domain)) { + (void)CreateGroup(file.Get(), "/model/shell"); + (void)CreateGroup(file.Get(), "/steps/Step-1/frames/0/element/shell"); + (void)CreateGroup(file.Get(), "/steps/Step-1/frames/0/global"); + } + WriteNodes(file.Get(), domain); + if (IsShellDomain(domain)) { + WriteShellElements(file.Get(), domain); + WriteShellModelData(file.Get(), domain, model_data); + } else { + WriteBeamElements(file.Get(), domain, model_data.beam_local_axes); + } + WriteResultDatasets(file.Get(), domain, state); + WriteDiagnostics(file.Get(), diagnostics); + RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL), + "Unable to flush the temporary HDF5 file."); + RequireHdf5(file.CloseChecked(), + "Unable to close the temporary HDF5 file after writing."); +} + +Hdf5Handle OpenDatasetForCheck(const hid_t file, const char* path) { + return Hdf5Handle{RequireHdf5Id(H5Dopen2(file, path, H5P_DEFAULT), + "A mandatory HDF5 dataset is missing."), + H5Dclose}; +} + +std::vector CheckedDimensions(const hid_t dataset) { + Hdf5Handle space{RequireHdf5Id(H5Dget_space(dataset), + "Unable to inspect an HDF5 dataspace."), + H5Sclose}; + const int rank = H5Sget_simple_extent_ndims(space.Get()); + if (rank < 0) { + throw Hdf5Failure{"Unable to inspect an HDF5 dataset rank."}; + } + std::vector dimensions(static_cast(rank)); + if (rank > 0) { + RequireHdf5( + H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr), + "Unable to inspect HDF5 dataset dimensions."); + } + return dimensions; +} + +void RequireStringAttribute(const hid_t object, const char* name, + const std::string& expected) { + Hdf5Handle attribute{ + RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), + "A mandatory HDF5 string attribute is missing."), + H5Aclose}; + Hdf5Handle type{RequireHdf5Id(H5Aget_type(attribute.Get()), + "Unable to inspect an HDF5 attribute type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_STRING || + H5Tis_variable_str(type.Get()) <= 0 || + H5Tget_cset(type.Get()) != H5T_CSET_UTF8) { + throw Hdf5Failure{"An HDF5 string attribute is not variable-length UTF-8."}; + } + char* raw = nullptr; + RequireHdf5(H5Aread(attribute.Get(), type.Get(), &raw), + "Unable to read an HDF5 string attribute."); + const std::string actual = raw == nullptr ? std::string{} : std::string{raw}; + if (raw != nullptr) { + RequireHdf5(H5free_memory(raw), "Unable to release an HDF5 string value."); + } + if (actual != expected) { + throw Hdf5Failure{"An HDF5 string attribute has the wrong value."}; + } +} + +void RequireUint64Attribute(const hid_t object, const char* name, + const std::uint64_t expected) { + Hdf5Handle attribute{ + RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), + "A mandatory HDF5 integer attribute is missing."), + H5Aclose}; + Hdf5Handle type{ + RequireHdf5Id(H5Aget_type(attribute.Get()), + "Unable to inspect an HDF5 integer attribute type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_INTEGER || + H5Tget_size(type.Get()) != sizeof(std::uint64_t) || + H5Tget_sign(type.Get()) != H5T_SGN_NONE || + H5Tequal(type.Get(), H5T_STD_U64LE) <= 0) { + throw Hdf5Failure{"An HDF5 integer attribute is not portable uint64."}; + } + std::uint64_t actual = 0U; + RequireHdf5(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &actual), + "Unable to read an HDF5 integer attribute."); + if (actual != expected) { + throw Hdf5Failure{"An HDF5 integer attribute has the wrong value."}; + } +} + +void RequirePortableCompoundMember(const hid_t compound_type, + const unsigned index, + const std::string& name) { + Hdf5Handle member_type{ + RequireHdf5Id(H5Tget_member_type(compound_type, index), + "Unable to inspect a compound HDF5 field type."), + H5Tclose}; + const bool is_uint64 = + name == "internal_node_id" || name == "internal_element_id" || + name == "internal_material_id" || name == "internal_section_id" || + name == "shell_section_internal_id" || name == "material_internal_id" || + name == "source_line" || name == "gauss_point_index" || + name == "section_point_index" || name == "line"; + const bool is_float64 = name == "x1" || name == "x2" || name == "S11" || + name == "E" || name == "nu" || name == "thickness"; + const bool is_string = + name == "instance_name" || name == "source_label" || + name == "source_element_type" || name == "internal_formulation" || + name == "name" || name == "source_file" || name == "source_elset" || + name == "source" || name == "severity" || name == "code" || + name == "file" || name == "keyword" || name == "entity_identity" || + name == "message"; + if (is_uint64) { + if (H5Tget_class(member_type.Get()) != H5T_INTEGER || + H5Tget_size(member_type.Get()) != sizeof(std::uint64_t) || + H5Tget_sign(member_type.Get()) != H5T_SGN_NONE || + H5Tequal(member_type.Get(), H5T_STD_U64LE) <= 0) { + throw Hdf5Failure{"A compound HDF5 field is not portable uint64."}; + } + return; + } + if (is_float64) { + if (H5Tget_class(member_type.Get()) != H5T_FLOAT || + H5Tget_size(member_type.Get()) != sizeof(double) || + H5Tequal(member_type.Get(), H5T_IEEE_F64LE) <= 0) { + throw Hdf5Failure{"A compound HDF5 field is not float64."}; + } + return; + } + if (is_string) { + if (H5Tget_class(member_type.Get()) != H5T_STRING || + H5Tis_variable_str(member_type.Get()) <= 0 || + H5Tget_cset(member_type.Get()) != H5T_CSET_UTF8) { + throw Hdf5Failure{"A compound HDF5 field is not UTF-8."}; + } + return; + } + + if (H5Tget_class(member_type.Get()) != H5T_ARRAY) { + throw Hdf5Failure{"A compound HDF5 array field has the wrong type."}; + } + const int rank = H5Tget_array_ndims(member_type.Get()); + if (rank <= 0) { + throw Hdf5Failure{"Unable to inspect a compound HDF5 array rank."}; + } + std::vector dimensions(static_cast(rank)); + RequireHdf5(H5Tget_array_dims2(member_type.Get(), dimensions.data()), + "Unable to inspect compound HDF5 array dimensions."); + Hdf5Handle base_type{ + RequireHdf5Id(H5Tget_super(member_type.Get()), + "Unable to inspect a compound HDF5 array base type."), + H5Tclose}; + if (name == "node_internal_ids") { + if ((dimensions != std::vector{2U} && + dimensions != std::vector{4U}) || + H5Tequal(base_type.Get(), H5T_STD_U64LE) <= 0) { + throw Hdf5Failure{"Element connectivity is not uint64[2] or uint64[4]."}; + } + } else if (name == "coordinates") { + if (dimensions != std::vector{3U} || + H5Tequal(base_type.Get(), H5T_IEEE_F64LE) <= 0) { + throw Hdf5Failure{"Node coordinates are not float64[3]."}; + } + } else if (name == "local_axes") { + if (dimensions != std::vector{3U, 3U} || + H5Tequal(base_type.Get(), H5T_IEEE_F64LE) <= 0) { + throw Hdf5Failure{"Element local axes are not float64[3,3]."}; + } + } else { + throw Hdf5Failure{"A compound HDF5 field has an unknown type contract."}; + } +} + +void RequireResultAttributes(const hid_t dataset, const char* component_names, + const char* component_units, + const char* coordinate_system, + const char* location) { + RequireStringAttribute(dataset, "component_names", component_names); + RequireStringAttribute(dataset, "component_unit_dimensions", component_units); + RequireStringAttribute(dataset, "coordinate_system", coordinate_system); + RequireStringAttribute(dataset, "location", location); + RequireStringAttribute(dataset, "step_name", kStepName); + RequireUint64Attribute(dataset, "frame_index", 0U); +} + +void RequireShellResultIdentity(const hid_t file, const char* path, + const bool uses_section_positions) { + auto dataset = OpenDatasetForCheck(file, path); + RequireStringAttribute(dataset.Get(), "source_element_type_dataset", + "/model/elements.source_element_type"); + RequireStringAttribute(dataset.Get(), "internal_formulation", "FESA-MITC4"); + RequireStringAttribute(dataset.Get(), "midsurface_location_dataset", + "/model/shell/midsurface_locations"); + RequireStringAttribute(dataset.Get(), "local_frame_dataset", + "/steps/Step-1/frames/0/element/shell/local_frame"); + if (uses_section_positions) { + RequireStringAttribute(dataset.Get(), "section_position_dataset", + "/model/shell/section_positions"); + } +} + +void RequireDoubleDataset(const hid_t file, const char* path, + const std::vector& expected_dimensions, + const char* component_names, + const char* component_units, + const char* coordinate_system, const char* location) { + auto dataset = OpenDatasetForCheck(file, path); + if (CheckedDimensions(dataset.Get()) != expected_dimensions) { + throw Hdf5Failure{"A floating-point HDF5 dataset has the wrong shape."}; + } + Hdf5Handle type{RequireHdf5Id(H5Dget_type(dataset.Get()), + "Unable to inspect an HDF5 dataset type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_FLOAT || + H5Tget_size(type.Get()) != sizeof(double) || + H5Tequal(type.Get(), H5T_IEEE_F64LE) <= 0) { + throw Hdf5Failure{ + "A result dataset is not IEEE-754 float64 little-endian."}; + } + RequireResultAttributes(dataset.Get(), component_names, component_units, + coordinate_system, location); + std::size_t value_count = 1U; + for (const hsize_t dimension : expected_dimensions) { + std::size_t next = 0U; + if (!SizeProductFits(value_count, static_cast(dimension), + next)) { + throw Hdf5Failure{"A result dataset shape overflows size_t."}; + } + value_count = next; + } + std::vector values(value_count); + if (!values.empty()) { + RequireHdf5(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, + H5P_DEFAULT, values.data()), + "Unable to read a result dataset during self-check."); + } + if (!std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); })) { + throw Hdf5Failure{"A result dataset contains a nonfinite value."}; + } +} + +void RequireModelDoubleDataset( + const hid_t file, const char* path, + const std::vector& expected_dimensions, + const char* component_names, const char* component_units, + const char* coordinate_system, const char* location, + const std::vector* expected_values = nullptr) { + auto dataset = OpenDatasetForCheck(file, path); + if (CheckedDimensions(dataset.Get()) != expected_dimensions) { + throw Hdf5Failure{"A model floating-point dataset has the wrong shape."}; + } + Hdf5Handle type{RequireHdf5Id(H5Dget_type(dataset.Get()), + "Unable to inspect a model dataset type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_FLOAT || + H5Tget_size(type.Get()) != sizeof(double) || + H5Tequal(type.Get(), H5T_IEEE_F64LE) <= 0) { + throw Hdf5Failure{"A model dataset is not IEEE-754 float64 little-endian."}; + } + RequireStringAttribute(dataset.Get(), "component_names", component_names); + RequireStringAttribute(dataset.Get(), "component_unit_dimensions", + component_units); + RequireStringAttribute(dataset.Get(), "coordinate_system", coordinate_system); + RequireStringAttribute(dataset.Get(), "location", location); + std::size_t value_count = 1U; + for (const hsize_t dimension : expected_dimensions) { + std::size_t next = 0U; + if (!SizeProductFits(value_count, static_cast(dimension), + next)) { + throw Hdf5Failure{"A model dataset shape overflows size_t."}; + } + value_count = next; + } + std::vector values(value_count); + if (!values.empty()) { + RequireHdf5(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, + H5P_DEFAULT, values.data()), + "Unable to read a model dataset during self-check."); + } + if (!std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); })) { + throw Hdf5Failure{"A model dataset contains a nonfinite value."}; + } + if (expected_values != nullptr && values != *expected_values) { + throw Hdf5Failure{"A fixed model dataset has the wrong value order."}; + } +} + +void RequireUint8Dataset(const hid_t file, const char* path, + const std::vector& expected_dimensions, + const std::vector& expected_values) { + auto dataset = OpenDatasetForCheck(file, path); + if (CheckedDimensions(dataset.Get()) != expected_dimensions) { + throw Hdf5Failure{"A uint8 HDF5 dataset has the wrong shape."}; + } + Hdf5Handle type{RequireHdf5Id(H5Dget_type(dataset.Get()), + "Unable to inspect a uint8 dataset type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_INTEGER || + H5Tget_size(type.Get()) != sizeof(std::uint8_t) || + H5Tget_sign(type.Get()) != H5T_SGN_NONE || + H5Tequal(type.Get(), H5T_STD_U8LE) <= 0) { + throw Hdf5Failure{"A constraint mask is not portable uint8."}; + } + std::vector values(expected_values.size()); + if (!values.empty()) { + RequireHdf5(H5Dread(dataset.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, + H5P_DEFAULT, values.data()), + "Unable to read the constraint mask during self-check."); + } + if (values != expected_values) { + throw Hdf5Failure{"The constraint mask has the wrong value order."}; + } + RequireStringAttribute(dataset.Get(), "component_names", + "UX,UY,UZ,URX,URY,URZ"); + RequireStringAttribute(dataset.Get(), "value_meaning", + "0=free,1=constrained"); +} + +void RequireCompoundDataset(const hid_t file, const char* path, + const hsize_t expected_rows, + const std::vector& expected_members) { + auto dataset = OpenDatasetForCheck(file, path); + if (CheckedDimensions(dataset.Get()) != std::vector{expected_rows}) { + throw Hdf5Failure{"A compound HDF5 dataset has the wrong shape."}; + } + Hdf5Handle type{RequireHdf5Id(H5Dget_type(dataset.Get()), + "Unable to inspect a compound type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_COMPOUND || + H5Tget_nmembers(type.Get()) != + static_cast(expected_members.size())) { + throw Hdf5Failure{"A compound HDF5 dataset has the wrong field count."}; + } + for (std::size_t index = 0U; index < expected_members.size(); ++index) { + char* raw_name = + H5Tget_member_name(type.Get(), static_cast(index)); + if (raw_name == nullptr) { + throw Hdf5Failure{"Unable to inspect a compound HDF5 field name."}; + } + const std::string actual_name{raw_name}; + RequireHdf5(H5free_memory(raw_name), + "Unable to release a compound field name."); + if (actual_name != expected_members[index]) { + throw Hdf5Failure{"A compound HDF5 field has the wrong identity."}; + } + RequirePortableCompoundMember(type.Get(), static_cast(index), + expected_members[index]); + } +} + +/// @brief Reopens the closed candidate and verifies its required external +/// schema. +void SelfCheckFile(const std::filesystem::path& path, const Domain& domain, + const AnalysisState& state, + const std::size_t diagnostic_count, + const WriterModelData& model_data) { + if (H5Fis_hdf5(path.string().c_str()) <= 0) { + throw Hdf5Failure{"The temporary output is not an HDF5 file."}; + } + Hdf5Handle file{ + RequireHdf5Id(H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), + "Unable to reopen the temporary HDF5 file read-only."), + H5Fclose}; + Hdf5Handle metadata{ + RequireHdf5Id(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), + "The HDF5 metadata group is missing."), + H5Gclose}; + RequireUint64Attribute(metadata.Get(), "schema_version", 0U); + RequireStringAttribute(metadata.Get(), "solver_version", + std::string{SolverVersion()}); + RequireStringAttribute(metadata.Get(), "source_input_identity", + SourceInputIdentity(domain)); + RequireStringAttribute(metadata.Get(), "unit_system_label", + "user-consistent-unspecified"); + if (IsShellDomain(domain)) { + RequireStringAttribute(metadata.Get(), "feature_id", + "linear-static-mitc4-shell"); + RequireStringAttribute( + metadata.Get(), "coordinate_convention", + "global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta"); + RequireStringAttribute(metadata.Get(), "internal_formulation", + "FESA-MITC4"); + RequireStringAttribute(metadata.Get(), "integration_rule", + "2x2x2-gauss; mitc4-edge-midpoint-shear"); + } else { + RequireStringAttribute(metadata.Get(), "feature_id", + "linear-static-3d-euler-beam"); + RequireStringAttribute(metadata.Get(), "coordinate_convention", + "global-cartesian; beam-local=(t,n1,t-cross-n1)"); + RequireStringAttribute(metadata.Get(), "element_formulation", + "B33-3D-Euler-Bernoulli"); + } + RequireStringAttribute(metadata.Get(), "step_name", kStepName); + RequireUint64Attribute(metadata.Get(), "frame_index", 0U); + + RequireCompoundDataset( + file.Get(), "/model/nodes", static_cast(domain.Nodes().size()), + {"internal_node_id", "instance_name", "source_label", "coordinates"}); + auto nodes = OpenDatasetForCheck(file.Get(), "/model/nodes"); + RequireStringAttribute(nodes.Get(), "coordinate_system", "global-cartesian"); + RequireStringAttribute(nodes.Get(), "units_label", "length"); + const std::vector nodal_dimensions = { + static_cast(domain.Nodes().size()), kDofsPerNode}; + RequireDoubleDataset(file.Get(), "/steps/Step-1/frames/0/nodal/displacement", + nodal_dimensions, "UX,UY,UZ,URX,URY,URZ", + "length,length,length,radian,radian,radian", + "global-cartesian", "nodal"); + RequireDoubleDataset( + file.Get(), "/steps/Step-1/frames/0/nodal/reaction", nodal_dimensions, + "RF1,RF2,RF3,RM1,RM2,RM3", + "force,force,force,force*length,force*length,force*length", + "global-cartesian", "nodal"); + + if (IsShellDomain(domain)) { + RequireCompoundDataset( + file.Get(), "/model/elements", + static_cast(domain.ShellElements().size()), + {"internal_element_id", "instance_name", "source_label", + "source_element_type", "internal_formulation", "node_internal_ids", + "shell_section_internal_id", "material_internal_id"}); + auto elements = OpenDatasetForCheck(file.Get(), "/model/elements"); + RequireStringAttribute(elements.Get(), "formulation", "FESA-MITC4"); + RequireCompoundDataset(file.Get(), "/model/shell/materials", + static_cast(domain.Materials().size()), + {"internal_material_id", "name", "E", "nu"}); + RequireCompoundDataset( + file.Get(), "/model/shell/sections", + static_cast(domain.ShellSections().size()), + {"internal_section_id", "source_file", "source_line", "source_elset", + "material_internal_id", "thickness"}); + std::vector directors; - directors.reserve(domain.Nodes().size() * 3U); std::vector frames; + directors.reserve(domain.Nodes().size() * 3U); frames.reserve(domain.Nodes().size() * 9U); for (const auto& frame : domain.ShellNodeInitialFrames()) { - directors.insert( - directors.end(), frame.director.begin(), frame.director.end()); - frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); - frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); - frames.insert(frames.end(), frame.director.begin(), frame.director.end()); + directors.insert(directors.end(), frame.director.begin(), + frame.director.end()); + frames.insert(frames.end(), frame.tangent_a.begin(), + frame.tangent_a.end()); + frames.insert(frames.end(), frame.tangent_b.begin(), + frame.tangent_b.end()); + frames.insert(frames.end(), frame.director.begin(), frame.director.end()); } - writeModelDoubleDataset( - file, "/model/shell/nodal_director", - {static_cast(domain.Nodes().size()), 3U}, - directors.data(), directors.size(), "D1,D2,D3", "1,1,1", - "global-cartesian", "nodal"); - writeModelDoubleDataset( - file, "/model/shell/nodal_frame", - {static_cast(domain.Nodes().size()), 3U, 3U}, - frames.data(), frames.size(), "X,Y,Z", "1,1,1", - "global-cartesian", "nodal-frame"); - { - Hdf5Handle dataset{ - requireHdf5Id( - H5Dopen2(file, "/model/shell/nodal_frame", H5P_DEFAULT), - "Unable to reopen the nodal-frame dataset."), - H5Dclose}; - writeStringAttribute(dataset.get(), "axis_names", "A,B,D"); - } - - writeShellMaterials(file, domain); - writeShellSections(file, domain); - const std::vector nodalDimensions{ - static_cast(domain.Nodes().size()), kDofsPerNode}; - writeUint8Dataset( - file, "/model/nodal_constraint_mask", nodalDimensions, - modelData.constraintMask.data(), modelData.constraintMask.size()); - writeModelDoubleDataset( - file, "/model/prescribed_displacement", nodalDimensions, - modelData.prescribedDisplacement.data(), - modelData.prescribedDisplacement.size(), - "UX,UY,UZ,URX,URY,URZ", - "length,length,length,radian,radian,radian", - "global-cartesian", "nodal-prescribed-value"); - + RequireModelDoubleDataset(file.Get(), "/model/shell/nodal_director", + {static_cast(domain.Nodes().size()), 3U}, + "D1,D2,D3", "1,1,1", "global-cartesian", "nodal", + &directors); + RequireModelDoubleDataset( + file.Get(), "/model/shell/nodal_frame", + {static_cast(domain.Nodes().size()), 3U, 3U}, "X,Y,Z", "1,1,1", + "global-cartesian", "nodal-frame", &frames); + auto nodal_frame = + OpenDatasetForCheck(file.Get(), "/model/shell/nodal_frame"); + RequireStringAttribute(nodal_frame.Get(), "axis_names", "A,B,D"); + RequireUint8Dataset(file.Get(), "/model/nodal_constraint_mask", + nodal_dimensions, model_data.constraint_mask); + RequireModelDoubleDataset(file.Get(), "/model/prescribed_displacement", + nodal_dimensions, "UX,UY,UZ,URX,URY,URZ", + "length,length,length,radian,radian,radian", + "global-cartesian", "nodal-prescribed-value", + &model_data.prescribed_displacement); const double gauss = 1.0 / std::sqrt(3.0); - const std::array locations{ - -gauss, -gauss, - gauss, -gauss, - gauss, gauss, - -gauss, gauss}; - writeModelDoubleDataset( - file, "/model/shell/midsurface_locations", {4U, 2U}, - locations.data(), locations.size(), "XI,ETA", "1,1", - "shell-natural", "midsurface-location"); - const std::array sectionPositions{-1.0, 0.0, 1.0}; - writeModelDoubleDataset( - file, "/model/shell/section_positions", {3U}, - sectionPositions.data(), sectionPositions.size(), "ZETA", "1", - "shell-natural", "section-position"); - { - Hdf5Handle dataset{ - requireHdf5Id( - H5Dopen2(file, "/model/shell/section_positions", H5P_DEFAULT), - "Unable to reopen shell section positions."), - H5Dclose}; - writeStringAttribute(dataset.get(), "position_names", "BOTTOM,MIDDLE,TOP"); - } -} + const std::vector locations{-gauss, -gauss, gauss, -gauss, + gauss, gauss, -gauss, gauss}; + RequireModelDoubleDataset(file.Get(), "/model/shell/midsurface_locations", + {4U, 2U}, "XI,ETA", "1,1", "shell-natural", + "midsurface-location", &locations); + const std::vector section_positions{-1.0, 0.0, 1.0}; + RequireModelDoubleDataset(file.Get(), "/model/shell/section_positions", + {3U}, "ZETA", "1", "shell-natural", + "section-position", §ion_positions); + auto positions = + OpenDatasetForCheck(file.Get(), "/model/shell/section_positions"); + RequireStringAttribute(positions.Get(), "position_names", + "BOTTOM,MIDDLE,TOP"); -std::vector flattenEndpointValues( - const std::vector& rows, - const bool sectionResultants) { - const std::size_t components = - sectionResultants ? kGeneralizedComponentCount : kEndActionComponentCount; - std::vector values; - values.reserve(rows.size() * components); - for (const auto& row : rows) { - if (sectionResultants) { - values.insert( - values.end(), row.section_resultant.begin(), row.section_resultant.end()); - } else { - values.insert(values.end(), row.end_action.begin(), row.end_action.end()); - } - } - return values; -} - -std::vector flattenGaussValues( - const std::vector& rows, - const bool resultants) { - std::vector values; - values.reserve(rows.size() * kGeneralizedComponentCount); - for (const auto& row : rows) { - const auto& rowValues = - resultants ? row.generalized_resultant : row.generalized_strain; - values.insert(values.end(), rowValues.begin(), rowValues.end()); - } - return values; -} - -void writeStress(const hid_t file, const AnalysisState& state) { - std::vector rows; - rows.reserve(state.StressResults().size()); - for (const auto& row : state.StressResults()) { - rows.push_back({ - static_cast(row.element), - static_cast(row.gauss_point), - static_cast(row.section_point), - row.x1, - row.x2, - row.source.c_str(), - row.s11}); - } - auto stringType = makeUtf8StringType(); - Hdf5Handle fileType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(StressWriteRow)), - "Unable to create the stress file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(StressWriteRow)), - "Unable to create the stress memory type."), - H5Tclose}; - const auto insertFields = [&](const hid_t type, - const hid_t integerType, - const hid_t doubleType) { - requireHdf5( - H5Tinsert(type, "internal_element_id", - HOFFSET(StressWriteRow, internalElementId), integerType), - "Unable to define the stress element field."); - requireHdf5( - H5Tinsert(type, "gauss_point_index", - HOFFSET(StressWriteRow, gaussPointIndex), integerType), - "Unable to define the stress Gauss field."); - requireHdf5( - H5Tinsert(type, "section_point_index", - HOFFSET(StressWriteRow, sectionPointIndex), integerType), - "Unable to define the stress section-point field."); - requireHdf5( - H5Tinsert(type, "x1", HOFFSET(StressWriteRow, x1), doubleType), - "Unable to define the stress x1 field."); - requireHdf5( - H5Tinsert(type, "x2", HOFFSET(StressWriteRow, x2), doubleType), - "Unable to define the stress x2 field."); - requireHdf5( - H5Tinsert(type, "source", HOFFSET(StressWriteRow, source), stringType.get()), - "Unable to define the stress source field."); - requireHdf5( - H5Tinsert(type, "S11", HOFFSET(StressWriteRow, s11), doubleType), - "Unable to define the stress S11 field."); - }; - insertFields(fileType.get(), H5T_STD_U64LE, H5T_IEEE_F64LE); - insertFields(memoryType.get(), H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE); - auto dataset = writeCompoundDataset( - file, - "/steps/Step-1/frames/0/element/stress_s11", - rows.size(), - fileType.get(), - memoryType.get(), - rows.data()); - writeResultAttributes( - dataset.get(), "S11", "force/length^2", "beam-local", "section-point"); -} - -void writeShellResultIdentity( - const hid_t file, - const std::string& path, - const bool usesSectionPositions) { - Hdf5Handle dataset{ - requireHdf5Id( - H5Dopen2(file, path.c_str(), H5P_DEFAULT), - "Unable to reopen a shell result dataset."), - H5Dclose}; - writeStringAttribute( - dataset.get(), - "source_element_type_dataset", - "/model/elements.source_element_type"); - writeStringAttribute(dataset.get(), "internal_formulation", "FESA-MITC4"); - writeStringAttribute( - dataset.get(), - "midsurface_location_dataset", - "/model/shell/midsurface_locations"); - writeStringAttribute( - dataset.get(), - "local_frame_dataset", - "/steps/Step-1/frames/0/element/shell/local_frame"); - if (usesSectionPositions) { - writeStringAttribute( - dataset.get(), - "section_position_dataset", - "/model/shell/section_positions"); - } -} - -void writeShellResultDatasets( - const hid_t file, const Domain& domain, const AnalysisState& state) { - std::vector localFrames; - std::vector generalizedStrains; - std::vector sectionResultants; - std::vector stresses; - localFrames.reserve(state.ShellResults().size() * 9U); - generalizedStrains.reserve( - state.ShellResults().size() * kShellGeneralizedComponentCount); - sectionResultants.reserve( - state.ShellResults().size() * kShellGeneralizedComponentCount); - stresses.reserve( - state.ShellResults().size() * kShellSectionPositionCount * - kShellStressComponentCount); - for (const auto& row : state.ShellResults()) { - for (const auto& axis : row.local_frame) { - localFrames.insert(localFrames.end(), axis.begin(), axis.end()); - } - generalizedStrains.insert( - generalizedStrains.end(), - row.generalized_strain.begin(), - row.generalized_strain.end()); - sectionResultants.insert( - sectionResultants.end(), - row.section_resultant.begin(), - row.section_resultant.end()); - for (const auto& position : row.stress) { - stresses.insert( - stresses.end(), - position.components.begin(), - position.components.end()); - } - } - - const hsize_t elementCount = + const hsize_t element_count = static_cast(domain.ShellElements().size()); - const std::string root = std::string{kStepRoot} + "/element/shell"; - const std::string framePath = root + "/local_frame"; - writeDoubleDataset( - file, framePath, {elementCount, 4U, 3U, 3U}, - localFrames.data(), localFrames.size(), "X,Y,Z", "1,1,1", - "global-cartesian", "shell-local-frame"); - { - Hdf5Handle dataset{ - requireHdf5Id( - H5Dopen2(file, framePath.c_str(), H5P_DEFAULT), - "Unable to reopen shell local-frame results."), - H5Dclose}; - writeStringAttribute(dataset.get(), "axis_names", "E1,E2,E3"); - writeStringAttribute( - dataset.get(), - "source_element_type_dataset", - "/model/elements.source_element_type"); - writeStringAttribute(dataset.get(), "internal_formulation", "FESA-MITC4"); - writeStringAttribute( - dataset.get(), - "midsurface_location_dataset", - "/model/shell/midsurface_locations"); - } - - const std::string strainPath = root + "/generalized_strain"; - writeDoubleDataset( - file, strainPath, {elementCount, 4U, 8U}, - generalizedStrains.data(), generalizedStrains.size(), - "E11,E22,G12,K11,K22,K12,G13,G23", - "1,1,1,1/length,1/length,1/length,1,1", + const std::string shell_root = std::string{kStepRoot} + "/element/shell"; + RequireDoubleDataset(file.Get(), (shell_root + "/local_frame").c_str(), + {element_count, 4U, 3U, 3U}, "X,Y,Z", "1,1,1", + "global-cartesian", "shell-local-frame"); + auto local_frame = + OpenDatasetForCheck(file.Get(), (shell_root + "/local_frame").c_str()); + RequireStringAttribute(local_frame.Get(), "axis_names", "E1,E2,E3"); + RequireStringAttribute(local_frame.Get(), "internal_formulation", + "FESA-MITC4"); + RequireStringAttribute(local_frame.Get(), "source_element_type_dataset", + "/model/elements.source_element_type"); + RequireStringAttribute(local_frame.Get(), "midsurface_location_dataset", + "/model/shell/midsurface_locations"); + RequireDoubleDataset( + file.Get(), (shell_root + "/generalized_strain").c_str(), + {element_count, 4U, 8U}, "E11,E22,G12,K11,K22,K12,G13,G23", + "1,1,1,1/length,1/length,1/length,1,1", "shell-local", "midsurface"); + RequireShellResultIdentity( + file.Get(), (shell_root + "/generalized_strain").c_str(), false); + RequireDoubleDataset( + file.Get(), (shell_root + "/section_resultant").c_str(), + {element_count, 4U, 8U}, "N11,N22,N12,M11,M22,M12,Q13,Q23", + "force/length,force/length,force/length,force,force,force,force/" + "length,force/length", "shell-local", "midsurface"); - writeShellResultIdentity(file, strainPath, false); - - const std::string resultantPath = root + "/section_resultant"; - writeDoubleDataset( - file, resultantPath, {elementCount, 4U, 8U}, - sectionResultants.data(), sectionResultants.size(), - "N11,N22,N12,M11,M22,M12,Q13,Q23", - "force/length,force/length,force/length,force,force,force,force/length,force/length", - "shell-local", "midsurface"); - writeShellResultIdentity(file, resultantPath, false); - - const std::string stressPath = root + "/stress"; - writeDoubleDataset( - file, stressPath, {elementCount, 4U, 3U, 3U}, - stresses.data(), stresses.size(), "S11,S22,S12", - "force/length^2,force/length^2,force/length^2", - "shell-local", "section-position"); - writeShellResultIdentity(file, stressPath, true); - - const double energy = state.PhysicalStrainEnergy(); - writeDoubleDataset( - file, std::string{kStepRoot} + "/global/energy", {1U}, - &energy, 1U, "PHYSICAL_STRAIN_ENERGY", "force*length", - "global", "global"); - writeDoubleDataset( - file, std::string{kStepRoot} + "/global/equilibrium", {6U}, - state.Equilibrium().data(), state.Equilibrium().size(), + RequireShellResultIdentity( + file.Get(), (shell_root + "/section_resultant").c_str(), false); + RequireDoubleDataset(file.Get(), (shell_root + "/stress").c_str(), + {element_count, 4U, 3U, 3U}, "S11,S22,S12", + "force/length^2,force/length^2,force/length^2", + "shell-local", "section-position"); + RequireShellResultIdentity(file.Get(), (shell_root + "/stress").c_str(), + true); + RequireDoubleDataset(file.Get(), "/steps/Step-1/frames/0/global/energy", + {1U}, "PHYSICAL_STRAIN_ENERGY", "force*length", + "global", "global"); + RequireDoubleDataset( + file.Get(), "/steps/Step-1/frames/0/global/equilibrium", {6U}, "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "global-origin"); - const std::string metricsPath = - std::string{kStepRoot} + "/global/verification_metrics"; - writeDoubleDataset( - file, metricsPath, {3U}, state.VerificationMetrics().data(), - state.VerificationMetrics().size(), - "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_NORMALIZED", + RequireDoubleDataset( + file.Get(), "/steps/Step-1/frames/0/global/verification_metrics", {3U}, + "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_" + "NORMALIZED", "1,1,1", "global", "verification"); - { - Hdf5Handle dataset{ - requireHdf5Id( - H5Dopen2(file, metricsPath.c_str(), H5P_DEFAULT), - "Unable to reopen shell verification metrics."), - H5Dclose}; - writeStringAttribute( - dataset.get(), - "metric_definition_ids", - "free-residual-l2-over-max-free-force-l2,force-balance-l2-over-max-force-sum,moment-balance-l2-over-max-moment-sum"); - writeStringAttribute( - dataset.get(), "acceptance_thresholds", "1e-10,1e-10,1e-10"); + auto metrics = OpenDatasetForCheck( + file.Get(), "/steps/Step-1/frames/0/global/verification_metrics"); + RequireStringAttribute( + metrics.Get(), "metric_definition_ids", + "free-residual-l2-over-max-free-force-l2,force-balance-l2-over-max-" + "force-sum,moment-balance-l2-over-max-moment-sum"); + RequireStringAttribute(metrics.Get(), "acceptance_thresholds", + "1e-10,1e-10,1e-10"); + for (const char* forbidden : + {"/steps/Step-1/frames/0/element/shell/drilling", + "/steps/Step-1/frames/0/element/shell/drilling_energy", + "/steps/Step-1/frames/0/element/shell/S33", + "/steps/Step-1/frames/0/element/shell/S13", + "/steps/Step-1/frames/0/element/shell/S23"}) { + if (H5Lexists(file.Get(), forbidden, H5P_DEFAULT) != 0) { + throw Hdf5Failure{"A forbidden shell result path exists."}; + } } -} - -void writeDiagnostics( - const hid_t file, const std::vector& inputDiagnostics) { - std::vector diagnostics = inputDiagnostics; - SortDiagnostics(diagnostics); - std::vector files; - files.reserve(diagnostics.size()); - for (const auto& diagnostic : diagnostics) { - files.push_back(normalizedPathString(diagnostic.location.file)); - } - std::vector rows; - rows.reserve(diagnostics.size()); - for (std::size_t index = 0U; index < diagnostics.size(); ++index) { - const auto& diagnostic = diagnostics[index]; - rows.push_back({ - diagnostic.severity == Severity::kWarning ? "warning" : "error", - diagnostic.code.c_str(), - files[index].c_str(), - static_cast(diagnostic.location.line), - diagnostic.keyword.c_str(), - diagnostic.entity_identity.c_str(), - diagnostic.message.c_str()}); - } - - auto stringType = makeUtf8StringType(); - Hdf5Handle fileType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticWriteRow)), - "Unable to create the diagnostic file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireHdf5Id( - H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticWriteRow)), - "Unable to create the diagnostic memory type."), - H5Tclose}; - const auto insertFields = [&](const hid_t type, const hid_t integerType) { - requireHdf5( - H5Tinsert(type, "severity", HOFFSET(DiagnosticWriteRow, severity), - stringType.get()), - "Unable to define diagnostic severity."); - requireHdf5( - H5Tinsert(type, "code", HOFFSET(DiagnosticWriteRow, code), - stringType.get()), - "Unable to define diagnostic code."); - requireHdf5( - H5Tinsert(type, "file", HOFFSET(DiagnosticWriteRow, file), - stringType.get()), - "Unable to define diagnostic file."); - requireHdf5( - H5Tinsert(type, "line", HOFFSET(DiagnosticWriteRow, line), integerType), - "Unable to define diagnostic line."); - requireHdf5( - H5Tinsert(type, "keyword", HOFFSET(DiagnosticWriteRow, keyword), - stringType.get()), - "Unable to define diagnostic keyword."); - requireHdf5( - H5Tinsert(type, "entity_identity", - HOFFSET(DiagnosticWriteRow, entityIdentity), stringType.get()), - "Unable to define diagnostic entity identity."); - requireHdf5( - H5Tinsert(type, "message", HOFFSET(DiagnosticWriteRow, message), - stringType.get()), - "Unable to define diagnostic message."); - }; - insertFields(fileType.get(), H5T_STD_U64LE); - insertFields(memoryType.get(), H5T_NATIVE_UINT64); - (void)writeCompoundDataset( - file, "/diagnostics", rows.size(), fileType.get(), memoryType.get(), - rows.data()); -} - -void writeResultDatasets( - const hid_t file, const Domain& domain, const AnalysisState& state) { - const std::vector nodalDimensions = { - static_cast(domain.Nodes().size()), kDofsPerNode}; - writeDoubleDataset( - file, - std::string{kStepRoot} + "/nodal/displacement", - nodalDimensions, - state.Displacement().Data(), - state.Displacement().Size(), - "UX,UY,UZ,URX,URY,URZ", - "length,length,length,radian,radian,radian", - "global-cartesian", - "nodal"); - writeDoubleDataset( - file, - std::string{kStepRoot} + "/nodal/reaction", - nodalDimensions, - state.Reaction().Data(), - state.Reaction().Size(), - "RF1,RF2,RF3,RM1,RM2,RM3", - "force,force,force,force*length,force*length,force*length", - "global-cartesian", - "nodal"); - - if (isShellDomain(domain)) { - writeShellResultDatasets(file, domain, state); - return; - } - - const std::vector endpointActionDimensions = { - static_cast(domain.Elements().size()), - kEndpointCount, + } else { + RequireCompoundDataset(file.Get(), "/model/elements", + static_cast(domain.Elements().size()), + {"internal_element_id", "instance_name", + "source_label", "node_internal_ids", "local_axes"}); + auto elements = OpenDatasetForCheck(file.Get(), "/model/elements"); + RequireStringAttribute(elements.Get(), "formulation", + "B33-3D-Euler-Bernoulli"); + const std::vector end_dimensions = { + static_cast(domain.Elements().size()), kEndpointCount, kEndActionComponentCount}; - const std::vector generalizedDimensions = { - static_cast(domain.Elements().size()), - kEndpointCount, + const std::vector generalized_dimensions = { + static_cast(domain.Elements().size()), kGaussPointCount, kGeneralizedComponentCount}; - const auto endActions = flattenEndpointValues(state.EndpointResults(), false); - writeDoubleDataset( - file, - std::string{kStepRoot} + "/element/end_force_local", - endpointActionDimensions, - endActions.data(), - endActions.size(), - "FX,FY,FZ,MX,MY,MZ", + RequireDoubleDataset( + file.Get(), "/steps/Step-1/frames/0/element/end_force_local", + end_dimensions, "FX,FY,FZ,MX,MY,MZ", "force,force,force,force*length,force*length,force*length", - "beam-local", - "endpoint-outward-action"); - const auto sectionResultants = - flattenEndpointValues(state.EndpointResults(), true); - writeDoubleDataset( - file, - std::string{kStepRoot} + "/element/section_resultant", - generalizedDimensions, - sectionResultants.data(), - sectionResultants.size(), - "N,T,My,Mz", - "force,force*length,force*length,force*length", - "beam-local", - "endpoint-positive-local-x-section-cut"); - const auto generalizedStrains = - flattenGaussValues(state.GaussResults(), false); - writeDoubleDataset( - file, - std::string{kStepRoot} + "/element/generalized_strain", - generalizedDimensions, - generalizedStrains.data(), - generalizedStrains.size(), - "epsilon0,kappa_x,kappa_y,kappa_z", - "1,1/length,1/length,1/length", - "beam-local", - "integration-point"); - const auto generalizedResultants = - flattenGaussValues(state.GaussResults(), true); - writeDoubleDataset( - file, - std::string{kStepRoot} + "/element/generalized_resultant", - generalizedDimensions, - generalizedResultants.data(), - generalizedResultants.size(), - "N,T,My,Mz", - "force,force*length,force*length,force*length", - "beam-local", - "integration-point"); - writeStress(file, state); + "beam-local", "endpoint-outward-action"); + RequireDoubleDataset(file.Get(), + "/steps/Step-1/frames/0/element/section_resultant", + generalized_dimensions, "N,T,My,Mz", + "force,force*length,force*length,force*length", + "beam-local", "endpoint-positive-local-x-section-cut"); + RequireDoubleDataset( + file.Get(), "/steps/Step-1/frames/0/element/generalized_strain", + generalized_dimensions, "epsilon0,kappa_x,kappa_y,kappa_z", + "1,1/length,1/length,1/length", "beam-local", "integration-point"); + RequireDoubleDataset(file.Get(), + "/steps/Step-1/frames/0/element/generalized_resultant", + generalized_dimensions, "N,T,My,Mz", + "force,force*length,force*length,force*length", + "beam-local", "integration-point"); + RequireCompoundDataset( + file.Get(), "/steps/Step-1/frames/0/element/stress_s11", + static_cast(state.StressResults().size()), + {"internal_element_id", "gauss_point_index", "section_point_index", + "x1", "x2", "source", "S11"}); + auto stress = OpenDatasetForCheck( + file.Get(), "/steps/Step-1/frames/0/element/stress_s11"); + RequireResultAttributes(stress.Get(), "S11", "force/length^2", "beam-local", + "section-point"); + } + RequireCompoundDataset(file.Get(), "/diagnostics", + static_cast(diagnostic_count), + {"severity", "code", "file", "line", "keyword", + "entity_identity", "message"}); + RequireHdf5(file.CloseChecked(), + "Unable to close the read-only HDF5 schema self-check handle."); } -void writeFile( - const std::filesystem::path& temporaryPath, - const Domain& domain, - const AnalysisState& state, - const std::vector& diagnostics, - const WriterModelData& modelData) { - Hdf5Handle file{ - requireHdf5Id( - H5Fcreate( - temporaryPath.string().c_str(), H5F_ACC_EXCL, - H5P_DEFAULT, H5P_DEFAULT), - "Unable to create the temporary HDF5 file."), - H5Fclose}; - writeMetadata(file.get(), domain); - (void)createGroup(file.get(), "/model"); - (void)createGroup(file.get(), "/steps/Step-1/frames/0/nodal"); - (void)createGroup(file.get(), "/steps/Step-1/frames/0/element"); - if (isShellDomain(domain)) { - (void)createGroup(file.get(), "/model/shell"); - (void)createGroup(file.get(), "/steps/Step-1/frames/0/element/shell"); - (void)createGroup(file.get(), "/steps/Step-1/frames/0/global"); +/// @brief Selects a unique same-directory path for atomic finalization. +std::filesystem::path MakeTemporaryPath( + const std::filesystem::path& output_path) { + static std::atomic sequence{0U}; + const std::filesystem::path parent = output_path.parent_path(); + for (std::size_t attempt = 0U; attempt < 1024U; ++attempt) { + const std::wstring filename = + L"." + output_path.filename().wstring() + L".tmp." + + std::to_wstring(GetCurrentProcessId()) + L"." + + std::to_wstring(sequence.fetch_add(1U)); + const std::filesystem::path candidate = parent / filename; + std::error_code error; + const bool exists = std::filesystem::exists(candidate, error); + if (error) { + throw Hdf5Failure{"Unable to inspect the temporary output path."}; } - writeNodes(file.get(), domain); - if (isShellDomain(domain)) { - writeShellElements(file.get(), domain); - writeShellModelData(file.get(), domain, modelData); - } else { - writeBeamElements(file.get(), domain, modelData.beamLocalAxes); + if (!exists) { + return candidate; } - writeResultDatasets(file.get(), domain, state); - writeDiagnostics(file.get(), diagnostics); - requireHdf5( - H5Fflush(file.get(), H5F_SCOPE_GLOBAL), - "Unable to flush the temporary HDF5 file."); - requireHdf5( - file.closeChecked(), - "Unable to close the temporary HDF5 file after writing."); + } + throw Hdf5Failure{"Unable to allocate a unique temporary output path."}; } -Hdf5Handle openDatasetForCheck(const hid_t file, const char* path) { - return Hdf5Handle{ - requireHdf5Id( - H5Dopen2(file, path, H5P_DEFAULT), - "A mandatory HDF5 dataset is missing."), - H5Dclose}; +/// @brief Atomically publishes a checked candidate without weakening an +/// existing final result on failure. +bool FinalizeFile(const std::filesystem::path& temporary_path, + const std::filesystem::path& output_path) { + const DWORD attributes = GetFileAttributesW(output_path.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES) { + // ReplaceFileW is the one-operation existing-final policy: failure + // leaves the old authoritative bytes in place. + return ReplaceFileW(output_path.c_str(), temporary_path.c_str(), nullptr, + 0U, nullptr, nullptr) != FALSE; + } + const DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) { + return false; + } + // A new final uses a same-directory write-through move after close/check. + return MoveFileExW(temporary_path.c_str(), output_path.c_str(), + MOVEFILE_WRITE_THROUGH) != FALSE; } -std::vector checkedDimensions(const hid_t dataset) { - Hdf5Handle space{ - requireHdf5Id( - H5Dget_space(dataset), "Unable to inspect an HDF5 dataspace."), - H5Sclose}; - const int rank = H5Sget_simple_extent_ndims(space.get()); - if (rank < 0) { - throw Hdf5Failure{"Unable to inspect an HDF5 dataset rank."}; +} // namespace + +Status Hdf5ResultsWriter::Write(const std::filesystem::path& output_path, + const Domain& domain, + const AnalysisState& state, + const std::vector& diagnostics) { + WriterModelData model_data; + try { + const Status validation = ValidateWriterInput(output_path, domain, state, + diagnostics, model_data); + if (!validation.IsOk()) { + return validation; } - std::vector dimensions(static_cast(rank)); - if (rank > 0) { - requireHdf5( - H5Sget_simple_extent_dims(space.get(), dimensions.data(), nullptr), - "Unable to inspect HDF5 dataset dimensions."); + + Hdf5ErrorSilencer silence_backend_errors; + const std::filesystem::path temporary_path = MakeTemporaryPath(output_path); + TemporaryFileGuard cleanup{temporary_path}; + WriteFile(temporary_path, domain, state, diagnostics, model_data); + // Close occurs when WriteFile returns; only the reopened read-only file + // can approve this temp artifact for authoritative replacement. + SelfCheckFile(temporary_path, domain, state, diagnostics.size(), + model_data); + if (!FinalizeFile(temporary_path, output_path)) { + return OutputFailure("hdf5-finalization-failure", + "The checked temporary HDF5 file could not " + "replace the final output."); } - return dimensions; + cleanup.Release(); + return Status::Ok(); + } catch (const Hdf5Failure& failure) { + return OutputFailure("hdf5-write-failure", failure.what()); + } catch (const std::exception& failure) { + return OutputFailure("hdf5-write-failure", failure.what()); + } } -void requireStringAttribute( - const hid_t object, const char* name, const std::string& expected) { - Hdf5Handle attribute{ - requireHdf5Id( - H5Aopen(object, name, H5P_DEFAULT), - "A mandatory HDF5 string attribute is missing."), - H5Aclose}; - Hdf5Handle type{ - requireHdf5Id( - H5Aget_type(attribute.get()), "Unable to inspect an HDF5 attribute type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_STRING || - H5Tis_variable_str(type.get()) <= 0 || - H5Tget_cset(type.get()) != H5T_CSET_UTF8) { - throw Hdf5Failure{"An HDF5 string attribute is not variable-length UTF-8."}; - } - char* raw = nullptr; - requireHdf5( - H5Aread(attribute.get(), type.get(), &raw), - "Unable to read an HDF5 string attribute."); - const std::string actual = raw == nullptr ? std::string{} : std::string{raw}; - if (raw != nullptr) { - requireHdf5(H5free_memory(raw), "Unable to release an HDF5 string value."); - } - if (actual != expected) { - throw Hdf5Failure{"An HDF5 string attribute has the wrong value."}; - } -} - -void requireUint64Attribute( - const hid_t object, const char* name, const std::uint64_t expected) { - Hdf5Handle attribute{ - requireHdf5Id( - H5Aopen(object, name, H5P_DEFAULT), - "A mandatory HDF5 integer attribute is missing."), - H5Aclose}; - Hdf5Handle type{ - requireHdf5Id( - H5Aget_type(attribute.get()), - "Unable to inspect an HDF5 integer attribute type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_INTEGER || - H5Tget_size(type.get()) != sizeof(std::uint64_t) || - H5Tget_sign(type.get()) != H5T_SGN_NONE || - H5Tequal(type.get(), H5T_STD_U64LE) <= 0) { - throw Hdf5Failure{"An HDF5 integer attribute is not portable uint64."}; - } - std::uint64_t actual = 0U; - requireHdf5( - H5Aread(attribute.get(), H5T_NATIVE_UINT64, &actual), - "Unable to read an HDF5 integer attribute."); - if (actual != expected) { - throw Hdf5Failure{"An HDF5 integer attribute has the wrong value."}; - } -} - -void requirePortableCompoundMember( - const hid_t compoundType, const unsigned index, const std::string& name) { - Hdf5Handle memberType{ - requireHdf5Id( - H5Tget_member_type(compoundType, index), - "Unable to inspect a compound HDF5 field type."), - H5Tclose}; - const bool isUint64 = - name == "internal_node_id" || name == "internal_element_id" || - name == "internal_material_id" || name == "internal_section_id" || - name == "shell_section_internal_id" || - name == "material_internal_id" || name == "source_line" || - name == "gauss_point_index" || name == "section_point_index" || - name == "line"; - const bool isFloat64 = - name == "x1" || name == "x2" || name == "S11" || - name == "E" || name == "nu" || name == "thickness"; - const bool isString = - name == "instance_name" || name == "source_label" || - name == "source_element_type" || name == "internal_formulation" || - name == "name" || name == "source_file" || name == "source_elset" || - name == "source" || name == "severity" || name == "code" || - name == "file" || name == "keyword" || - name == "entity_identity" || name == "message"; - if (isUint64) { - if (H5Tget_class(memberType.get()) != H5T_INTEGER || - H5Tget_size(memberType.get()) != sizeof(std::uint64_t) || - H5Tget_sign(memberType.get()) != H5T_SGN_NONE || - H5Tequal(memberType.get(), H5T_STD_U64LE) <= 0) { - throw Hdf5Failure{"A compound HDF5 field is not portable uint64."}; - } - return; - } - if (isFloat64) { - if (H5Tget_class(memberType.get()) != H5T_FLOAT || - H5Tget_size(memberType.get()) != sizeof(double) || - H5Tequal(memberType.get(), H5T_IEEE_F64LE) <= 0) { - throw Hdf5Failure{"A compound HDF5 field is not float64."}; - } - return; - } - if (isString) { - if (H5Tget_class(memberType.get()) != H5T_STRING || - H5Tis_variable_str(memberType.get()) <= 0 || - H5Tget_cset(memberType.get()) != H5T_CSET_UTF8) { - throw Hdf5Failure{"A compound HDF5 field is not UTF-8."}; - } - return; - } - - if (H5Tget_class(memberType.get()) != H5T_ARRAY) { - throw Hdf5Failure{"A compound HDF5 array field has the wrong type."}; - } - const int rank = H5Tget_array_ndims(memberType.get()); - if (rank <= 0) { - throw Hdf5Failure{"Unable to inspect a compound HDF5 array rank."}; - } - std::vector dimensions(static_cast(rank)); - requireHdf5( - H5Tget_array_dims2(memberType.get(), dimensions.data()), - "Unable to inspect compound HDF5 array dimensions."); - Hdf5Handle baseType{ - requireHdf5Id( - H5Tget_super(memberType.get()), - "Unable to inspect a compound HDF5 array base type."), - H5Tclose}; - if (name == "node_internal_ids") { - if ((dimensions != std::vector{2U} && - dimensions != std::vector{4U}) || - H5Tequal(baseType.get(), H5T_STD_U64LE) <= 0) { - throw Hdf5Failure{"Element connectivity is not uint64[2] or uint64[4]."}; - } - } else if (name == "coordinates") { - if (dimensions != std::vector{3U} || - H5Tequal(baseType.get(), H5T_IEEE_F64LE) <= 0) { - throw Hdf5Failure{"Node coordinates are not float64[3]."}; - } - } else if (name == "local_axes") { - if (dimensions != std::vector{3U, 3U} || - H5Tequal(baseType.get(), H5T_IEEE_F64LE) <= 0) { - throw Hdf5Failure{"Element local axes are not float64[3,3]."}; - } - } else { - throw Hdf5Failure{"A compound HDF5 field has an unknown type contract."}; - } -} - -void requireResultAttributes( - const hid_t dataset, - const char* componentNames, - const char* componentUnits, - const char* coordinateSystem, - const char* location) { - requireStringAttribute(dataset, "component_names", componentNames); - requireStringAttribute( - dataset, "component_unit_dimensions", componentUnits); - requireStringAttribute(dataset, "coordinate_system", coordinateSystem); - requireStringAttribute(dataset, "location", location); - requireStringAttribute(dataset, "step_name", kStepName); - requireUint64Attribute(dataset, "frame_index", 0U); -} - -void requireShellResultIdentity( - const hid_t file, const char* path, const bool usesSectionPositions) { - auto dataset = openDatasetForCheck(file, path); - requireStringAttribute( - dataset.get(), - "source_element_type_dataset", - "/model/elements.source_element_type"); - requireStringAttribute( - dataset.get(), "internal_formulation", "FESA-MITC4"); - requireStringAttribute( - dataset.get(), - "midsurface_location_dataset", - "/model/shell/midsurface_locations"); - requireStringAttribute( - dataset.get(), - "local_frame_dataset", - "/steps/Step-1/frames/0/element/shell/local_frame"); - if (usesSectionPositions) { - requireStringAttribute( - dataset.get(), - "section_position_dataset", - "/model/shell/section_positions"); - } -} - -void requireDoubleDataset( - const hid_t file, - const char* path, - const std::vector& expectedDimensions, - const char* componentNames, - const char* componentUnits, - const char* coordinateSystem, - const char* location) { - auto dataset = openDatasetForCheck(file, path); - if (checkedDimensions(dataset.get()) != expectedDimensions) { - throw Hdf5Failure{"A floating-point HDF5 dataset has the wrong shape."}; - } - Hdf5Handle type{ - requireHdf5Id( - H5Dget_type(dataset.get()), "Unable to inspect an HDF5 dataset type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_FLOAT || - H5Tget_size(type.get()) != sizeof(double) || - H5Tequal(type.get(), H5T_IEEE_F64LE) <= 0) { - throw Hdf5Failure{"A result dataset is not IEEE-754 float64 little-endian."}; - } - requireResultAttributes( - dataset.get(), componentNames, componentUnits, coordinateSystem, location); - std::size_t valueCount = 1U; - for (const hsize_t dimension : expectedDimensions) { - std::size_t next = 0U; - if (!sizeProductFits( - valueCount, static_cast(dimension), next)) { - throw Hdf5Failure{"A result dataset shape overflows size_t."}; - } - valueCount = next; - } - std::vector values(valueCount); - if (!values.empty()) { - requireHdf5( - H5Dread( - dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()), - "Unable to read a result dataset during self-check."); - } - if (!std::all_of(values.begin(), values.end(), [](const double value) { - return std::isfinite(value); - })) { - throw Hdf5Failure{"A result dataset contains a nonfinite value."}; - } -} - -void requireModelDoubleDataset( - const hid_t file, - const char* path, - const std::vector& expectedDimensions, - const char* componentNames, - const char* componentUnits, - const char* coordinateSystem, - const char* location, - const std::vector* expectedValues = nullptr) { - auto dataset = openDatasetForCheck(file, path); - if (checkedDimensions(dataset.get()) != expectedDimensions) { - throw Hdf5Failure{"A model floating-point dataset has the wrong shape."}; - } - Hdf5Handle type{ - requireHdf5Id( - H5Dget_type(dataset.get()), "Unable to inspect a model dataset type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_FLOAT || - H5Tget_size(type.get()) != sizeof(double) || - H5Tequal(type.get(), H5T_IEEE_F64LE) <= 0) { - throw Hdf5Failure{"A model dataset is not IEEE-754 float64 little-endian."}; - } - requireStringAttribute(dataset.get(), "component_names", componentNames); - requireStringAttribute( - dataset.get(), "component_unit_dimensions", componentUnits); - requireStringAttribute(dataset.get(), "coordinate_system", coordinateSystem); - requireStringAttribute(dataset.get(), "location", location); - std::size_t valueCount = 1U; - for (const hsize_t dimension : expectedDimensions) { - std::size_t next = 0U; - if (!sizeProductFits( - valueCount, static_cast(dimension), next)) { - throw Hdf5Failure{"A model dataset shape overflows size_t."}; - } - valueCount = next; - } - std::vector values(valueCount); - if (!values.empty()) { - requireHdf5( - H5Dread( - dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()), - "Unable to read a model dataset during self-check."); - } - if (!std::all_of(values.begin(), values.end(), [](const double value) { - return std::isfinite(value); - })) { - throw Hdf5Failure{"A model dataset contains a nonfinite value."}; - } - if (expectedValues != nullptr && values != *expectedValues) { - throw Hdf5Failure{"A fixed model dataset has the wrong value order."}; - } -} - -void requireUint8Dataset( - const hid_t file, - const char* path, - const std::vector& expectedDimensions, - const std::vector& expectedValues) { - auto dataset = openDatasetForCheck(file, path); - if (checkedDimensions(dataset.get()) != expectedDimensions) { - throw Hdf5Failure{"A uint8 HDF5 dataset has the wrong shape."}; - } - Hdf5Handle type{ - requireHdf5Id( - H5Dget_type(dataset.get()), "Unable to inspect a uint8 dataset type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_INTEGER || - H5Tget_size(type.get()) != sizeof(std::uint8_t) || - H5Tget_sign(type.get()) != H5T_SGN_NONE || - H5Tequal(type.get(), H5T_STD_U8LE) <= 0) { - throw Hdf5Failure{"A constraint mask is not portable uint8."}; - } - std::vector values(expectedValues.size()); - if (!values.empty()) { - requireHdf5( - H5Dread( - dataset.get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()), - "Unable to read the constraint mask during self-check."); - } - if (values != expectedValues) { - throw Hdf5Failure{"The constraint mask has the wrong value order."}; - } - requireStringAttribute(dataset.get(), "component_names", "UX,UY,UZ,URX,URY,URZ"); - requireStringAttribute(dataset.get(), "value_meaning", "0=free,1=constrained"); -} - -void requireCompoundDataset( - const hid_t file, - const char* path, - const hsize_t expectedRows, - const std::vector& expectedMembers) { - auto dataset = openDatasetForCheck(file, path); - if (checkedDimensions(dataset.get()) != std::vector{expectedRows}) { - throw Hdf5Failure{"A compound HDF5 dataset has the wrong shape."}; - } - Hdf5Handle type{ - requireHdf5Id( - H5Dget_type(dataset.get()), "Unable to inspect a compound type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_COMPOUND || - H5Tget_nmembers(type.get()) != static_cast(expectedMembers.size())) { - throw Hdf5Failure{"A compound HDF5 dataset has the wrong field count."}; - } - for (std::size_t index = 0U; index < expectedMembers.size(); ++index) { - char* rawName = H5Tget_member_name( - type.get(), static_cast(index)); - if (rawName == nullptr) { - throw Hdf5Failure{"Unable to inspect a compound HDF5 field name."}; - } - const std::string actualName{rawName}; - requireHdf5( - H5free_memory(rawName), "Unable to release a compound field name."); - if (actualName != expectedMembers[index]) { - throw Hdf5Failure{"A compound HDF5 field has the wrong identity."}; - } - requirePortableCompoundMember( - type.get(), static_cast(index), expectedMembers[index]); - } -} - -void selfCheckFile( - const std::filesystem::path& path, - const Domain& domain, - const AnalysisState& state, - const std::size_t diagnosticCount, - const WriterModelData& modelData) { - if (H5Fis_hdf5(path.string().c_str()) <= 0) { - throw Hdf5Failure{"The temporary output is not an HDF5 file."}; - } - Hdf5Handle file{ - requireHdf5Id( - H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), - "Unable to reopen the temporary HDF5 file read-only."), - H5Fclose}; - Hdf5Handle metadata{ - requireHdf5Id( - H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), - "The HDF5 metadata group is missing."), - H5Gclose}; - requireUint64Attribute(metadata.get(), "schema_version", 0U); - requireStringAttribute( - metadata.get(), "solver_version", std::string{SolverVersion()}); - requireStringAttribute( - metadata.get(), "source_input_identity", sourceInputIdentity(domain)); - requireStringAttribute( - metadata.get(), "unit_system_label", "user-consistent-unspecified"); - if (isShellDomain(domain)) { - requireStringAttribute( - metadata.get(), "feature_id", "linear-static-mitc4-shell"); - requireStringAttribute( - metadata.get(), - "coordinate_convention", - "global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta"); - requireStringAttribute( - metadata.get(), "internal_formulation", "FESA-MITC4"); - requireStringAttribute( - metadata.get(), - "integration_rule", - "2x2x2-gauss; mitc4-edge-midpoint-shear"); - } else { - requireStringAttribute( - metadata.get(), "feature_id", "linear-static-3d-euler-beam"); - requireStringAttribute( - metadata.get(), - "coordinate_convention", - "global-cartesian; beam-local=(t,n1,t-cross-n1)"); - requireStringAttribute( - metadata.get(), "element_formulation", "B33-3D-Euler-Bernoulli"); - } - requireStringAttribute(metadata.get(), "step_name", kStepName); - requireUint64Attribute(metadata.get(), "frame_index", 0U); - - requireCompoundDataset( - file.get(), - "/model/nodes", - static_cast(domain.Nodes().size()), - {"internal_node_id", "instance_name", "source_label", "coordinates"}); - auto nodes = openDatasetForCheck(file.get(), "/model/nodes"); - requireStringAttribute(nodes.get(), "coordinate_system", "global-cartesian"); - requireStringAttribute(nodes.get(), "units_label", "length"); - const std::vector nodalDimensions = { - static_cast(domain.Nodes().size()), kDofsPerNode}; - requireDoubleDataset( - file.get(), - "/steps/Step-1/frames/0/nodal/displacement", - nodalDimensions, - "UX,UY,UZ,URX,URY,URZ", - "length,length,length,radian,radian,radian", - "global-cartesian", - "nodal"); - requireDoubleDataset( - file.get(), - "/steps/Step-1/frames/0/nodal/reaction", - nodalDimensions, - "RF1,RF2,RF3,RM1,RM2,RM3", - "force,force,force,force*length,force*length,force*length", - "global-cartesian", - "nodal"); - - if (isShellDomain(domain)) { - requireCompoundDataset( - file.get(), - "/model/elements", - static_cast(domain.ShellElements().size()), - {"internal_element_id", "instance_name", "source_label", - "source_element_type", "internal_formulation", "node_internal_ids", - "shell_section_internal_id", "material_internal_id"}); - auto elements = openDatasetForCheck(file.get(), "/model/elements"); - requireStringAttribute(elements.get(), "formulation", "FESA-MITC4"); - requireCompoundDataset( - file.get(), - "/model/shell/materials", - static_cast(domain.Materials().size()), - {"internal_material_id", "name", "E", "nu"}); - requireCompoundDataset( - file.get(), - "/model/shell/sections", - static_cast(domain.ShellSections().size()), - {"internal_section_id", "source_file", "source_line", "source_elset", - "material_internal_id", "thickness"}); - - std::vector directors; - std::vector frames; - directors.reserve(domain.Nodes().size() * 3U); - frames.reserve(domain.Nodes().size() * 9U); - for (const auto& frame : domain.ShellNodeInitialFrames()) { - directors.insert( - directors.end(), frame.director.begin(), frame.director.end()); - frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); - frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); - frames.insert(frames.end(), frame.director.begin(), frame.director.end()); - } - requireModelDoubleDataset( - file.get(), "/model/shell/nodal_director", - {static_cast(domain.Nodes().size()), 3U}, - "D1,D2,D3", "1,1,1", "global-cartesian", "nodal", &directors); - requireModelDoubleDataset( - file.get(), "/model/shell/nodal_frame", - {static_cast(domain.Nodes().size()), 3U, 3U}, - "X,Y,Z", "1,1,1", "global-cartesian", "nodal-frame", &frames); - auto nodalFrame = openDatasetForCheck( - file.get(), "/model/shell/nodal_frame"); - requireStringAttribute(nodalFrame.get(), "axis_names", "A,B,D"); - requireUint8Dataset( - file.get(), "/model/nodal_constraint_mask", nodalDimensions, - modelData.constraintMask); - requireModelDoubleDataset( - file.get(), "/model/prescribed_displacement", nodalDimensions, - "UX,UY,UZ,URX,URY,URZ", - "length,length,length,radian,radian,radian", - "global-cartesian", "nodal-prescribed-value", - &modelData.prescribedDisplacement); - const double gauss = 1.0 / std::sqrt(3.0); - const std::vector locations{ - -gauss, -gauss, gauss, -gauss, - gauss, gauss, -gauss, gauss}; - requireModelDoubleDataset( - file.get(), "/model/shell/midsurface_locations", {4U, 2U}, - "XI,ETA", "1,1", "shell-natural", "midsurface-location", - &locations); - const std::vector sectionPositions{-1.0, 0.0, 1.0}; - requireModelDoubleDataset( - file.get(), "/model/shell/section_positions", {3U}, - "ZETA", "1", "shell-natural", "section-position", - §ionPositions); - auto positions = openDatasetForCheck( - file.get(), "/model/shell/section_positions"); - requireStringAttribute( - positions.get(), "position_names", "BOTTOM,MIDDLE,TOP"); - - const hsize_t elementCount = - static_cast(domain.ShellElements().size()); - const std::string shellRoot = std::string{kStepRoot} + "/element/shell"; - requireDoubleDataset( - file.get(), (shellRoot + "/local_frame").c_str(), - {elementCount, 4U, 3U, 3U}, "X,Y,Z", "1,1,1", - "global-cartesian", "shell-local-frame"); - auto localFrame = openDatasetForCheck( - file.get(), (shellRoot + "/local_frame").c_str()); - requireStringAttribute(localFrame.get(), "axis_names", "E1,E2,E3"); - requireStringAttribute( - localFrame.get(), "internal_formulation", "FESA-MITC4"); - requireStringAttribute( - localFrame.get(), - "source_element_type_dataset", - "/model/elements.source_element_type"); - requireStringAttribute( - localFrame.get(), - "midsurface_location_dataset", - "/model/shell/midsurface_locations"); - requireDoubleDataset( - file.get(), (shellRoot + "/generalized_strain").c_str(), - {elementCount, 4U, 8U}, - "E11,E22,G12,K11,K22,K12,G13,G23", - "1,1,1,1/length,1/length,1/length,1,1", - "shell-local", "midsurface"); - requireShellResultIdentity( - file.get(), (shellRoot + "/generalized_strain").c_str(), false); - requireDoubleDataset( - file.get(), (shellRoot + "/section_resultant").c_str(), - {elementCount, 4U, 8U}, - "N11,N22,N12,M11,M22,M12,Q13,Q23", - "force/length,force/length,force/length,force,force,force,force/length,force/length", - "shell-local", "midsurface"); - requireShellResultIdentity( - file.get(), (shellRoot + "/section_resultant").c_str(), false); - requireDoubleDataset( - file.get(), (shellRoot + "/stress").c_str(), - {elementCount, 4U, 3U, 3U}, "S11,S22,S12", - "force/length^2,force/length^2,force/length^2", - "shell-local", "section-position"); - requireShellResultIdentity( - file.get(), (shellRoot + "/stress").c_str(), true); - requireDoubleDataset( - file.get(), "/steps/Step-1/frames/0/global/energy", {1U}, - "PHYSICAL_STRAIN_ENERGY", "force*length", "global", "global"); - requireDoubleDataset( - file.get(), "/steps/Step-1/frames/0/global/equilibrium", {6U}, - "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", - "force,force,force,force*length,force*length,force*length", - "global-cartesian", "global-origin"); - requireDoubleDataset( - file.get(), "/steps/Step-1/frames/0/global/verification_metrics", {3U}, - "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_NORMALIZED", - "1,1,1", "global", "verification"); - auto metrics = openDatasetForCheck( - file.get(), "/steps/Step-1/frames/0/global/verification_metrics"); - requireStringAttribute( - metrics.get(), - "metric_definition_ids", - "free-residual-l2-over-max-free-force-l2,force-balance-l2-over-max-force-sum,moment-balance-l2-over-max-moment-sum"); - requireStringAttribute( - metrics.get(), "acceptance_thresholds", "1e-10,1e-10,1e-10"); - for (const char* forbidden : { - "/steps/Step-1/frames/0/element/shell/drilling", - "/steps/Step-1/frames/0/element/shell/drilling_energy", - "/steps/Step-1/frames/0/element/shell/S33", - "/steps/Step-1/frames/0/element/shell/S13", - "/steps/Step-1/frames/0/element/shell/S23"}) { - if (H5Lexists(file.get(), forbidden, H5P_DEFAULT) != 0) { - throw Hdf5Failure{"A forbidden shell result path exists."}; - } - } - } else { - requireCompoundDataset( - file.get(), - "/model/elements", - static_cast(domain.Elements().size()), - {"internal_element_id", "instance_name", "source_label", - "node_internal_ids", "local_axes"}); - auto elements = openDatasetForCheck(file.get(), "/model/elements"); - requireStringAttribute( - elements.get(), "formulation", "B33-3D-Euler-Bernoulli"); - const std::vector endDimensions = { - static_cast(domain.Elements().size()), - kEndpointCount, - kEndActionComponentCount}; - const std::vector generalizedDimensions = { - static_cast(domain.Elements().size()), - kGaussPointCount, - kGeneralizedComponentCount}; - requireDoubleDataset( - file.get(), "/steps/Step-1/frames/0/element/end_force_local", - endDimensions, "FX,FY,FZ,MX,MY,MZ", - "force,force,force,force*length,force*length,force*length", - "beam-local", "endpoint-outward-action"); - requireDoubleDataset( - file.get(), "/steps/Step-1/frames/0/element/section_resultant", - generalizedDimensions, "N,T,My,Mz", - "force,force*length,force*length,force*length", - "beam-local", "endpoint-positive-local-x-section-cut"); - requireDoubleDataset( - file.get(), "/steps/Step-1/frames/0/element/generalized_strain", - generalizedDimensions, "epsilon0,kappa_x,kappa_y,kappa_z", - "1,1/length,1/length,1/length", "beam-local", "integration-point"); - requireDoubleDataset( - file.get(), "/steps/Step-1/frames/0/element/generalized_resultant", - generalizedDimensions, "N,T,My,Mz", - "force,force*length,force*length,force*length", - "beam-local", "integration-point"); - requireCompoundDataset( - file.get(), "/steps/Step-1/frames/0/element/stress_s11", - static_cast(state.StressResults().size()), - {"internal_element_id", "gauss_point_index", "section_point_index", - "x1", "x2", "source", "S11"}); - auto stress = openDatasetForCheck( - file.get(), "/steps/Step-1/frames/0/element/stress_s11"); - requireResultAttributes( - stress.get(), "S11", "force/length^2", "beam-local", "section-point"); - } - requireCompoundDataset( - file.get(), - "/diagnostics", - static_cast(diagnosticCount), - {"severity", "code", "file", "line", "keyword", - "entity_identity", "message"}); - requireHdf5( - file.closeChecked(), - "Unable to close the read-only HDF5 schema self-check handle."); -} - -std::filesystem::path makeTemporaryPath( - const std::filesystem::path& outputPath) { - static std::atomic sequence{0U}; - const std::filesystem::path parent = outputPath.parent_path(); - for (std::size_t attempt = 0U; attempt < 1024U; ++attempt) { - const std::wstring filename = - L"." + outputPath.filename().wstring() + L".tmp." + - std::to_wstring(GetCurrentProcessId()) + L"." + - std::to_wstring(sequence.fetch_add(1U)); - const std::filesystem::path candidate = parent / filename; - std::error_code error; - const bool exists = std::filesystem::exists(candidate, error); - if (error) { - throw Hdf5Failure{"Unable to inspect the temporary output path."}; - } - if (!exists) { - return candidate; - } - } - throw Hdf5Failure{"Unable to allocate a unique temporary output path."}; -} - -bool finalizeFile( - const std::filesystem::path& temporaryPath, - const std::filesystem::path& outputPath) { - const DWORD attributes = GetFileAttributesW(outputPath.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES) { - // ReplaceFileW is the one-operation existing-final policy: failure - // leaves the old authoritative bytes in place. - return ReplaceFileW( - outputPath.c_str(), - temporaryPath.c_str(), - nullptr, - 0U, - nullptr, - nullptr) != FALSE; - } - const DWORD error = GetLastError(); - if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) { - return false; - } - // A new final uses a same-directory write-through move after close/check. - return MoveFileExW( - temporaryPath.c_str(), - outputPath.c_str(), - MOVEFILE_WRITE_THROUGH) != FALSE; -} - -} // namespace - -Status Hdf5ResultsWriter::Write( - const std::filesystem::path& outputPath, - const Domain& domain, - const AnalysisState& state, - const std::vector& diagnostics) { - WriterModelData modelData; - try { - const Status validation = validateWriterInput( - outputPath, domain, state, diagnostics, modelData); - if (!validation.IsOk()) { - return validation; - } - - Hdf5ErrorSilencer silenceBackendErrors; - const std::filesystem::path temporaryPath = makeTemporaryPath(outputPath); - TemporaryFileGuard cleanup{temporaryPath}; - writeFile(temporaryPath, domain, state, diagnostics, modelData); - // Close occurs when writeFile returns; only the reopened read-only file - // can approve this temp artifact for authoritative replacement. - selfCheckFile( - temporaryPath, domain, state, diagnostics.size(), modelData); - if (!finalizeFile(temporaryPath, outputPath)) { - return outputFailure( - "hdf5-finalization-failure", - "The checked temporary HDF5 file could not replace the final output."); - } - cleanup.release(); - return Status::Ok(); - } catch (const Hdf5Failure& failure) { - return outputFailure("hdf5-write-failure", failure.what()); - } catch (const std::exception& failure) { - return outputFailure("hdf5-write-failure", failure.what()); - } -} - -} // namespace fesa +} // namespace fesa diff --git a/tests/integration/app/fesa_application_test.cpp b/tests/integration/app/fesa_application_test.cpp index 04ad264..8d3f73b 100644 --- a/tests/integration/app/fesa_application_test.cpp +++ b/tests/integration/app/fesa_application_test.cpp @@ -1,4 +1,4 @@ -#include "fesa/app/fesa_application.hpp" +#include "fesa/app/fesa_application.h" #include #include @@ -19,101 +19,94 @@ namespace { constexpr const char* kStepRoot = "/steps/Step-1/frames/0"; class TempDirectory { -public: - explicit TempDirectory(const std::string& label) { - static std::atomic sequence{0U}; - const auto tick = std::chrono::steady_clock::now() - .time_since_epoch() - .count(); - path_ = std::filesystem::temp_directory_path() / - ("fesa-step24-app-" + label + "-" + - std::to_string(tick) + "-" + + public: + explicit TempDirectory(const std::string& label) { + static std::atomic sequence{0U}; + const auto tick = + std::chrono::steady_clock::now().time_since_epoch().count(); + path_ = std::filesystem::temp_directory_path() / + ("fesa-step24-app-" + label + "-" + std::to_string(tick) + "-" + std::to_string(sequence.fetch_add(1U))); - std::error_code error; - if (!std::filesystem::create_directory(path_, error) || error) { - throw std::runtime_error{"Unable to create the Step 24 app fixture."}; - } + std::error_code error; + if (!std::filesystem::create_directory(path_, error) || error) { + throw std::runtime_error{"Unable to create the Step 24 app fixture."}; } + } - TempDirectory(const TempDirectory&) = delete; - TempDirectory& operator=(const TempDirectory&) = delete; + TempDirectory(const TempDirectory&) = delete; + TempDirectory& operator=(const TempDirectory&) = delete; - ~TempDirectory() { - std::error_code ignored; - std::filesystem::remove_all(path_, ignored); - } + ~TempDirectory() { + std::error_code ignored; + std::filesystem::remove_all(path_, ignored); + } - const std::filesystem::path& path() const noexcept { return path_; } + const std::filesystem::path& Path() const noexcept { return path_; } -private: - std::filesystem::path path_; + private: + std::filesystem::path path_; }; class CurrentDirectoryGuard { -public: - explicit CurrentDirectoryGuard(const std::filesystem::path& replacement) - : original_{std::filesystem::current_path()} { - std::filesystem::current_path(replacement); - } + public: + explicit CurrentDirectoryGuard(const std::filesystem::path& replacement) + : original_{std::filesystem::current_path()} { + std::filesystem::current_path(replacement); + } - CurrentDirectoryGuard(const CurrentDirectoryGuard&) = delete; - CurrentDirectoryGuard& operator=(const CurrentDirectoryGuard&) = delete; + CurrentDirectoryGuard(const CurrentDirectoryGuard&) = delete; + CurrentDirectoryGuard& operator=(const CurrentDirectoryGuard&) = delete; - ~CurrentDirectoryGuard() { - std::error_code ignored; - std::filesystem::current_path(original_, ignored); - } + ~CurrentDirectoryGuard() { + std::error_code ignored; + std::filesystem::current_path(original_, ignored); + } -private: - std::filesystem::path original_; + private: + std::filesystem::path original_; }; class Hdf5Handle { -public: - using Closer = herr_t (*)(hid_t); + public: + using Closer = herr_t (*)(hid_t); - Hdf5Handle(const hid_t value, Closer closer) - : value_{value}, closer_{closer} {} - Hdf5Handle(const Hdf5Handle&) = delete; - Hdf5Handle& operator=(const Hdf5Handle&) = delete; - Hdf5Handle(Hdf5Handle&& other) noexcept - : value_{other.value_}, closer_{other.closer_} { - other.value_ = -1; - other.closer_ = nullptr; - } - ~Hdf5Handle() { - if (value_ >= 0 && closer_ != nullptr) { - (void)closer_(value_); - } + Hdf5Handle(const hid_t value, Closer closer) + : value_{value}, closer_{closer} {} + Hdf5Handle(const Hdf5Handle&) = delete; + Hdf5Handle& operator=(const Hdf5Handle&) = delete; + Hdf5Handle(Hdf5Handle&& other) noexcept + : value_{other.value_}, closer_{other.closer_} { + other.value_ = -1; + other.closer_ = nullptr; + } + ~Hdf5Handle() { + if (value_ >= 0 && closer_ != nullptr) { + (void)closer_(value_); } + } - hid_t get() const noexcept { return value_; } + hid_t Get() const noexcept { return value_; } -private: - hid_t value_{-1}; - Closer closer_{nullptr}; + private: + hid_t value_{-1}; + Closer closer_{nullptr}; }; -void writeText(const std::filesystem::path& path, const std::string& text) { - std::ofstream stream{path, std::ios::binary | std::ios::trunc}; - stream.write(text.data(), static_cast(text.size())); - if (!stream) { - throw std::runtime_error{"Unable to write the Step 24 app input."}; - } +void WriteText(const std::filesystem::path& path, const std::string& text) { + std::ofstream stream{path, std::ios::binary | std::ios::trunc}; + stream.write(text.data(), static_cast(text.size())); + if (!stream) { + throw std::runtime_error{"Unable to write the Step 24 app input."}; + } } -std::string axialDeck( - const bool constrained, - const bool zeroLength, - const bool outputRequests) { - const std::string secondNode = zeroLength - ? "2, 0., 0., 0.\n" - : "2, 2., 0., 0.\n"; - const std::string boundaries = constrained - ? "*Boundary\nRoot, 1, 6\nTip, 2, 6\n" - : ""; - const std::string outputs = outputRequests - ? R"inp(*Output, field +std::string AxialDeck(const bool constrained, const bool zero_length, + const bool output_requests) { + const std::string second_node = + zero_length ? "2, 0., 0., 0.\n" : "2, 2., 0., 0.\n"; + const std::string boundaries = + constrained ? "*Boundary\nRoot, 1, 6\nTip, 2, 6\n" : ""; + const std::string outputs = output_requests ? R"inp(*Output, field *Node Output U, RF *Element Output, directions=YES @@ -121,12 +114,13 @@ S, SF *Output, history *Contact Output )inp" - : ""; + : ""; - return std::string{R"inp(*Part, name=BeamPart + return std::string{R"inp(*Part, name=BeamPart *Node 1, 0., 0., 0. -)inp"} + secondNode + R"inp(*Element, type=B33 +)inp"} + second_node + + R"inp(*Element, type=B33 1, 1, 2 *Elset, elset=BeamSet 1 @@ -145,17 +139,19 @@ S, SF *Material, name=Steel *Elastic 100., 0.25 -)inp" + boundaries + R"inp(*Step, name=Load, nlgeom=NO +)inp" + boundaries + + R"inp(*Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Cload Tip, 1, 10. -)inp" + outputs + R"inp(*End Step +)inp" + outputs + + R"inp(*End Step )inp"; } -std::string allConstrainedShellDeck() { - return R"inp(*Part, name=ShellPart +std::string AllConstrainedShellDeck() { + return R"inp(*Part, name=ShellPart *Node 1, 0., 0., 0. 2, 1., 0., 0. @@ -186,299 +182,277 @@ All, 1, 6 )inp"; } -Hdf5Handle openFile(const std::filesystem::path& path) { - const hid_t file = H5Fopen( - path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - if (file < 0) { - throw std::runtime_error{"Unable to open the CLI HDF5 artifact."}; - } - return Hdf5Handle{file, H5Fclose}; +Hdf5Handle OpenFile(const std::filesystem::path& path) { + const hid_t file = + H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); + if (file < 0) { + throw std::runtime_error{"Unable to open the CLI HDF5 artifact."}; + } + return Hdf5Handle{file, H5Fclose}; } -Hdf5Handle openDataset(const hid_t file, const std::string& path) { - const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT); - if (dataset < 0) { - throw std::runtime_error{"Unable to open mandatory dataset: " + path}; - } - return Hdf5Handle{dataset, H5Dclose}; +Hdf5Handle OpenDataset(const hid_t file, const std::string& path) { + const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT); + if (dataset < 0) { + throw std::runtime_error{"Unable to open mandatory dataset: " + path}; + } + return Hdf5Handle{dataset, H5Dclose}; } -std::vector datasetDimensions( - const hid_t file, const std::string& path) { - const auto dataset = openDataset(file, path); - Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; - const int rank = H5Sget_simple_extent_ndims(space.get()); - if (space.get() < 0 || rank < 0) { - throw std::runtime_error{"Unable to inspect mandatory dataset dimensions."}; - } - std::vector dimensions(static_cast(rank)); - if (rank > 0 && - H5Sget_simple_extent_dims(space.get(), dimensions.data(), nullptr) < 0) { - throw std::runtime_error{"Unable to read mandatory dataset dimensions."}; - } - return dimensions; +std::vector DatasetDimensions(const hid_t file, + const std::string& path) { + const auto dataset = OpenDataset(file, path); + Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; + const int rank = H5Sget_simple_extent_ndims(space.Get()); + if (space.Get() < 0 || rank < 0) { + throw std::runtime_error{"Unable to inspect mandatory dataset dimensions."}; + } + std::vector dimensions(static_cast(rank)); + if (rank > 0 && + H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0) { + throw std::runtime_error{"Unable to read mandatory dataset dimensions."}; + } + return dimensions; } -std::vector readDoubleDataset( - const hid_t file, const std::string& path) { - const auto dimensions = datasetDimensions(file, path); - std::size_t count = 1U; - for (const hsize_t dimension : dimensions) { - count *= static_cast(dimension); - } - const auto dataset = openDataset(file, path); - std::vector values(count); - if (!values.empty() && - H5Dread(dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()) < 0) { - throw std::runtime_error{"Unable to read mandatory numeric results."}; - } - return values; +std::vector ReadDoubleDataset(const hid_t file, + const std::string& path) { + const auto dimensions = DatasetDimensions(file, path); + std::size_t count = 1U; + for (const hsize_t dimension : dimensions) { + count *= static_cast(dimension); + } + const auto dataset = OpenDataset(file, path); + std::vector values(count); + if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, + H5S_ALL, H5P_DEFAULT, values.data()) < 0) { + throw std::runtime_error{"Unable to read mandatory numeric results."}; + } + return values; } -std::string readStringAttribute(const hid_t object, const char* name) { - Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; - Hdf5Handle type{H5Aget_type(attribute.get()), H5Tclose}; - if (attribute.get() < 0 || type.get() < 0 || - H5Tget_class(type.get()) != H5T_STRING || - H5Tis_variable_str(type.get()) <= 0) { - throw std::runtime_error{"Expected a variable-length string attribute."}; - } - char* raw = nullptr; - if (H5Aread(attribute.get(), type.get(), &raw) < 0 || raw == nullptr) { - throw std::runtime_error{"Unable to read the HDF5 identity attribute."}; - } - const std::string value{raw}; - (void)H5free_memory(raw); - return value; +std::string ReadStringAttribute(const hid_t object, const char* name) { + Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; + Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; + if (attribute.Get() < 0 || type.Get() < 0 || + H5Tget_class(type.Get()) != H5T_STRING || + H5Tis_variable_str(type.Get()) <= 0) { + throw std::runtime_error{"Expected a variable-length string attribute."}; + } + char* raw = nullptr; + if (H5Aread(attribute.Get(), type.Get(), &raw) < 0 || raw == nullptr) { + throw std::runtime_error{"Unable to read the HDF5 identity attribute."}; + } + const std::string value{raw}; + (void)H5free_memory(raw); + return value; } -void expectMandatoryInventory(const hid_t file) { - for (const char* path : { - "/metadata", - "/model/nodes", - "/model/elements", - "/steps/Step-1/frames/0/nodal/displacement", - "/steps/Step-1/frames/0/nodal/reaction", - "/steps/Step-1/frames/0/element/end_force_local", - "/steps/Step-1/frames/0/element/section_resultant", - "/steps/Step-1/frames/0/element/generalized_strain", - "/steps/Step-1/frames/0/element/generalized_resultant", - "/steps/Step-1/frames/0/element/stress_s11", - "/diagnostics"}) { - EXPECT_GT(H5Lexists(file, path, H5P_DEFAULT), 0) << path; - } +void ExpectMandatoryInventory(const hid_t file) { + for (const char* path : + {"/metadata", "/model/nodes", "/model/elements", + "/steps/Step-1/frames/0/nodal/displacement", + "/steps/Step-1/frames/0/nodal/reaction", + "/steps/Step-1/frames/0/element/end_force_local", + "/steps/Step-1/frames/0/element/section_resultant", + "/steps/Step-1/frames/0/element/generalized_strain", + "/steps/Step-1/frames/0/element/generalized_resultant", + "/steps/Step-1/frames/0/element/stress_s11", "/diagnostics"}) { + EXPECT_GT(H5Lexists(file, path, H5P_DEFAULT), 0) << path; + } } -void expectFesaHdf5Identity( - const std::filesystem::path& output, - const std::filesystem::path& input) { - ASSERT_TRUE(std::filesystem::exists(output)); - ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); - const auto file = openFile(output); - expectMandatoryInventory(file.get()); - Hdf5Handle metadata{ - H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose}; - ASSERT_GE(metadata.get(), 0); - EXPECT_EQ( - readStringAttribute(metadata.get(), "feature_id"), - "linear-static-3d-euler-beam"); - const std::string normalizedInput = - std::filesystem::absolute(input).lexically_normal().generic_u8string(); - EXPECT_EQ( - readStringAttribute(metadata.get(), "source_input_identity").find( - "path=" + normalizedInput + ";content_identity="), - 0U); +void ExpectFesaHdf5Identity(const std::filesystem::path& output, + const std::filesystem::path& input) { + ASSERT_TRUE(std::filesystem::exists(output)); + ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); + const auto file = OpenFile(output); + ExpectMandatoryInventory(file.Get()); + Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; + ASSERT_GE(metadata.Get(), 0); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), + "linear-static-3d-euler-beam"); + const std::string normalized_input = + std::filesystem::absolute(input).lexically_normal().generic_u8string(); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity") + .find("path=" + normalized_input + ";content_identity="), + 0U); } -void expectShellHdf5Identity( - const std::filesystem::path& output, - const std::filesystem::path& input) { - ASSERT_TRUE(std::filesystem::exists(output)); - ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); - const auto file = openFile(output); - Hdf5Handle metadata{ - H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose}; - ASSERT_GE(metadata.get(), 0); - EXPECT_EQ( - readStringAttribute(metadata.get(), "feature_id"), - "linear-static-mitc4-shell"); - EXPECT_GT( - H5Lexists( - file.get(), - "/steps/Step-1/frames/0/element/shell/generalized_strain", - H5P_DEFAULT), - 0); - EXPECT_EQ( - datasetDimensions( - file.get(), - "/steps/Step-1/frames/0/nodal/displacement"), - (std::vector{4U, 6U})); - const std::string normalizedInput = - std::filesystem::absolute(input).lexically_normal().generic_u8string(); - EXPECT_EQ( - readStringAttribute(metadata.get(), "source_input_identity").find( - "path=" + normalizedInput + ";content_identity="), - 0U); +void ExpectShellHdf5Identity(const std::filesystem::path& output, + const std::filesystem::path& input) { + ASSERT_TRUE(std::filesystem::exists(output)); + ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); + const auto file = OpenFile(output); + Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; + ASSERT_GE(metadata.Get(), 0); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), + "linear-static-mitc4-shell"); + EXPECT_GT(H5Lexists(file.Get(), + "/steps/Step-1/frames/0/element/shell/generalized_strain", + H5P_DEFAULT), + 0); + EXPECT_EQ(DatasetDimensions(file.Get(), + "/steps/Step-1/frames/0/nodal/displacement"), + (std::vector{4U, 6U})); + const std::string normalized_input = + std::filesystem::absolute(input).lexically_normal().generic_u8string(); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity") + .find("path=" + normalized_input + ";content_identity="), + 0U); } struct AppRun { - int exitCode; - std::string standardError; + int exit_code; + std::string standard_error; }; -AppRun runApplication(const std::vector& arguments) { - testing::internal::CaptureStderr(); - try { - const int exitCode = fesa::FesaApplication{}.run(arguments); - return {exitCode, testing::internal::GetCapturedStderr()}; - } catch (...) { - (void)testing::internal::GetCapturedStderr(); - throw; - } +AppRun RunApplication(const std::vector& arguments) { + testing::internal::CaptureStderr(); + try { + const int exit_code = fesa::FesaApplication{}.Run(arguments); + return {exit_code, testing::internal::GetCapturedStderr()}; + } catch (...) { + (void)testing::internal::GetCapturedStderr(); + throw; + } } -void expectDiagnosticFieldOrder(const std::string& text) { - ASSERT_FALSE(text.empty()); - std::size_t cursor = 0U; - for (const char* field : { - "severity", "code", "file", "line", "keyword", - "entity_identity", "message"}) { - const auto position = text.find(field, cursor); - ASSERT_NE(position, std::string::npos) - << "Missing or out-of-order diagnostic field: " << field - << "\nstderr:\n" << text; - cursor = position + std::string{field}.size(); - } +void ExpectDiagnosticFieldOrder(const std::string& text) { + ASSERT_FALSE(text.empty()); + std::size_t cursor = 0U; + for (const char* field : {"severity", "code", "file", "line", "keyword", + "entity_identity", "message"}) { + const auto position = text.find(field, cursor); + ASSERT_NE(position, std::string::npos) + << "Missing or out-of-order diagnostic field: " << field + << "\nstderr:\n" + << text; + cursor = position + std::string{field}.size(); + } } -std::vector explicitOutputArguments( - const std::filesystem::path& input, - const std::filesystem::path& output) { - // FesaApplication receives argv[0]-excluded operands and options. - return {input.string(), "--output", output.string()}; +std::vector ExplicitOutputArguments( + const std::filesystem::path& input, const std::filesystem::path& output) { + // FesaApplication receives argv[0]-excluded operands and options. + return {input.string(), "--output", output.string()}; } -} // namespace +} // namespace TEST(LinearStaticCli, DefaultAndExplicitOutputProduceFesaHdf5) { - TempDirectory directory{"paths"}; - const auto input = directory.path() / "model.inp"; - writeText(input, axialDeck(true, false, false)); + TempDirectory directory{"paths"}; + const auto input = directory.Path() / "model.inp"; + WriteText(input, AxialDeck(true, false, false)); - const auto defaultOutput = directory.path() / "results.h5"; - { - CurrentDirectoryGuard currentDirectory{directory.path()}; - const auto result = runApplication({input.string()}); - ASSERT_EQ(result.exitCode, 0) << result.standardError; - } - expectFesaHdf5Identity(defaultOutput, input); + const auto default_output = directory.Path() / "results.h5"; + { + CurrentDirectoryGuard current_directory{directory.Path()}; + const auto result = RunApplication({input.string()}); + ASSERT_EQ(result.exit_code, 0) << result.standard_error; + } + ExpectFesaHdf5Identity(default_output, input); - const auto explicitOutput = directory.path() / "named-output.h5"; - const auto result = runApplication( - explicitOutputArguments(input, explicitOutput)); - ASSERT_EQ(result.exitCode, 0) << result.standardError; - expectFesaHdf5Identity(explicitOutput, input); + const auto explicit_output = directory.Path() / "named-output.h5"; + const auto result = + RunApplication(ExplicitOutputArguments(input, explicit_output)); + ASSERT_EQ(result.exit_code, 0) << result.standard_error; + ExpectFesaHdf5Identity(explicit_output, input); } TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) { - TempDirectory directory{"exit-codes"}; - const auto validInput = directory.path() / "valid.inp"; - const auto modelInput = directory.path() / "invalid-model.inp"; - const auto solverInput = directory.path() / "singular.inp"; - writeText(validInput, axialDeck(true, false, false)); - writeText(modelInput, axialDeck(true, true, false)); - writeText(solverInput, axialDeck(false, false, false)); + TempDirectory directory{"exit-codes"}; + const auto valid_input = directory.Path() / "valid.inp"; + const auto model_input = directory.Path() / "invalid-model.inp"; + const auto solver_input = directory.Path() / "singular.inp"; + WriteText(valid_input, AxialDeck(true, false, false)); + WriteText(model_input, AxialDeck(true, true, false)); + WriteText(solver_input, AxialDeck(false, false, false)); - const auto success = runApplication(explicitOutputArguments( - validInput, directory.path() / "success.h5")); - const auto usage = runApplication({}); - const auto missingInput = directory.path() / "missing.inp"; - const auto input = runApplication({missingInput.string()}); - const auto repeatedOutput = runApplication( - {missingInput.string(), "--output", "--output"}); - const auto unknownOutputOption = runApplication( - {missingInput.string(), "--output", "--bogus"}); - const auto model = runApplication(explicitOutputArguments( - modelInput, directory.path() / "model-failure.h5")); - const auto solver = runApplication(explicitOutputArguments( - solverInput, directory.path() / "solver-failure.h5")); - const auto output = runApplication(explicitOutputArguments( - validInput, - directory.path() / "nonexistent-parent" / "results.h5")); + const auto success = RunApplication( + ExplicitOutputArguments(valid_input, directory.Path() / "success.h5")); + const auto usage = RunApplication({}); + const auto missing_input = directory.Path() / "missing.inp"; + const auto input = RunApplication({missing_input.string()}); + const auto repeated_output = + RunApplication({missing_input.string(), "--output", "--output"}); + const auto unknown_output_option = + RunApplication({missing_input.string(), "--output", "--bogus"}); + const auto model = RunApplication(ExplicitOutputArguments( + model_input, directory.Path() / "model-failure.h5")); + const auto solver = RunApplication(ExplicitOutputArguments( + solver_input, directory.Path() / "solver-failure.h5")); + const auto output = RunApplication(ExplicitOutputArguments( + valid_input, directory.Path() / "nonexistent-parent" / "results.h5")); - EXPECT_EQ(success.exitCode, 0) << success.standardError; - EXPECT_EQ(usage.exitCode, 2); - EXPECT_EQ(input.exitCode, 3); - EXPECT_EQ(model.exitCode, 4); - EXPECT_EQ(solver.exitCode, 5); - EXPECT_EQ(output.exitCode, 6); + EXPECT_EQ(success.exit_code, 0) << success.standard_error; + EXPECT_EQ(usage.exit_code, 2); + EXPECT_EQ(input.exit_code, 3); + EXPECT_EQ(model.exit_code, 4); + EXPECT_EQ(solver.exit_code, 5); + EXPECT_EQ(output.exit_code, 6); - expectDiagnosticFieldOrder(usage.standardError); - expectDiagnosticFieldOrder(input.standardError); - expectDiagnosticFieldOrder(model.standardError); - expectDiagnosticFieldOrder(solver.standardError); - expectDiagnosticFieldOrder(output.standardError); - EXPECT_EQ(repeatedOutput.exitCode, 2); - expectDiagnosticFieldOrder(repeatedOutput.standardError); - EXPECT_NE(repeatedOutput.standardError.find("code=cli-usage"), - std::string::npos); - EXPECT_EQ(unknownOutputOption.exitCode, 2); - expectDiagnosticFieldOrder(unknownOutputOption.standardError); - EXPECT_NE(unknownOutputOption.standardError.find("code=cli-usage"), - std::string::npos); + ExpectDiagnosticFieldOrder(usage.standard_error); + ExpectDiagnosticFieldOrder(input.standard_error); + ExpectDiagnosticFieldOrder(model.standard_error); + ExpectDiagnosticFieldOrder(solver.standard_error); + ExpectDiagnosticFieldOrder(output.standard_error); + EXPECT_EQ(repeated_output.exit_code, 2); + ExpectDiagnosticFieldOrder(repeated_output.standard_error); + EXPECT_NE(repeated_output.standard_error.find("code=cli-usage"), + std::string::npos); + EXPECT_EQ(unknown_output_option.exit_code, 2); + ExpectDiagnosticFieldOrder(unknown_output_option.standard_error); + EXPECT_NE(unknown_output_option.standard_error.find("code=cli-usage"), + std::string::npos); } TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) { - TempDirectory directory{"output-requests"}; - const auto plainInput = directory.path() / "plain.inp"; - const auto requestedInput = directory.path() / "requested.inp"; - const auto plainOutput = directory.path() / "plain.h5"; - const auto requestedOutput = directory.path() / "requested.h5"; - writeText(plainInput, axialDeck(true, false, false)); - writeText(requestedInput, axialDeck(true, false, true)); + TempDirectory directory{"output-requests"}; + const auto plain_input = directory.Path() / "plain.inp"; + const auto requested_input = directory.Path() / "requested.inp"; + const auto plain_output = directory.Path() / "plain.h5"; + const auto requested_output = directory.Path() / "requested.h5"; + WriteText(plain_input, AxialDeck(true, false, false)); + WriteText(requested_input, AxialDeck(true, false, true)); - const auto plain = runApplication( - explicitOutputArguments(plainInput, plainOutput)); - const auto requested = runApplication( - explicitOutputArguments(requestedInput, requestedOutput)); - ASSERT_EQ(plain.exitCode, 0) << plain.standardError; - ASSERT_EQ(requested.exitCode, 0) << requested.standardError; + const auto plain = + RunApplication(ExplicitOutputArguments(plain_input, plain_output)); + const auto requested = RunApplication( + ExplicitOutputArguments(requested_input, requested_output)); + ASSERT_EQ(plain.exit_code, 0) << plain.standard_error; + ASSERT_EQ(requested.exit_code, 0) << requested.standard_error; - const auto plainFile = openFile(plainOutput); - const auto requestedFile = openFile(requestedOutput); - expectMandatoryInventory(plainFile.get()); - expectMandatoryInventory(requestedFile.get()); + const auto plain_file = OpenFile(plain_output); + const auto requested_file = OpenFile(requested_output); + ExpectMandatoryInventory(plain_file.Get()); + ExpectMandatoryInventory(requested_file.Get()); - for (const char* suffix : { - "/nodal/displacement", - "/nodal/reaction", - "/element/end_force_local", - "/element/section_resultant", - "/element/generalized_strain", - "/element/generalized_resultant"}) { - const std::string path = std::string{kStepRoot} + suffix; - EXPECT_EQ( - readDoubleDataset(requestedFile.get(), path), - readDoubleDataset(plainFile.get(), path)) - << path; - } - EXPECT_EQ(datasetDimensions(plainFile.get(), "/diagnostics"), - std::vector({0U})); - const auto requestedDiagnostics = - datasetDimensions(requestedFile.get(), "/diagnostics"); - ASSERT_EQ(requestedDiagnostics.size(), 1U); - EXPECT_GT(requestedDiagnostics[0U], 0U); + for (const char* suffix : + {"/nodal/displacement", "/nodal/reaction", "/element/end_force_local", + "/element/section_resultant", "/element/generalized_strain", + "/element/generalized_resultant"}) { + const std::string path = std::string{kStepRoot} + suffix; + EXPECT_EQ(ReadDoubleDataset(requested_file.Get(), path), + ReadDoubleDataset(plain_file.Get(), path)) + << path; + } + EXPECT_EQ(DatasetDimensions(plain_file.Get(), "/diagnostics"), + std::vector({0U})); + const auto requested_diagnostics = + DatasetDimensions(requested_file.Get(), "/diagnostics"); + ASSERT_EQ(requested_diagnostics.size(), 1U); + EXPECT_GT(requested_diagnostics[0U], 0U); } // MITC4-FLOW-001: shell input uses the unchanged application route and syntax. TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) { - TempDirectory directory{"shell-route"}; - const auto input = directory.path() / "shell.inp"; - const auto output = directory.path() / "shell-results.h5"; - writeText(input, allConstrainedShellDeck()); + TempDirectory directory{"shell-route"}; + const auto input = directory.Path() / "shell.inp"; + const auto output = directory.Path() / "shell-results.h5"; + WriteText(input, AllConstrainedShellDeck()); - const auto result = runApplication(explicitOutputArguments(input, output)); - ASSERT_EQ(result.exitCode, 0) << result.standardError; - expectShellHdf5Identity(output, input); + const auto result = RunApplication(ExplicitOutputArguments(input, output)); + ASSERT_EQ(result.exit_code, 0) << result.standard_error; + ExpectShellHdf5Identity(output, input); } diff --git a/tests/reference/b33_reference_comparison_test.cpp b/tests/reference/b33_reference_comparison_test.cpp index a19ac3a..cf46735 100644 --- a/tests/reference/b33_reference_comparison_test.cpp +++ b/tests/reference/b33_reference_comparison_test.cpp @@ -1,10 +1,5 @@ -#include "reference_comparison.hpp" - -#include "fesa/app/fesa_application.hpp" - -#include - #include +#include #include #include @@ -17,6 +12,9 @@ #include #include +#include "fesa/app/fesa_application.h" +#include "reference_comparison.h" + #ifndef FESA_TEST_SOURCE_DIR #error FESA_TEST_SOURCE_DIR must identify the repository root. #endif @@ -27,250 +25,224 @@ namespace { -constexpr const char* kStressPath = - "/steps/Step-1/frames/0/element/stress_s11"; +constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11"; constexpr std::size_t kExpectedRowCount = 176U; constexpr std::size_t kExpectedMetricCount = 16U; class Hdf5Handle { -public: - using Closer = herr_t (*)(hid_t); + public: + using Closer = herr_t (*)(hid_t); - Hdf5Handle(const hid_t value, Closer closer) - : value_{value}, closer_{closer} {} - Hdf5Handle(const Hdf5Handle&) = delete; - Hdf5Handle& operator=(const Hdf5Handle&) = delete; - ~Hdf5Handle() { - if (value_ >= 0 && closer_ != nullptr) { - (void)closer_(value_); - } + Hdf5Handle(const hid_t value, Closer closer) + : value_{value}, closer_{closer} {} + Hdf5Handle(const Hdf5Handle&) = delete; + Hdf5Handle& operator=(const Hdf5Handle&) = delete; + ~Hdf5Handle() { + if (value_ >= 0 && closer_ != nullptr) { + (void)closer_(value_); } + } - hid_t get() const noexcept { return value_; } + hid_t Get() const noexcept { return value_; } -private: - hid_t value_; - Closer closer_; + private: + hid_t value_; + Closer closer_; }; struct ReferenceSnapshotEntry { - std::filesystem::path relativePath; - bool isDirectory; - std::string bytes; - std::filesystem::file_time_type lastWriteTime; + std::filesystem::path relative_path; + bool is_directory; + std::string bytes; + std::filesystem::file_time_type last_write_time; }; -std::string readBytes(const std::filesystem::path& path) { - std::ifstream stream{path, std::ios::binary}; - if (!stream) { - throw std::runtime_error{"Unable to read reference evidence: " + - path.string()}; - } - return {std::istreambuf_iterator{stream}, - std::istreambuf_iterator{}}; +std::string ReadBytes(const std::filesystem::path& path) { + std::ifstream stream{path, std::ios::binary}; + if (!stream) { + throw std::runtime_error{"Unable to read reference evidence: " + + path.string()}; + } + return {std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; } -std::vector snapshotTree( +std::vector SnapshotTree( const std::filesystem::path& root) { - std::vector entries; - for (const auto& entry : std::filesystem::recursive_directory_iterator{root}) { - const bool isDirectory = entry.is_directory(); - if (!isDirectory && !entry.is_regular_file()) { - throw std::runtime_error{"Unexpected reference-tree entry type."}; - } - entries.push_back({ - std::filesystem::relative(entry.path(), root), - isDirectory, - isDirectory ? std::string{} : readBytes(entry.path()), - entry.last_write_time()}); + std::vector entries; + for (const auto& entry : + std::filesystem::recursive_directory_iterator{root}) { + const bool is_directory = entry.is_directory(); + if (!is_directory && !entry.is_regular_file()) { + throw std::runtime_error{"Unexpected reference-tree entry type."}; } - std::sort( - entries.begin(), - entries.end(), - [](const ReferenceSnapshotEntry& left, - const ReferenceSnapshotEntry& right) { - return left.relativePath.generic_string() < - right.relativePath.generic_string(); - }); - return entries; + entries.push_back({std::filesystem::relative(entry.path(), root), + is_directory, + is_directory ? std::string{} : ReadBytes(entry.path()), + entry.last_write_time()}); + } + std::sort(entries.begin(), entries.end(), + [](const ReferenceSnapshotEntry& left, + const ReferenceSnapshotEntry& right) { + return left.relative_path.generic_string() < + right.relative_path.generic_string(); + }); + return entries; } -void expectTreeUnchanged( - const std::vector& before, - const std::vector& after) { - ASSERT_EQ(after.size(), before.size()); - for (std::size_t index = 0U; index < before.size(); ++index) { - EXPECT_EQ(after[index].relativePath, before[index].relativePath); - EXPECT_EQ(after[index].isDirectory, before[index].isDirectory); - EXPECT_EQ(after[index].bytes, before[index].bytes) - << before[index].relativePath.string(); - EXPECT_EQ(after[index].lastWriteTime, before[index].lastWriteTime) - << before[index].relativePath.string(); - } +void ExpectTreeUnchanged(const std::vector& before, + const std::vector& after) { + ASSERT_EQ(after.size(), before.size()); + for (std::size_t index = 0U; index < before.size(); ++index) { + EXPECT_EQ(after[index].relative_path, before[index].relative_path); + EXPECT_EQ(after[index].is_directory, before[index].is_directory); + EXPECT_EQ(after[index].bytes, before[index].bytes) + << before[index].relative_path.string(); + EXPECT_EQ(after[index].last_write_time, before[index].last_write_time) + << before[index].relative_path.string(); + } } -double norm(const std::array& value) { - return std::sqrt( - value[0U] * value[0U] + value[1U] * value[1U] + - value[2U] * value[2U]); +double Norm(const std::array& value) { + return std::sqrt(value[0U] * value[0U] + value[1U] * value[1U] + + value[2U] * value[2U]); } -std::array sum( - const std::array& left, - const std::array& right) { - return { - left[0U] + right[0U], - left[1U] + right[1U], - left[2U] + right[2U]}; +std::array Sum(const std::array& left, + const std::array& right) { + return {left[0U] + right[0U], left[1U] + right[1U], left[2U] + right[2U]}; } -std::size_t stressRowCount(const std::filesystem::path& results) { - const hid_t fileId = - H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - if (fileId < 0) { - throw std::runtime_error{"Unable to open authoritative HDF5 output."}; - } - const Hdf5Handle file{fileId, H5Fclose}; - if (H5Lexists(file.get(), kStressPath, H5P_DEFAULT) <= 0) { - throw std::runtime_error{"Mandatory stress_s11 dataset is missing."}; - } - const hid_t datasetId = H5Dopen2(file.get(), kStressPath, H5P_DEFAULT); - if (datasetId < 0) { - throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."}; - } - const Hdf5Handle dataset{datasetId, H5Dclose}; - const hid_t spaceId = H5Dget_space(dataset.get()); - if (spaceId < 0) { - throw std::runtime_error{"Unable to inspect stress_s11 dataspace."}; - } - const Hdf5Handle space{spaceId, H5Sclose}; - if (H5Sget_simple_extent_ndims(space.get()) != 1) { - throw std::runtime_error{"stress_s11 must be a flat row dataset."}; - } - hsize_t count = 0U; - if (H5Sget_simple_extent_dims(space.get(), &count, nullptr) < 0) { - throw std::runtime_error{"Unable to read stress_s11 extent."}; - } - return static_cast(count); +std::size_t StressRowCount(const std::filesystem::path& results) { + const hid_t file_id = + H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); + if (file_id < 0) { + throw std::runtime_error{"Unable to open authoritative HDF5 output."}; + } + const Hdf5Handle file{file_id, H5Fclose}; + if (H5Lexists(file.Get(), kStressPath, H5P_DEFAULT) <= 0) { + throw std::runtime_error{"Mandatory stress_s11 dataset is missing."}; + } + const hid_t dataset_id = H5Dopen2(file.Get(), kStressPath, H5P_DEFAULT); + if (dataset_id < 0) { + throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."}; + } + const Hdf5Handle dataset{dataset_id, H5Dclose}; + const hid_t space_id = H5Dget_space(dataset.Get()); + if (space_id < 0) { + throw std::runtime_error{"Unable to inspect stress_s11 dataspace."}; + } + const Hdf5Handle space{space_id, H5Sclose}; + if (H5Sget_simple_extent_ndims(space.Get()) != 1) { + throw std::runtime_error{"stress_s11 must be a flat row dataset."}; + } + hsize_t count = 0U; + if (H5Sget_simple_extent_dims(space.Get(), &count, nullptr) < 0) { + throw std::runtime_error{"Unable to read stress_s11 extent."}; + } + return static_cast(count); } -} // namespace +} // namespace -TEST(B33ReferenceComparison, - GeneratesAuthoritativeHdf5AndComparisonEvidence) { - const std::filesystem::path sourceRoot{FESA_TEST_SOURCE_DIR}; - const std::filesystem::path binaryRoot{FESA_TEST_BINARY_DIR}; - const auto referenceDirectory = - sourceRoot / "reference" / "cantilever beam"; - const auto input = referenceDirectory / "cantilever beam.inp"; - const auto outputDirectory = - binaryRoot / "reference" / "cantilever-beam-b33"; - const auto results = outputDirectory / "results.h5"; - const auto comparison = outputDirectory / "comparison.json"; +TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) { + const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR}; + const std::filesystem::path binary_root{FESA_TEST_BINARY_DIR}; + const auto reference_directory = + source_root / "reference" / "cantilever beam"; + const auto input = reference_directory / "cantilever beam.inp"; + const auto output_directory = + binary_root / "reference" / "cantilever-beam-b33"; + const auto results = output_directory / "results.h5"; + const auto comparison = output_directory / "comparison.json"; - // Only the exact build-local evidence directory is reset; the approved - // reference tree is snapshotted and subsequently opened read-only. - std::error_code error; - std::filesystem::remove_all(outputDirectory, error); - error.clear(); - ASSERT_TRUE(std::filesystem::create_directories(outputDirectory, error)); - ASSERT_FALSE(error); - const auto referenceBefore = snapshotTree(referenceDirectory); - ASSERT_EQ(referenceBefore.size(), 4U); - EXPECT_EQ( - referenceBefore[0U].relativePath, - std::filesystem::path{"cantilever beam displacements.csv"}); - EXPECT_EQ( - referenceBefore[1U].relativePath, - std::filesystem::path{"cantilever beam elemental forces.csv"}); - EXPECT_EQ( - referenceBefore[2U].relativePath, - std::filesystem::path{"cantilever beam reactions.csv"}); - EXPECT_EQ( - referenceBefore[3U].relativePath, - std::filesystem::path{"cantilever beam.inp"}); + // Only the exact build-local evidence directory is reset; the approved + // reference tree is snapshotted and subsequently opened read-only. + std::error_code error; + std::filesystem::remove_all(output_directory, error); + error.clear(); + ASSERT_TRUE(std::filesystem::create_directories(output_directory, error)); + ASSERT_FALSE(error); + const auto reference_before = SnapshotTree(reference_directory); + ASSERT_EQ(reference_before.size(), 4U); + EXPECT_EQ(reference_before[0U].relative_path, + std::filesystem::path{"cantilever beam displacements.csv"}); + EXPECT_EQ(reference_before[1U].relative_path, + std::filesystem::path{"cantilever beam elemental forces.csv"}); + EXPECT_EQ(reference_before[2U].relative_path, + std::filesystem::path{"cantilever beam reactions.csv"}); + EXPECT_EQ(reference_before[3U].relative_path, + std::filesystem::path{"cantilever beam.inp"}); - fesa::FesaApplication application; - // FesaApplication receives application operands/options; main strips argv[0]. - ASSERT_EQ( - application.run( - {input.string(), "--output", results.string()}), - 0); - ASSERT_TRUE(std::filesystem::is_regular_file(results)); - ASSERT_GT(H5Fis_hdf5(results.string().c_str()), 0); - EXPECT_GT(stressRowCount(results), 0U); + fesa::FesaApplication application; + // FesaApplication receives application operands/options; main strips argv[0]. + ASSERT_EQ(application.Run({input.string(), "--output", results.string()}), 0); + ASSERT_TRUE(std::filesystem::is_regular_file(results)); + ASSERT_GT(H5Fis_hdf5(results.string().c_str()), 0); + EXPECT_GT(StressRowCount(results), 0U); - auto comparisonResult = fesa::test::ReferenceComparison::compare( - results, referenceDirectory); - ASSERT_TRUE(comparisonResult.HasValue()); - const auto& report = comparisonResult.Value(); - ASSERT_TRUE(report.passed); - ASSERT_EQ(report.rows.size(), kExpectedRowCount); - ASSERT_EQ(report.metrics.size(), kExpectedMetricCount); - EXPECT_TRUE(std::all_of( - report.rows.begin(), - report.rows.end(), - [](const fesa::test::RowDecision& row) { - return row.passed && std::isfinite(row.absoluteError) && - std::isfinite(row.tolerance) && row.tolerance > 0.0 && - row.fesa.modelId == "cantilever-beam-b33" && - row.reference.modelId == "cantilever-beam-b33" && - row.fesa.stepName == "Step-1" && - row.reference.stepName == "Step-1" && - row.fesa.frameIndex == 0U && - row.reference.frameIndex == 0U && - row.fesa.instanceName == "PART-1_1-1" && - row.reference.instanceName == "PART-1_1-1" && - !row.fesa.hdf5DatasetPath.empty(); - })); - EXPECT_TRUE(std::all_of( - report.metrics.begin(), - report.metrics.end(), - [](const fesa::test::ComponentMetrics& metric) { - return std::isfinite(metric.referenceScale) && - std::isfinite(metric.maximumAbsoluteError) && - std::isfinite(metric.maximumNormalizedError) && - std::isfinite(metric.rmsError) && - std::isfinite(metric.normError) && - metric.maximumNormalizedError <= 1.0; - })); + auto comparison_result = + fesa::test::ReferenceComparison::Compare(results, reference_directory); + ASSERT_TRUE(comparison_result.HasValue()); + const auto& report = comparison_result.Value(); + ASSERT_TRUE(report.passed); + ASSERT_EQ(report.rows.size(), kExpectedRowCount); + ASSERT_EQ(report.metrics.size(), kExpectedMetricCount); + EXPECT_TRUE(std::all_of( + report.rows.begin(), report.rows.end(), + [](const fesa::test::RowDecision& row) { + return row.passed && std::isfinite(row.absolute_error) && + std::isfinite(row.tolerance) && row.tolerance > 0.0 && + row.fesa.model_id == "cantilever-beam-b33" && + row.reference.model_id == "cantilever-beam-b33" && + row.fesa.step_name == "Step-1" && + row.reference.step_name == "Step-1" && + row.fesa.frame_index == 0U && row.reference.frame_index == 0U && + row.fesa.instance_name == "PART-1_1-1" && + row.reference.instance_name == "PART-1_1-1" && + !row.fesa.hdf5_dataset_path.empty(); + })); + EXPECT_TRUE( + std::all_of(report.metrics.begin(), report.metrics.end(), + [](const fesa::test::ComponentMetrics& metric) { + return std::isfinite(metric.reference_scale) && + std::isfinite(metric.maximum_absolute_error) && + std::isfinite(metric.maximum_normalized_error) && + std::isfinite(metric.rms_error) && + std::isfinite(metric.norm_error) && + metric.maximum_normalized_error <= 1.0; + })); - EXPECT_FALSE(report.stressComparisonApplicable); - EXPECT_NE(report.stressComparisonReason.find("N/A"), std::string::npos); - EXPECT_NE(report.stressComparisonReason.find("HDF5"), std::string::npos); - EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed); - EXPECT_TRUE(std::isfinite(report.physicsEvidence.freeResidualNorm)); - EXPECT_LE(report.physicsEvidence.freeResidualNorm, 1.0e-3); - EXPECT_LE( - norm(sum( - report.physicsEvidence.appliedForce, - report.physicsEvidence.reactionForce)), - 1.0e-3); - EXPECT_LE( - norm(sum( - report.physicsEvidence.appliedMomentAboutOrigin, - report.physicsEvidence.reactionMomentAboutOrigin)), - 1.0e-2); + EXPECT_FALSE(report.stress_comparison_applicable); + EXPECT_NE(report.stress_comparison_reason.find("N/A"), std::string::npos); + EXPECT_NE(report.stress_comparison_reason.find("HDF5"), std::string::npos); + EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed); + EXPECT_TRUE(std::isfinite(report.physics_evidence.free_residual_norm)); + EXPECT_LE(report.physics_evidence.free_residual_norm, 1.0e-3); + EXPECT_LE(Norm(Sum(report.physics_evidence.applied_force, + report.physics_evidence.reaction_force)), + 1.0e-3); + EXPECT_LE(Norm(Sum(report.physics_evidence.applied_moment_about_origin, + report.physics_evidence.reaction_moment_about_origin)), + 1.0e-2); - ASSERT_TRUE( - fesa::test::ReferenceComparison::writeDeterministicJson( - report, comparison) - .IsOk()); - ASSERT_TRUE(std::filesystem::is_regular_file(comparison)); - const std::string json = readBytes(comparison); - EXPECT_NE(json.find("\"stress_comparison_applicable\":false"), - std::string::npos); - EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos); + ASSERT_TRUE(fesa::test::ReferenceComparison::WriteDeterministicJson( + report, comparison) + .IsOk()); + ASSERT_TRUE(std::filesystem::is_regular_file(comparison)); + const std::string json = ReadBytes(comparison); + EXPECT_NE(json.find("\"stress_comparison_applicable\":false"), + std::string::npos); + EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos); - std::vector generatedNames; - for (const auto& entry : - std::filesystem::directory_iterator{outputDirectory}) { - generatedNames.push_back(entry.path().filename().string()); - } - std::sort(generatedNames.begin(), generatedNames.end()); - EXPECT_EQ( - generatedNames, - (std::vector{"comparison.json", "results.h5"})); + std::vector generated_names; + for (const auto& entry : + std::filesystem::directory_iterator{output_directory}) { + generated_names.push_back(entry.path().filename().string()); + } + std::sort(generated_names.begin(), generated_names.end()); + EXPECT_EQ(generated_names, + (std::vector{"comparison.json", "results.h5"})); - expectTreeUnchanged(referenceBefore, snapshotTree(referenceDirectory)); + ExpectTreeUnchanged(reference_before, SnapshotTree(reference_directory)); } diff --git a/tests/reference/mitc4_reference_cases_test.cpp b/tests/reference/mitc4_reference_cases_test.cpp index ad6db0f..b078cf6 100644 --- a/tests/reference/mitc4_reference_cases_test.cpp +++ b/tests/reference/mitc4_reference_cases_test.cpp @@ -1,7 +1,3 @@ -#include "mitc4_reference_comparison.hpp" - -#include "fesa/app/fesa_application.hpp" - #include #include @@ -14,6 +10,9 @@ #include #include +#include "fesa/app/fesa_application.h" +#include "mitc4_reference_comparison.h" + #ifndef FESA_TEST_SOURCE_DIR #error FESA_TEST_SOURCE_DIR must identify the repository root. #endif @@ -30,150 +29,139 @@ constexpr const char* kIntegrationRule = constexpr std::size_t kNodeCount = 49U; constexpr std::size_t kComponentCount = 6U; -std::string readBytes(const std::filesystem::path& path) { - std::ifstream stream{path, std::ios::binary}; - if (!stream) { - throw std::runtime_error{"Unable to read declared reference artifact."}; - } - return {std::istreambuf_iterator{stream}, - std::istreambuf_iterator{}}; +std::string ReadBytes(const std::filesystem::path& path) { + std::ifstream stream{path, std::ios::binary}; + if (!stream) { + throw std::runtime_error{"Unable to read declared reference artifact."}; + } + return {std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; } struct ArtifactSnapshot { - std::string bytes; - std::filesystem::file_time_type lastWriteTime; + std::string bytes; + std::filesystem::file_time_type last_write_time; }; -ArtifactSnapshot snapshot(const std::filesystem::path& path) { - return {readBytes(path), std::filesystem::last_write_time(path)}; +ArtifactSnapshot Snapshot(const std::filesystem::path& path) { + return {ReadBytes(path), std::filesystem::last_write_time(path)}; } -void expectUnchanged( - const std::filesystem::path& path, const ArtifactSnapshot& before) { - EXPECT_EQ(readBytes(path), before.bytes) << path.string(); - EXPECT_EQ(std::filesystem::last_write_time(path), before.lastWriteTime) - << path.string(); +void ExpectUnchanged(const std::filesystem::path& path, + const ArtifactSnapshot& before) { + EXPECT_EQ(ReadBytes(path), before.bytes) << path.string(); + EXPECT_EQ(std::filesystem::last_write_time(path), before.last_write_time) + << path.string(); } struct CaseEvidence { - fesa::test::Mitc4ComparisonReport report; - std::filesystem::path comparisonJson; + fesa::test::Mitc4ComparisonReport report; + std::filesystem::path comparison_json; }; -CaseEvidence runCase( - const std::string& caseId, - const std::string& sourceElementType, - const std::filesystem::path& referenceDirectory, - const std::filesystem::path& input, - const std::filesystem::path& csv, - const std::string& outputName) { - const auto inputBefore = snapshot(input); - const auto csvBefore = snapshot(csv); - const std::filesystem::path outputDirectory = - std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / outputName; - std::error_code error; - std::filesystem::remove_all(outputDirectory, error); - error.clear(); - if (!std::filesystem::create_directories(outputDirectory, error) || error) { - throw std::runtime_error{"Unable to create MITC4 evidence directory."}; - } - const auto results = outputDirectory / "results.h5"; - const auto comparison = outputDirectory / "comparison.json"; +CaseEvidence RunCase(const std::string& case_id, + const std::string& source_element_type, + const std::filesystem::path& reference_directory, + const std::filesystem::path& input, + const std::filesystem::path& csv, + const std::string& output_name) { + const auto input_before = Snapshot(input); + const auto csv_before = Snapshot(csv); + const std::filesystem::path output_directory = + std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / output_name; + std::error_code error; + std::filesystem::remove_all(output_directory, error); + error.clear(); + if (!std::filesystem::create_directories(output_directory, error) || error) { + throw std::runtime_error{"Unable to create MITC4 evidence directory."}; + } + const auto results = output_directory / "results.h5"; + const auto comparison = output_directory / "comparison.json"; - fesa::FesaApplication application; - EXPECT_EQ( - application.run({input.string(), "--output", results.string()}), 0); - EXPECT_TRUE(std::filesystem::is_regular_file(results)); - auto comparisonResult = fesa::test::Mitc4ReferenceComparison::compare( - {caseId, sourceElementType, input, csv, results}); - if (!comparisonResult.HasValue()) { - std::string diagnostics; - for (const auto& diagnostic : - comparisonResult.GetStatus().Diagnostics()) { - diagnostics += "\n" + diagnostic.code + ": " + diagnostic.message; - } - ADD_FAILURE() << "MITC4 comparison precheck failed for " << caseId - << diagnostics; - return {{}, comparison}; + fesa::FesaApplication application; + EXPECT_EQ(application.Run({input.string(), "--output", results.string()}), 0); + EXPECT_TRUE(std::filesystem::is_regular_file(results)); + auto comparison_result = fesa::test::Mitc4ReferenceComparison::Compare( + {case_id, source_element_type, input, csv, results}); + if (!comparison_result.HasValue()) { + std::string diagnostics; + for (const auto& diagnostic : comparison_result.GetStatus().Diagnostics()) { + diagnostics += "\n" + diagnostic.code + ": " + diagnostic.message; } - EXPECT_TRUE( - fesa::test::Mitc4ReferenceComparison::writeDeterministicJson( - comparisonResult.Value(), comparison) - .IsOk()); - EXPECT_TRUE(std::filesystem::is_regular_file(comparison)); + ADD_FAILURE() << "MITC4 comparison precheck failed for " << case_id + << diagnostics; + return {{}, comparison}; + } + EXPECT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson( + comparison_result.Value(), comparison) + .IsOk()); + EXPECT_TRUE(std::filesystem::is_regular_file(comparison)); - std::vector generated; - for (const auto& entry : - std::filesystem::directory_iterator{outputDirectory}) { - generated.push_back(entry.path().filename().string()); - } - std::sort(generated.begin(), generated.end()); - EXPECT_EQ( - generated, - (std::vector{"comparison.json", "results.h5"})); - EXPECT_TRUE(std::filesystem::is_directory(referenceDirectory)); - expectUnchanged(input, inputBefore); - expectUnchanged(csv, csvBefore); - return {std::move(comparisonResult.Value()), comparison}; + std::vector generated; + for (const auto& entry : + std::filesystem::directory_iterator{output_directory}) { + generated.push_back(entry.path().filename().string()); + } + std::sort(generated.begin(), generated.end()); + EXPECT_EQ(generated, + (std::vector{"comparison.json", "results.h5"})); + EXPECT_TRUE(std::filesystem::is_directory(reference_directory)); + ExpectUnchanged(input, input_before); + ExpectUnchanged(csv, csv_before); + return {std::move(comparison_result.Value()), comparison}; } -void expectCommonMetadata( - const fesa::test::Mitc4ComparisonReport& report, - const std::string& caseId, - const std::string& sourceElementType) { - EXPECT_EQ(report.caseId, caseId); - EXPECT_EQ(report.sourceElementType, sourceElementType); - EXPECT_EQ(report.internalFormulation, kInternalFormulation); - EXPECT_EQ(report.integrationRule, kIntegrationRule); +void ExpectCommonMetadata(const fesa::test::Mitc4ComparisonReport& report, + const std::string& case_id, + const std::string& source_element_type) { + EXPECT_EQ(report.case_id, case_id); + EXPECT_EQ(report.source_element_type, source_element_type); + EXPECT_EQ(report.internal_formulation, kInternalFormulation); + EXPECT_EQ(report.integration_rule, kIntegrationRule); } -void expectComparisonCoverage( - const fesa::test::Mitc4ComparisonReport& report) { - ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount); - ASSERT_EQ(report.metrics.size(), kComponentCount); - ASSERT_EQ(report.vectorMetrics.size(), kNodeCount); - EXPECT_TRUE(report.passed); - const std::size_t blockingRows = static_cast(std::count_if( - report.rows.begin(), report.rows.end(), - [](const fesa::test::Mitc4RowDecision& row) { - return row.blocking; - })); - const std::size_t rotationRows = report.rows.size() - blockingRows; - EXPECT_EQ(blockingRows, kNodeCount * 3U); - EXPECT_EQ(rotationRows, kNodeCount * 3U); - EXPECT_TRUE(std::all_of( - report.rows.begin(), report.rows.end(), - [](const fesa::test::Mitc4RowDecision& row) { - return !row.blocking || row.withinTolerance; - })); - EXPECT_EQ( - report.warnings.size(), - static_cast(std::count_if( - report.rows.begin(), report.rows.end(), - [](const fesa::test::Mitc4RowDecision& row) { - return !row.blocking && !row.withinTolerance; - }))); +void ExpectComparisonCoverage(const fesa::test::Mitc4ComparisonReport& report) { + ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount); + ASSERT_EQ(report.metrics.size(), kComponentCount); + ASSERT_EQ(report.vector_metrics.size(), kNodeCount); + EXPECT_TRUE(report.passed); + const std::size_t blocking_rows = static_cast(std::count_if( + report.rows.begin(), report.rows.end(), + [](const fesa::test::Mitc4RowDecision& row) { return row.blocking; })); + const std::size_t rotation_rows = report.rows.size() - blocking_rows; + EXPECT_EQ(blocking_rows, kNodeCount * 3U); + EXPECT_EQ(rotation_rows, kNodeCount * 3U); + EXPECT_TRUE(std::all_of(report.rows.begin(), report.rows.end(), + [](const fesa::test::Mitc4RowDecision& row) { + return !row.blocking || row.within_tolerance; + })); + EXPECT_EQ(report.warnings.size(), + static_cast( + std::count_if(report.rows.begin(), report.rows.end(), + [](const fesa::test::Mitc4RowDecision& row) { + return !row.blocking && !row.within_tolerance; + }))); } // MITC4-E2E-S4-001 TEST(Mitc4S4Reference, PreservesS4AndWritesCommonMitc4Metadata) { - const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; - const auto directory = root / "reference" / "shell"; - const auto evidence = runCase( - "shell-s4", "S4", directory, directory / "shell.inp", - directory / "shell displacements.csv", "mitc4-shell-s4-metadata"); - expectCommonMetadata(evidence.report, "shell-s4", "S4"); + const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; + const auto directory = root / "reference" / "shell"; + const auto evidence = + RunCase("shell-s4", "S4", directory, directory / "shell.inp", + directory / "shell displacements.csv", "mitc4-shell-s4-metadata"); + ExpectCommonMetadata(evidence.report, "shell-s4", "S4"); } // MITC4-E2E-S4-002 TEST(Mitc4S4Reference, PassesBlockingUAndReportsEveryUrRow) { - const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; - const auto directory = root / "reference" / "shell"; - const auto evidence = runCase( - "shell-s4", "S4", directory, directory / "shell.inp", - directory / "shell displacements.csv", "mitc4-shell-s4-comparison"); - expectCommonMetadata(evidence.report, "shell-s4", "S4"); - expectComparisonCoverage(evidence.report); + const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; + const auto directory = root / "reference" / "shell"; + const auto evidence = RunCase( + "shell-s4", "S4", directory, directory / "shell.inp", + directory / "shell displacements.csv", "mitc4-shell-s4-comparison"); + ExpectCommonMetadata(evidence.report, "shell-s4", "S4"); + ExpectComparisonCoverage(evidence.report); } -} // namespace +} // namespace diff --git a/tests/reference/mitc4_reference_comparison.cpp b/tests/reference/mitc4_reference_comparison.cpp index 3be97f6..3aa44e1 100644 --- a/tests/reference/mitc4_reference_comparison.cpp +++ b/tests/reference/mitc4_reference_comparison.cpp @@ -1,11 +1,11 @@ -#include "mitc4_reference_comparison.hpp" +#include "mitc4_reference_comparison.h" #include #include #include -#include #include +#include #include #include #include @@ -33,914 +33,877 @@ constexpr const char* kInternalFormulation = "FESA-MITC4"; constexpr const char* kIntegrationRule = "2x2x2-gauss; mitc4-edge-midpoint-shear"; constexpr double kFixedAbsoluteTolerance = 1.0e-5; -constexpr std::array kComponents{ - "U1", "U2", "U3", "UR1", "UR2", "UR3"}; -const std::vector kExpectedHeader{ - "Part Instance Name", "Node Label", "U-U1", "U-U2", "U-U3", - "UR-UR1", "UR-UR2", "UR-UR3"}; +constexpr std::array kComponents{"U1", "U2", "U3", + "UR1", "UR2", "UR3"}; +const std::vector expected_header{"Part Instance Name", + "Node Label", + "U-U1", + "U-U2", + "U-U3", + "UR-UR1", + "UR-UR2", + "UR-UR3"}; class ComparisonFailure final : public std::runtime_error { -public: - ComparisonFailure(std::string code, std::string message) - : std::runtime_error{std::move(message)}, code_{std::move(code)} {} + public: + ComparisonFailure(std::string code, std::string message) + : std::runtime_error{std::move(message)}, code_{std::move(code)} {} - const std::string& code() const noexcept { return code_; } + const std::string& Code() const noexcept { return code_; } -private: - std::string code_; + private: + std::string code_; }; -[[noreturn]] void fail(const std::string& code, const std::string& message) { - throw ComparisonFailure{code, message}; +[[noreturn]] void Fail(const std::string& code, const std::string& message) { + throw ComparisonFailure{code, message}; } -Status failureStatus( - const std::string& caseId, - const std::string& code, - const std::string& message) { - return Status::Failure( - FailureCategory::kModel, - {{Severity::kError, code, {}, "", caseId, message}}); +Status FailureStatus(const std::string& case_id, const std::string& code, + const std::string& message) { + return Status::Failure(FailureCategory::kModel, + {{Severity::kError, code, {}, "", case_id, message}}); } -std::string trim(const std::string& value) { - const auto isSpace = [](const unsigned char character) { - return std::isspace(character) != 0; - }; - const auto begin = std::find_if_not( - value.begin(), value.end(), [&](const char character) { - return isSpace(static_cast(character)); - }); - const auto end = std::find_if_not( - value.rbegin(), value.rend(), [&](const char character) { - return isSpace(static_cast(character)); - }).base(); - return begin < end ? std::string{begin, end} : std::string{}; +std::string Trim(const std::string& value) { + const auto is_space = [](const unsigned char character) { + return std::isspace(character) != 0; + }; + const auto begin = + std::find_if_not(value.begin(), value.end(), [&](const char character) { + return is_space(static_cast(character)); + }); + const auto end = + std::find_if_not(value.rbegin(), value.rend(), [&](const char character) { + return is_space(static_cast(character)); + }).base(); + return begin < end ? std::string{begin, end} : std::string{}; } -std::vector splitCsvLine(const std::string& line) { - std::vector fields; - std::size_t start = 0U; - while (true) { - const std::size_t comma = line.find(',', start); - fields.push_back(trim(line.substr(start, comma - start))); - if (comma == std::string::npos) { - break; - } - start = comma + 1U; +std::vector SplitCsvLine(const std::string& line) { + std::vector fields; + std::size_t start = 0U; + while (true) { + const std::size_t comma = line.find(',', start); + fields.push_back(Trim(line.substr(start, comma - start))); + if (comma == std::string::npos) { + break; } - return fields; + start = comma + 1U; + } + return fields; } -std::int64_t parsePositiveLabel(const std::string& field) { - std::int64_t value = 0; - const char* const begin = field.data(); - const char* const end = begin + field.size(); - const auto parsed = std::from_chars(begin, end, value); - if (parsed.ec != std::errc{} || parsed.ptr != end || value <= 0) { - fail("schema-mismatch", "A source-node label is invalid."); - } - return value; +std::int64_t ParsePositiveLabel(const std::string& field) { + std::int64_t value = 0; + const char* const begin = field.data(); + const char* const end = begin + field.size(); + const auto parsed = std::from_chars(begin, end, value); + if (parsed.ec != std::errc{} || parsed.ptr != end || value <= 0) { + Fail("schema-mismatch", "A source-node label is invalid."); + } + return value; } -double parseFiniteDouble(const std::string& field) { - if (field.empty()) { - fail("schema-mismatch", "A displacement CSV numeric field is empty."); - } - errno = 0; - char* end = nullptr; - const double value = std::strtod(field.c_str(), &end); - if (errno == ERANGE || end == field.c_str() || end == nullptr || - *end != '\0' || !std::isfinite(value)) { - fail( - "schema-mismatch", - "A displacement CSV numeric field is invalid or nonfinite."); - } - return value; +double ParseFiniteDouble(const std::string& field) { + if (field.empty()) { + Fail("schema-mismatch", "A displacement CSV numeric field is empty."); + } + errno = 0; + char* end = nullptr; + const double value = std::strtod(field.c_str(), &end); + if (errno == ERANGE || end == field.c_str() || end == nullptr || + *end != '\0' || !std::isfinite(value)) { + Fail("schema-mismatch", + "A displacement CSV numeric field is invalid or nonfinite."); + } + return value; } -std::string uppercaseAscii(std::string value) { - std::transform( - value.begin(), value.end(), value.begin(), [](const char character) { - return character >= 'a' && character <= 'z' - ? static_cast(character - 'a' + 'A') - : character; - }); - return value; +std::string UppercaseAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](const char character) { + return character >= 'a' && character <= 'z' + ? static_cast(character - 'a' + 'A') + : character; + }); + return value; } struct IdentityKey { - std::string instanceName; - std::int64_t sourceNodeLabel; + std::string instance_name; + std::int64_t source_node_label; - bool operator<(const IdentityKey& other) const { - const std::string normalizedInstance = uppercaseAscii(instanceName); - const std::string normalizedOther = uppercaseAscii(other.instanceName); - if (normalizedInstance != normalizedOther) { - return normalizedInstance < normalizedOther; - } - return sourceNodeLabel < other.sourceNodeLabel; + bool operator<(const IdentityKey& other) const { + const std::string normalized_instance = UppercaseAscii(instance_name); + const std::string normalized_other = UppercaseAscii(other.instance_name); + if (normalized_instance != normalized_other) { + return normalized_instance < normalized_other; } + return source_node_label < other.source_node_label; + } }; struct WideRow { - IdentityKey identity; - std::array values; + IdentityKey identity; + std::array values; }; -std::vector readReferenceCsv(const std::filesystem::path& path) { - std::ifstream stream{path}; - if (!stream) { - fail( - "needs-reference-artifacts", - "The declared displacement CSV is missing or unreadable."); - } - std::string line; - if (!std::getline(stream, line)) { - fail("schema-mismatch", "The declared displacement CSV is empty."); - } - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - if (splitCsvLine(line) != kExpectedHeader) { - fail( - "schema-mismatch", - "The displacement CSV header does not match the six-component contract."); - } +std::vector ReadReferenceCsv(const std::filesystem::path& path) { + std::ifstream stream{path}; + if (!stream) { + Fail("needs-reference-artifacts", + "The declared displacement CSV is missing or unreadable."); + } + std::string line; + if (!std::getline(stream, line)) { + Fail("schema-mismatch", "The declared displacement CSV is empty."); + } + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (SplitCsvLine(line) != expected_header) { + Fail("schema-mismatch", + "The displacement CSV header does not match the six-component " + "contract."); + } - std::vector rows; - std::map identities; - while (std::getline(stream, line)) { - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - if (line.empty()) { - fail("schema-mismatch", "Blank displacement CSV rows are not allowed."); - } - const auto fields = splitCsvLine(line); - if (fields.size() != kExpectedHeader.size() || fields[0U].empty()) { - fail("schema-mismatch", "A displacement CSV row has invalid schema."); - } - WideRow row{}; - row.identity.instanceName = fields[0U]; - row.identity.sourceNodeLabel = parsePositiveLabel(fields[1U]); - for (std::size_t component = 0U; - component < row.values.size(); - ++component) { - row.values[component] = parseFiniteDouble(fields[component + 2U]); - } - if (!identities.emplace(row.identity, rows.size()).second) { - fail("schema-mismatch", "A displacement CSV row identity is duplicated."); - } - rows.push_back(std::move(row)); + std::vector rows; + std::map identities; + while (std::getline(stream, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); } - if (rows.empty()) { - fail("schema-mismatch", "The displacement CSV contains no data rows."); + if (line.empty()) { + Fail("schema-mismatch", "Blank displacement CSV rows are not allowed."); } - return rows; + const auto fields = SplitCsvLine(line); + if (fields.size() != expected_header.size() || fields[0U].empty()) { + Fail("schema-mismatch", "A displacement CSV row has invalid schema."); + } + WideRow row{}; + row.identity.instance_name = fields[0U]; + row.identity.source_node_label = ParsePositiveLabel(fields[1U]); + for (std::size_t component = 0U; component < row.values.size(); + ++component) { + row.values[component] = ParseFiniteDouble(fields[component + 2U]); + } + if (!identities.emplace(row.identity, rows.size()).second) { + Fail("schema-mismatch", "A displacement CSV row identity is duplicated."); + } + rows.push_back(std::move(row)); + } + if (rows.empty()) { + Fail("schema-mismatch", "The displacement CSV contains no data rows."); + } + return rows; } class Hdf5Handle { -public: - using Closer = herr_t (*)(hid_t); + public: + using Closer = herr_t (*)(hid_t); - Hdf5Handle() = default; - Hdf5Handle(const hid_t value, Closer closer) - : value_{value}, closer_{closer} {} - Hdf5Handle(const Hdf5Handle&) = delete; - Hdf5Handle& operator=(const Hdf5Handle&) = delete; - Hdf5Handle(Hdf5Handle&& other) noexcept - : value_{other.value_}, closer_{other.closer_} { - other.value_ = -1; - other.closer_ = nullptr; - } - ~Hdf5Handle() { - if (value_ >= 0 && closer_ != nullptr) { - (void)closer_(value_); - } + Hdf5Handle() = default; + Hdf5Handle(const hid_t value, Closer closer) + : value_{value}, closer_{closer} {} + Hdf5Handle(const Hdf5Handle&) = delete; + Hdf5Handle& operator=(const Hdf5Handle&) = delete; + Hdf5Handle(Hdf5Handle&& other) noexcept + : value_{other.value_}, closer_{other.closer_} { + other.value_ = -1; + other.closer_ = nullptr; + } + ~Hdf5Handle() { + if (value_ >= 0 && closer_ != nullptr) { + (void)closer_(value_); } + } - hid_t get() const noexcept { return value_; } + hid_t Get() const noexcept { return value_; } -private: - hid_t value_{-1}; - Closer closer_{nullptr}; + private: + hid_t value_{-1}; + Closer closer_{nullptr}; }; class Hdf5ErrorSilencer { -public: - Hdf5ErrorSilencer() { - if (H5Eget_auto2(H5E_DEFAULT, &callback_, &clientData_) >= 0 && - H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { - active_ = true; - } + public: + Hdf5ErrorSilencer() { + if (H5Eget_auto2(H5E_DEFAULT, &callback_, &client_data_) >= 0 && + H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { + active_ = true; } - Hdf5ErrorSilencer(const Hdf5ErrorSilencer&) = delete; - Hdf5ErrorSilencer& operator=(const Hdf5ErrorSilencer&) = delete; - ~Hdf5ErrorSilencer() { - if (active_) { - (void)H5Eset_auto2(H5E_DEFAULT, callback_, clientData_); - } + } + Hdf5ErrorSilencer(const Hdf5ErrorSilencer&) = delete; + Hdf5ErrorSilencer& operator=(const Hdf5ErrorSilencer&) = delete; + ~Hdf5ErrorSilencer() { + if (active_) { + (void)H5Eset_auto2(H5E_DEFAULT, callback_, client_data_); } + } -private: - H5E_auto2_t callback_{nullptr}; - void* clientData_{nullptr}; - bool active_{false}; + private: + H5E_auto2_t callback_{nullptr}; + void* client_data_{nullptr}; + bool active_{false}; }; class Hdf5VlenReclaimer { -public: - Hdf5VlenReclaimer( - const hid_t memoryType, - const hid_t dataSpace, - void* const data) noexcept - : memoryType_{memoryType}, dataSpace_{dataSpace}, data_{data} {} - Hdf5VlenReclaimer(const Hdf5VlenReclaimer&) = delete; - Hdf5VlenReclaimer& operator=(const Hdf5VlenReclaimer&) = delete; - ~Hdf5VlenReclaimer() { - if (active_) { - (void)H5Dvlen_reclaim( - memoryType_, dataSpace_, H5P_DEFAULT, data_); - } + public: + Hdf5VlenReclaimer(const hid_t memory_type, const hid_t data_space, + void* const data) noexcept + : memory_type_{memory_type}, data_space_{data_space}, data_{data} {} + Hdf5VlenReclaimer(const Hdf5VlenReclaimer&) = delete; + Hdf5VlenReclaimer& operator=(const Hdf5VlenReclaimer&) = delete; + ~Hdf5VlenReclaimer() { + if (active_) { + (void)H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_); } + } - void reclaim() { - active_ = false; - if (H5Dvlen_reclaim(memoryType_, dataSpace_, H5P_DEFAULT, data_) < 0) { - fail("schema-mismatch", "Unable to reclaim HDF5 variable strings."); - } + void Reclaim() { + active_ = false; + if (H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_) < 0) { + Fail("schema-mismatch", "Unable to reclaim HDF5 variable strings."); } + } -private: - hid_t memoryType_; - hid_t dataSpace_; - void* data_; - bool active_{true}; + private: + hid_t memory_type_; + hid_t data_space_; + void* data_; + bool active_{true}; }; -hid_t requireId(const hid_t value, const char* message) { - if (value < 0) { - fail("schema-mismatch", message); - } - return value; +hid_t RequireId(const hid_t value, const char* message) { + if (value < 0) { + Fail("schema-mismatch", message); + } + return value; } -void requireHdf(const herr_t value, const char* message) { - if (value < 0) { - fail("schema-mismatch", message); - } +void RequireHdf(const herr_t value, const char* message) { + if (value < 0) { + Fail("schema-mismatch", message); + } } -Hdf5Handle makeUtf8StringType() { - Hdf5Handle type{ - requireId(H5Tcopy(H5T_C_S1), "Unable to copy an HDF5 string type."), - H5Tclose}; - requireHdf( - H5Tset_size(type.get(), H5T_VARIABLE), - "Unable to define an HDF5 variable string type."); - requireHdf( - H5Tset_cset(type.get(), H5T_CSET_UTF8), - "Unable to define an HDF5 UTF-8 string type."); - return type; +Hdf5Handle MakeUtf8StringType() { + Hdf5Handle type{ + RequireId(H5Tcopy(H5T_C_S1), "Unable to copy an HDF5 string type."), + H5Tclose}; + RequireHdf(H5Tset_size(type.Get(), H5T_VARIABLE), + "Unable to define an HDF5 variable string type."); + RequireHdf(H5Tset_cset(type.Get(), H5T_CSET_UTF8), + "Unable to define an HDF5 UTF-8 string type."); + return type; } -Hdf5Handle openDataset(const hid_t file, const char* path) { - return { - requireId( - H5Dopen2(file, path, H5P_DEFAULT), - "A required HDF5 dataset is missing."), - H5Dclose}; +Hdf5Handle OpenDataset(const hid_t file, const char* path) { + return {RequireId(H5Dopen2(file, path, H5P_DEFAULT), + "A required HDF5 dataset is missing."), + H5Dclose}; } -std::vector dimensions(const hid_t dataset) { - Hdf5Handle space{ - requireId(H5Dget_space(dataset), "Unable to inspect HDF5 dimensions."), - H5Sclose}; - const int rank = H5Sget_simple_extent_ndims(space.get()); - if (rank < 0) { - fail("schema-mismatch", "Unable to inspect HDF5 rank."); - } - std::vector result(static_cast(rank)); - if (rank > 0) { - requireHdf( - H5Sget_simple_extent_dims(space.get(), result.data(), nullptr), - "Unable to inspect HDF5 extents."); - } - return result; +std::vector Dimensions(const hid_t dataset) { + Hdf5Handle space{ + RequireId(H5Dget_space(dataset), "Unable to inspect HDF5 dimensions."), + H5Sclose}; + const int rank = H5Sget_simple_extent_ndims(space.Get()); + if (rank < 0) { + Fail("schema-mismatch", "Unable to inspect HDF5 rank."); + } + std::vector result(static_cast(rank)); + if (rank > 0) { + RequireHdf(H5Sget_simple_extent_dims(space.Get(), result.data(), nullptr), + "Unable to inspect HDF5 extents."); + } + return result; } -std::string readStringAttribute(const hid_t object, const char* name) { - Hdf5Handle attribute{ - requireId( - H5Aopen(object, name, H5P_DEFAULT), - "A required HDF5 string attribute is missing."), - H5Aclose}; - Hdf5Handle type{ - requireId(H5Aget_type(attribute.get()), "Unable to inspect an attribute."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_STRING || - H5Tis_variable_str(type.get()) <= 0 || - H5Tget_cset(type.get()) != H5T_CSET_UTF8) { - fail("schema-mismatch", "An HDF5 string attribute has the wrong type."); - } - char* raw = nullptr; - requireHdf( - H5Aread(attribute.get(), type.get(), &raw), - "Unable to read an HDF5 string attribute."); - if (raw == nullptr) { - fail("schema-mismatch", "An HDF5 string attribute is null."); - } - const std::string result{raw}; - requireHdf(H5free_memory(raw), "Unable to free HDF5 attribute memory."); - return result; +std::string ReadStringAttribute(const hid_t object, const char* name) { + Hdf5Handle attribute{ + RequireId(H5Aopen(object, name, H5P_DEFAULT), + "A required HDF5 string attribute is missing."), + H5Aclose}; + Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()), + "Unable to inspect an attribute."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_STRING || + H5Tis_variable_str(type.Get()) <= 0 || + H5Tget_cset(type.Get()) != H5T_CSET_UTF8) { + Fail("schema-mismatch", "An HDF5 string attribute has the wrong type."); + } + char* raw = nullptr; + RequireHdf(H5Aread(attribute.Get(), type.Get(), &raw), + "Unable to read an HDF5 string attribute."); + if (raw == nullptr) { + Fail("schema-mismatch", "An HDF5 string attribute is null."); + } + const std::string result{raw}; + RequireHdf(H5free_memory(raw), "Unable to free HDF5 attribute memory."); + return result; } -std::uint64_t readUint64Attribute(const hid_t object, const char* name) { - Hdf5Handle attribute{ - requireId( - H5Aopen(object, name, H5P_DEFAULT), - "A required HDF5 integer attribute is missing."), - H5Aclose}; - Hdf5Handle type{ - requireId(H5Aget_type(attribute.get()), "Unable to inspect an attribute."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_INTEGER || - H5Tget_size(type.get()) != sizeof(std::uint64_t) || - H5Tget_sign(type.get()) != H5T_SGN_NONE) { - fail("schema-mismatch", "An HDF5 integer attribute has the wrong type."); - } - std::uint64_t result = 0U; - requireHdf( - H5Aread(attribute.get(), H5T_NATIVE_UINT64, &result), - "Unable to read an HDF5 integer attribute."); - return result; +std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) { + Hdf5Handle attribute{ + RequireId(H5Aopen(object, name, H5P_DEFAULT), + "A required HDF5 integer attribute is missing."), + H5Aclose}; + Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()), + "Unable to inspect an attribute."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_INTEGER || + H5Tget_size(type.Get()) != sizeof(std::uint64_t) || + H5Tget_sign(type.Get()) != H5T_SGN_NONE) { + Fail("schema-mismatch", "An HDF5 integer attribute has the wrong type."); + } + std::uint64_t result = 0U; + RequireHdf(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &result), + "Unable to read an HDF5 integer attribute."); + return result; } -void requireStringAttribute( - const hid_t object, const char* name, const std::string& expected) { - if (readStringAttribute(object, name) != expected) { - fail("schema-mismatch", "An HDF5 string attribute has the wrong value."); - } +void RequireStringAttribute(const hid_t object, const char* name, + const std::string& expected) { + if (ReadStringAttribute(object, name) != expected) { + Fail("schema-mismatch", "An HDF5 string attribute has the wrong value."); + } } -void requireExactCompoundMembers( - const hid_t dataset, const std::vector& expected) { - Hdf5Handle type{ - requireId(H5Dget_type(dataset), "Unable to inspect an HDF5 compound type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_COMPOUND || - H5Tget_nmembers(type.get()) != static_cast(expected.size())) { - fail("schema-mismatch", "An HDF5 compound schema has the wrong member count."); +void RequireExactCompoundMembers(const hid_t dataset, + const std::vector& expected) { + Hdf5Handle type{RequireId(H5Dget_type(dataset), + "Unable to inspect an HDF5 compound type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_COMPOUND || + H5Tget_nmembers(type.Get()) != static_cast(expected.size())) { + Fail("schema-mismatch", + "An HDF5 compound schema has the wrong member count."); + } + for (std::size_t index = 0U; index < expected.size(); ++index) { + char* name = H5Tget_member_name(type.Get(), static_cast(index)); + if (name == nullptr) { + Fail("schema-mismatch", "Unable to inspect an HDF5 compound member."); } - for (std::size_t index = 0U; index < expected.size(); ++index) { - char* name = H5Tget_member_name(type.get(), static_cast(index)); - if (name == nullptr) { - fail("schema-mismatch", "Unable to inspect an HDF5 compound member."); - } - const std::string actual{name}; - requireHdf(H5free_memory(name), "Unable to free HDF5 member memory."); - if (actual != expected[index]) { - fail("schema-mismatch", "An HDF5 compound member is out of contract order."); - } + const std::string actual{name}; + RequireHdf(H5free_memory(name), "Unable to free HDF5 member memory."); + if (actual != expected[index]) { + Fail("schema-mismatch", + "An HDF5 compound member is out of contract order."); } + } } struct NodeReadRow { - std::uint64_t internalNodeId; - char* instanceName; - char* sourceLabel; - double coordinates[3]; + std::uint64_t internal_node_id; + char* instance_name; + char* source_label; + double coordinates[3]; }; -std::vector readNodes(const hid_t file) { - auto dataset = openDataset(file, "/model/nodes"); - const auto shape = dimensions(dataset.get()); - if (shape.size() != 1U || shape[0U] == 0U || - shape[0U] > static_cast((std::numeric_limits::max)())) { - fail("schema-mismatch", "The HDF5 node dataset has an invalid shape."); - } - requireExactCompoundMembers( - dataset.get(), - {"internal_node_id", "instance_name", "source_label", "coordinates"}); - requireStringAttribute(dataset.get(), "coordinate_system", "global-cartesian"); - requireStringAttribute(dataset.get(), "units_label", "length"); +std::vector ReadNodes(const hid_t file) { + auto dataset = OpenDataset(file, "/model/nodes"); + const auto shape = Dimensions(dataset.Get()); + if (shape.size() != 1U || shape[0U] == 0U || + shape[0U] > + static_cast((std::numeric_limits::max)())) { + Fail("schema-mismatch", "The HDF5 node dataset has an invalid shape."); + } + RequireExactCompoundMembers( + dataset.Get(), + {"internal_node_id", "instance_name", "source_label", "coordinates"}); + RequireStringAttribute(dataset.Get(), "coordinate_system", + "global-cartesian"); + RequireStringAttribute(dataset.Get(), "units_label", "length"); - auto stringType = makeUtf8StringType(); - const hsize_t coordinateDimensions[] = {3U}; - Hdf5Handle coordinates{ - requireId( - H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), - "Unable to create node coordinate memory type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireId( - H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), - "Unable to create node memory type."), - H5Tclose}; - requireHdf( - H5Tinsert( - memoryType.get(), "internal_node_id", - HOFFSET(NodeReadRow, internalNodeId), H5T_NATIVE_UINT64), - "Unable to define node ID memory field."); - requireHdf( - H5Tinsert( - memoryType.get(), "instance_name", HOFFSET(NodeReadRow, instanceName), - stringType.get()), - "Unable to define node instance memory field."); - requireHdf( - H5Tinsert( - memoryType.get(), "source_label", HOFFSET(NodeReadRow, sourceLabel), - stringType.get()), - "Unable to define node label memory field."); - requireHdf( - H5Tinsert( - memoryType.get(), "coordinates", HOFFSET(NodeReadRow, coordinates), - coordinates.get()), - "Unable to define node coordinate memory field."); + auto string_type = MakeUtf8StringType(); + const hsize_t coordinate_dimensions[] = {3U}; + Hdf5Handle coordinates{ + RequireId(H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), + "Unable to create node coordinate memory type."), + H5Tclose}; + Hdf5Handle memory_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), + "Unable to create node memory type."), + H5Tclose}; + RequireHdf( + H5Tinsert(memory_type.Get(), "internal_node_id", + HOFFSET(NodeReadRow, internal_node_id), H5T_NATIVE_UINT64), + "Unable to define node ID memory field."); + RequireHdf(H5Tinsert(memory_type.Get(), "instance_name", + HOFFSET(NodeReadRow, instance_name), string_type.Get()), + "Unable to define node instance memory field."); + RequireHdf(H5Tinsert(memory_type.Get(), "source_label", + HOFFSET(NodeReadRow, source_label), string_type.Get()), + "Unable to define node label memory field."); + RequireHdf(H5Tinsert(memory_type.Get(), "coordinates", + HOFFSET(NodeReadRow, coordinates), coordinates.Get()), + "Unable to define node coordinate memory field."); - const std::size_t count = static_cast(shape[0U]); - std::vector raw(count); - requireHdf( - H5Dread( - dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, - raw.data()), - "Unable to read HDF5 node identities."); - Hdf5Handle space{ - requireId(H5Dget_space(dataset.get()), "Unable to inspect node space."), - H5Sclose}; - Hdf5VlenReclaimer reclaimer{memoryType.get(), space.get(), raw.data()}; - std::vector rows; - rows.reserve(count); - std::map identities; - for (std::size_t index = 0U; index < count; ++index) { - if (raw[index].internalNodeId != index || raw[index].instanceName == nullptr || - raw[index].sourceLabel == nullptr || raw[index].instanceName[0] == '\0' || - !std::all_of( - std::begin(raw[index].coordinates), - std::end(raw[index].coordinates), - [](const double value) { return std::isfinite(value); })) { - fail("schema-mismatch", "An HDF5 node row has invalid identity or values."); - } - WideRow row{}; - row.identity.instanceName = raw[index].instanceName; - row.identity.sourceNodeLabel = parsePositiveLabel(raw[index].sourceLabel); - if (!identities.emplace(row.identity, rows.size()).second) { - fail("schema-mismatch", "An HDF5 node identity is duplicated."); - } - rows.push_back(std::move(row)); + const std::size_t count = static_cast(shape[0U]); + std::vector raw(count); + RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, + H5P_DEFAULT, raw.data()), + "Unable to read HDF5 node identities."); + Hdf5Handle space{ + RequireId(H5Dget_space(dataset.Get()), "Unable to inspect node space."), + H5Sclose}; + Hdf5VlenReclaimer reclaimer{memory_type.Get(), space.Get(), raw.data()}; + std::vector rows; + rows.reserve(count); + std::map identities; + for (std::size_t index = 0U; index < count; ++index) { + if (raw[index].internal_node_id != index || + raw[index].instance_name == nullptr || + raw[index].source_label == nullptr || + raw[index].instance_name[0] == '\0' || + !std::all_of(std::begin(raw[index].coordinates), + std::end(raw[index].coordinates), + [](const double value) { return std::isfinite(value); })) { + Fail("schema-mismatch", + "An HDF5 node row has invalid identity or values."); } - reclaimer.reclaim(); - return rows; + WideRow row{}; + row.identity.instance_name = raw[index].instance_name; + row.identity.source_node_label = + ParsePositiveLabel(raw[index].source_label); + if (!identities.emplace(row.identity, rows.size()).second) { + Fail("schema-mismatch", "An HDF5 node identity is duplicated."); + } + rows.push_back(std::move(row)); + } + reclaimer.Reclaim(); + return rows; } struct ElementIdentityReadRow { - char* sourceElementType; - char* internalFormulation; + char* source_element_type; + char* internal_formulation; }; -void requireElementIdentity( - const hid_t file, const std::string& expectedSourceType) { - auto dataset = openDataset(file, "/model/elements"); - const auto shape = dimensions(dataset.get()); - if (shape.size() != 1U || shape[0U] == 0U || - shape[0U] > static_cast((std::numeric_limits::max)())) { - fail("schema-mismatch", "The HDF5 element dataset has an invalid shape."); - } - requireExactCompoundMembers( - dataset.get(), - {"internal_element_id", "instance_name", "source_label", - "source_element_type", "internal_formulation", "node_internal_ids", - "shell_section_internal_id", "material_internal_id"}); - requireStringAttribute(dataset.get(), "formulation", kInternalFormulation); +void RequireElementIdentity(const hid_t file, + const std::string& expected_source_type) { + auto dataset = OpenDataset(file, "/model/elements"); + const auto shape = Dimensions(dataset.Get()); + if (shape.size() != 1U || shape[0U] == 0U || + shape[0U] > + static_cast((std::numeric_limits::max)())) { + Fail("schema-mismatch", "The HDF5 element dataset has an invalid shape."); + } + RequireExactCompoundMembers( + dataset.Get(), + {"internal_element_id", "instance_name", "source_label", + "source_element_type", "internal_formulation", "node_internal_ids", + "shell_section_internal_id", "material_internal_id"}); + RequireStringAttribute(dataset.Get(), "formulation", kInternalFormulation); - auto stringType = makeUtf8StringType(); - Hdf5Handle memoryType{ - requireId( - H5Tcreate(H5T_COMPOUND, sizeof(ElementIdentityReadRow)), - "Unable to create element identity memory type."), - H5Tclose}; - requireHdf( - H5Tinsert( - memoryType.get(), "source_element_type", - HOFFSET(ElementIdentityReadRow, sourceElementType), stringType.get()), - "Unable to define source element type memory field."); - requireHdf( - H5Tinsert( - memoryType.get(), "internal_formulation", - HOFFSET(ElementIdentityReadRow, internalFormulation), stringType.get()), - "Unable to define formulation memory field."); - const std::size_t count = static_cast(shape[0U]); - std::vector rows(count); - requireHdf( - H5Dread( - dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, - rows.data()), - "Unable to read HDF5 element identity."); - Hdf5Handle space{ - requireId(H5Dget_space(dataset.get()), "Unable to inspect element space."), - H5Sclose}; - Hdf5VlenReclaimer reclaimer{memoryType.get(), space.get(), rows.data()}; - for (const auto& row : rows) { - if (row.sourceElementType == nullptr || row.internalFormulation == nullptr || - row.sourceElementType != expectedSourceType || - row.internalFormulation != std::string{kInternalFormulation}) { - fail( - "schema-mismatch", - "The HDF5 source element type or internal formulation is invalid."); - } + auto string_type = MakeUtf8StringType(); + Hdf5Handle memory_type{ + RequireId(H5Tcreate(H5T_COMPOUND, sizeof(ElementIdentityReadRow)), + "Unable to create element identity memory type."), + H5Tclose}; + RequireHdf(H5Tinsert(memory_type.Get(), "source_element_type", + HOFFSET(ElementIdentityReadRow, source_element_type), + string_type.Get()), + "Unable to define source element type memory field."); + RequireHdf(H5Tinsert(memory_type.Get(), "internal_formulation", + HOFFSET(ElementIdentityReadRow, internal_formulation), + string_type.Get()), + "Unable to define formulation memory field."); + const std::size_t count = static_cast(shape[0U]); + std::vector rows(count); + RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, + H5P_DEFAULT, rows.data()), + "Unable to read HDF5 element identity."); + Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()), + "Unable to inspect element space."), + H5Sclose}; + Hdf5VlenReclaimer reclaimer{memory_type.Get(), space.Get(), rows.data()}; + for (const auto& row : rows) { + if (row.source_element_type == nullptr || + row.internal_formulation == nullptr || + row.source_element_type != expected_source_type || + row.internal_formulation != std::string{kInternalFormulation}) { + Fail("schema-mismatch", + "The HDF5 source element type or internal formulation is invalid."); } - reclaimer.reclaim(); + } + reclaimer.Reclaim(); } -std::vector readDisplacement( - const hid_t file, const std::size_t nodeCount) { - auto dataset = openDataset(file, kDisplacementPath); - if (dimensions(dataset.get()) != - std::vector{static_cast(nodeCount), 6U}) { - fail("schema-mismatch", "The HDF5 displacement dataset has the wrong shape."); - } - Hdf5Handle type{ - requireId(H5Dget_type(dataset.get()), "Unable to inspect displacement type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_FLOAT || - H5Tget_size(type.get()) != sizeof(double) || - H5Tequal(type.get(), H5T_IEEE_F64LE) <= 0) { - fail("schema-mismatch", "The HDF5 displacement dataset is not float64 LE."); - } - requireStringAttribute(dataset.get(), "component_names", "UX,UY,UZ,URX,URY,URZ"); - requireStringAttribute( - dataset.get(), "component_unit_dimensions", - "length,length,length,radian,radian,radian"); - requireStringAttribute(dataset.get(), "coordinate_system", "global-cartesian"); - requireStringAttribute(dataset.get(), "location", "nodal"); - requireStringAttribute(dataset.get(), "step_name", "Step-1"); - if (readUint64Attribute(dataset.get(), "frame_index") != 0U) { - fail("schema-mismatch", "The HDF5 displacement frame identity is invalid."); - } - std::vector values(nodeCount * kComponents.size()); - if (!values.empty()) { - requireHdf( - H5Dread( - dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()), - "Unable to read HDF5 displacement values."); - } - if (!std::all_of(values.begin(), values.end(), [](const double value) { - return std::isfinite(value); - })) { - fail("schema-mismatch", "An HDF5 displacement value is nonfinite."); - } - return values; +std::vector ReadDisplacement(const hid_t file, + const std::size_t node_count) { + auto dataset = OpenDataset(file, kDisplacementPath); + if (Dimensions(dataset.Get()) != + std::vector{static_cast(node_count), 6U}) { + Fail("schema-mismatch", + "The HDF5 displacement dataset has the wrong shape."); + } + Hdf5Handle type{RequireId(H5Dget_type(dataset.Get()), + "Unable to inspect displacement type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_FLOAT || + H5Tget_size(type.Get()) != sizeof(double) || + H5Tequal(type.Get(), H5T_IEEE_F64LE) <= 0) { + Fail("schema-mismatch", "The HDF5 displacement dataset is not float64 LE."); + } + RequireStringAttribute(dataset.Get(), "component_names", + "UX,UY,UZ,URX,URY,URZ"); + RequireStringAttribute(dataset.Get(), "component_unit_dimensions", + "length,length,length,radian,radian,radian"); + RequireStringAttribute(dataset.Get(), "coordinate_system", + "global-cartesian"); + RequireStringAttribute(dataset.Get(), "location", "nodal"); + RequireStringAttribute(dataset.Get(), "step_name", "Step-1"); + if (ReadUint64Attribute(dataset.Get(), "frame_index") != 0U) { + Fail("schema-mismatch", "The HDF5 displacement frame identity is invalid."); + } + std::vector values(node_count * kComponents.size()); + if (!values.empty()) { + RequireHdf(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, + H5P_DEFAULT, values.data()), + "Unable to read HDF5 displacement values."); + } + if (!std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); })) { + Fail("schema-mismatch", "An HDF5 displacement value is nonfinite."); + } + return values; } struct Hdf5Projection { - std::vector rows; - std::string sourceElementType; - std::string internalFormulation; - std::string integrationRule; + std::vector rows; + std::string source_element_type; + std::string internal_formulation; + std::string integration_rule; }; -Hdf5Projection readHdf5(const Mitc4ReferenceCase& referenceCase) { - Hdf5ErrorSilencer silencer; - Hdf5Handle file{ - requireId( - H5Fopen( - referenceCase.resultsHdf5Path.string().c_str(), H5F_ACC_RDONLY, - H5P_DEFAULT), - "The authoritative HDF5 output cannot be opened."), - H5Fclose}; - Hdf5Handle metadata{ - requireId( - H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), - "The HDF5 metadata group is missing."), - H5Gclose}; - if (readUint64Attribute(metadata.get(), "schema_version") != 0U || - readUint64Attribute(metadata.get(), "frame_index") != 0U) { - fail("schema-mismatch", "The HDF5 schema or frame version is invalid."); - } - requireStringAttribute(metadata.get(), "feature_id", "linear-static-mitc4-shell"); - requireStringAttribute(metadata.get(), "step_name", "Step-1"); - requireStringAttribute( - metadata.get(), "internal_formulation", kInternalFormulation); - requireStringAttribute(metadata.get(), "integration_rule", kIntegrationRule); - const std::string normalizedInput = - std::filesystem::absolute(referenceCase.inputPath) - .lexically_normal() - .generic_u8string(); - const std::string sourceIdentity = - readStringAttribute(metadata.get(), "source_input_identity"); - if (sourceIdentity.find("path=" + normalizedInput + ";content_identity=") != 0U) { - fail( - "schema-mismatch", - "The HDF5 source input identity does not match the declared input."); - } - auto rows = readNodes(file.get()); - requireElementIdentity(file.get(), referenceCase.expectedSourceElementType); - const auto values = readDisplacement(file.get(), rows.size()); - for (std::size_t row = 0U; row < rows.size(); ++row) { - std::copy_n( - values.begin() + static_cast(row * kComponents.size()), - kComponents.size(), rows[row].values.begin()); - } - return { - std::move(rows), referenceCase.expectedSourceElementType, - kInternalFormulation, kIntegrationRule}; +Hdf5Projection ReadHdf5(const Mitc4ReferenceCase& reference_case) { + Hdf5ErrorSilencer silencer; + Hdf5Handle file{ + RequireId(H5Fopen(reference_case.results_hdf5_path.string().c_str(), + H5F_ACC_RDONLY, H5P_DEFAULT), + "The authoritative HDF5 output cannot be opened."), + H5Fclose}; + Hdf5Handle metadata{RequireId(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), + "The HDF5 metadata group is missing."), + H5Gclose}; + if (ReadUint64Attribute(metadata.Get(), "schema_version") != 0U || + ReadUint64Attribute(metadata.Get(), "frame_index") != 0U) { + Fail("schema-mismatch", "The HDF5 schema or frame version is invalid."); + } + RequireStringAttribute(metadata.Get(), "feature_id", + "linear-static-mitc4-shell"); + RequireStringAttribute(metadata.Get(), "step_name", "Step-1"); + RequireStringAttribute(metadata.Get(), "internal_formulation", + kInternalFormulation); + RequireStringAttribute(metadata.Get(), "integration_rule", kIntegrationRule); + const std::string normalized_input = + std::filesystem::absolute(reference_case.input_path) + .lexically_normal() + .generic_u8string(); + const std::string source_identity = + ReadStringAttribute(metadata.Get(), "source_input_identity"); + if (source_identity.find("path=" + normalized_input + ";content_identity=") != + 0U) { + Fail("schema-mismatch", + "The HDF5 source input identity does not match the declared input."); + } + auto rows = ReadNodes(file.Get()); + RequireElementIdentity(file.Get(), + reference_case.expected_source_element_type); + const auto values = ReadDisplacement(file.Get(), rows.size()); + for (std::size_t row = 0U; row < rows.size(); ++row) { + std::copy_n( + values.begin() + static_cast(row * kComponents.size()), + kComponents.size(), rows[row].values.begin()); + } + return {std::move(rows), reference_case.expected_source_element_type, + kInternalFormulation, kIntegrationRule}; } -void requireArtifacts(const Mitc4ReferenceCase& referenceCase) { - if (referenceCase.caseId.empty() || - (referenceCase.expectedSourceElementType != "S4" && - referenceCase.expectedSourceElementType != "S4R")) { - fail("schema-mismatch", "The MITC4 reference case identity is invalid."); - } - std::error_code error; - for (const auto* path : { - &referenceCase.inputPath, - &referenceCase.displacementCsvPath, - &referenceCase.resultsHdf5Path}) { - if (!std::filesystem::is_regular_file(*path, error) || error) { - fail( - "needs-reference-artifacts", - "A declared MITC4 input, displacement CSV, or HDF5 file is missing."); - } +void RequireArtifacts(const Mitc4ReferenceCase& reference_case) { + if (reference_case.case_id.empty() || + (reference_case.expected_source_element_type != "S4" && + reference_case.expected_source_element_type != "S4R")) { + Fail("schema-mismatch", "The MITC4 reference case identity is invalid."); + } + std::error_code error; + for (const auto* path : + {&reference_case.input_path, &reference_case.displacement_csv_path, + &reference_case.results_hdf5_path}) { + if (!std::filesystem::is_regular_file(*path, error) || error) { + Fail( + "needs-reference-artifacts", + "A declared MITC4 input, displacement CSV, or HDF5 file is missing."); } + } } -std::string finiteText(const double value) { - std::ostringstream stream; - stream.imbue(std::locale::classic()); - stream << std::setprecision(std::numeric_limits::max_digits10) - << value; - return stream.str(); +std::string FiniteText(const double value) { + std::ostringstream stream; + stream.imbue(std::locale::classic()); + stream << std::setprecision(std::numeric_limits::max_digits10) + << value; + return stream.str(); } -std::string jsonEscape(const std::string& value) { - std::ostringstream stream; - for (const unsigned char character : value) { - switch (character) { - case '"': stream << "\\\""; break; - case '\\': stream << "\\\\"; break; - case '\b': stream << "\\b"; break; - case '\f': stream << "\\f"; break; - case '\n': stream << "\\n"; break; - case '\r': stream << "\\r"; break; - case '\t': stream << "\\t"; break; - default: - if (character < 0x20U) { - stream << "\\u00" << std::hex << std::setw(2) - << std::setfill('0') << static_cast(character) - << std::dec << std::setfill(' '); - } else { - stream << static_cast(character); - } - break; +std::string JsonEscape(const std::string& value) { + std::ostringstream stream; + for (const unsigned char character : value) { + switch (character) { + case '"': + stream << "\\\""; + break; + case '\\': + stream << "\\\\"; + break; + case '\b': + stream << "\\b"; + break; + case '\f': + stream << "\\f"; + break; + case '\n': + stream << "\\n"; + break; + case '\r': + stream << "\\r"; + break; + case '\t': + stream << "\\t"; + break; + default: + if (character < 0x20U) { + stream << "\\u00" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(character) << std::dec + << std::setfill(' '); + } else { + stream << static_cast(character); } + break; } - return stream.str(); + } + return stream.str(); } -} // namespace +} // namespace -Result Mitc4ReferenceComparison::compare( - const Mitc4ReferenceCase& referenceCase) { - try { - requireArtifacts(referenceCase); - const auto referenceRows = - readReferenceCsv(referenceCase.displacementCsvPath); - auto hdf5 = readHdf5(referenceCase); +Result Mitc4ReferenceComparison::Compare( + const Mitc4ReferenceCase& reference_case) { + try { + RequireArtifacts(reference_case); + const auto reference_rows = + ReadReferenceCsv(reference_case.displacement_csv_path); + auto hdf5 = ReadHdf5(reference_case); - std::map referenceByIdentity; - for (const auto& row : referenceRows) { - referenceByIdentity.emplace(row.identity, &row); - } - if (referenceRows.size() != hdf5.rows.size()) { - fail( - "schema-mismatch", - "The HDF5 and CSV source-node row counts are not equal."); - } - for (const auto& row : hdf5.rows) { - if (referenceByIdentity.find(row.identity) == referenceByIdentity.end()) { - fail( - "schema-mismatch", - "The HDF5 and CSV source-node identities are not equal."); - } - } - - std::array scales{}; - for (const auto& row : referenceRows) { - for (std::size_t component = 0U; - component < scales.size(); - ++component) { - scales[component] = - (std::max)(scales[component], std::abs(row.values[component])); - } - } - - Mitc4ComparisonReport report{}; - report.caseId = referenceCase.caseId; - report.sourceElementType = std::move(hdf5.sourceElementType); - report.internalFormulation = std::move(hdf5.internalFormulation); - report.integrationRule = std::move(hdf5.integrationRule); - report.passed = true; - report.rows.reserve(hdf5.rows.size() * kComponents.size()); - report.vectorMetrics.reserve(hdf5.rows.size()); - std::array errorNorms{}; - std::array maximumErrors{}; - std::array maximumNormalized{}; - std::array worstRows{}; - double globalWorstNormalized = -1.0; - - for (const auto& hdf5Row : hdf5.rows) { - const auto reference = referenceByIdentity.find(hdf5Row.identity); - if (reference == referenceByIdentity.end()) { - fail("schema-mismatch", "A projected reference row is missing."); - } - std::array errors{}; - for (std::size_t component = 0U; - component < kComponents.size(); - ++component) { - const double tolerance = kFixedAbsoluteTolerance; - const double absoluteError = std::abs( - hdf5Row.values[component] - - reference->second->values[component]); - const double normalizedError = absoluteError / tolerance; - if (!std::isfinite(tolerance) || !(tolerance > 0.0) || - !std::isfinite(absoluteError) || - !std::isfinite(normalizedError)) { - fail( - "schema-mismatch", - "A finite row produced a nonfinite comparison metric."); - } - const bool blocking = component < 3U; - const bool withinTolerance = absoluteError <= tolerance; - const std::size_t rowIndex = report.rows.size(); - report.rows.push_back({ - referenceCase.caseId, - hdf5Row.identity.instanceName, - hdf5Row.identity.sourceNodeLabel, - kComponents[component], - hdf5Row.values[component], - reference->second->values[component], - absoluteError, - tolerance, - normalizedError, - blocking, - withinTolerance}); - errors[component] = absoluteError; - errorNorms[component] = - std::hypot(errorNorms[component], absoluteError); - if (normalizedError > maximumNormalized[component]) { - maximumNormalized[component] = normalizedError; - maximumErrors[component] = absoluteError; - worstRows[component] = rowIndex; - } - if (normalizedError > globalWorstNormalized) { - globalWorstNormalized = normalizedError; - report.worstRow = rowIndex; - } - if (blocking && !withinTolerance) { - report.passed = false; - } else if (!blocking && !withinTolerance) { - const std::string message = - "case=" + referenceCase.caseId + ";instance=" + - hdf5Row.identity.instanceName + ";node=" + - std::to_string(hdf5Row.identity.sourceNodeLabel) + - ";component=" + kComponents[component] + - ";absolute_error=" + finiteText(absoluteError) + - ";tolerance=" + finiteText(tolerance); - report.warnings.push_back({ - "rotation-reference-exceedance", rowIndex, message}); - } - } - report.vectorMetrics.push_back({ - hdf5Row.identity.instanceName, - hdf5Row.identity.sourceNodeLabel, - std::hypot(errors[0U], errors[1U], errors[2U]), - std::hypot(errors[3U], errors[4U], errors[5U])}); - } - - report.metrics.reserve(kComponents.size()); - const double rowCount = static_cast(hdf5.rows.size()); - for (std::size_t component = 0U; - component < kComponents.size(); - ++component) { - report.metrics.push_back({ - kComponents[component], - scales[component], - kFixedAbsoluteTolerance, - maximumErrors[component], - maximumNormalized[component], - errorNorms[component] / std::sqrt(rowCount), - errorNorms[component], - worstRows[component]}); - } - return Result::Success(std::move(report)); - } catch (const ComparisonFailure& exception) { - return Result::Failure(failureStatus( - referenceCase.caseId, exception.code(), exception.what())); - } catch (const std::exception& exception) { - return Result::Failure(failureStatus( - referenceCase.caseId, "comparison-failure", exception.what())); + std::map reference_by_identity; + for (const auto& row : reference_rows) { + reference_by_identity.emplace(row.identity, &row); } + if (reference_rows.size() != hdf5.rows.size()) { + Fail("schema-mismatch", + "The HDF5 and CSV source-node row counts are not equal."); + } + for (const auto& row : hdf5.rows) { + if (reference_by_identity.find(row.identity) == + reference_by_identity.end()) { + Fail("schema-mismatch", + "The HDF5 and CSV source-node identities are not equal."); + } + } + + std::array scales{}; + for (const auto& row : reference_rows) { + for (std::size_t component = 0U; component < scales.size(); ++component) { + scales[component] = + (std::max)(scales[component], std::abs(row.values[component])); + } + } + + Mitc4ComparisonReport report{}; + report.case_id = reference_case.case_id; + report.source_element_type = std::move(hdf5.source_element_type); + report.internal_formulation = std::move(hdf5.internal_formulation); + report.integration_rule = std::move(hdf5.integration_rule); + report.passed = true; + report.rows.reserve(hdf5.rows.size() * kComponents.size()); + report.vector_metrics.reserve(hdf5.rows.size()); + std::array error_norms{}; + std::array maximum_errors{}; + std::array maximum_normalized{}; + std::array worst_rows{}; + double global_worst_normalized = -1.0; + + for (const auto& hdf5_row : hdf5.rows) { + const auto reference = reference_by_identity.find(hdf5_row.identity); + if (reference == reference_by_identity.end()) { + Fail("schema-mismatch", "A projected reference row is missing."); + } + std::array errors{}; + for (std::size_t component = 0U; component < kComponents.size(); + ++component) { + const double tolerance = kFixedAbsoluteTolerance; + const double absolute_error = std::abs( + hdf5_row.values[component] - reference->second->values[component]); + const double normalized_error = absolute_error / tolerance; + if (!std::isfinite(tolerance) || !(tolerance > 0.0) || + !std::isfinite(absolute_error) || + !std::isfinite(normalized_error)) { + Fail("schema-mismatch", + "A finite row produced a nonfinite comparison metric."); + } + const bool blocking = component < 3U; + const bool within_tolerance = absolute_error <= tolerance; + const std::size_t row_index = report.rows.size(); + report.rows.push_back( + {reference_case.case_id, hdf5_row.identity.instance_name, + hdf5_row.identity.source_node_label, kComponents[component], + hdf5_row.values[component], reference->second->values[component], + absolute_error, tolerance, normalized_error, blocking, + within_tolerance}); + errors[component] = absolute_error; + error_norms[component] = + std::hypot(error_norms[component], absolute_error); + if (normalized_error > maximum_normalized[component]) { + maximum_normalized[component] = normalized_error; + maximum_errors[component] = absolute_error; + worst_rows[component] = row_index; + } + if (normalized_error > global_worst_normalized) { + global_worst_normalized = normalized_error; + report.worst_row = row_index; + } + if (blocking && !within_tolerance) { + report.passed = false; + } else if (!blocking && !within_tolerance) { + const std::string message = + "case=" + reference_case.case_id + + ";instance=" + hdf5_row.identity.instance_name + + ";node=" + std::to_string(hdf5_row.identity.source_node_label) + + ";component=" + kComponents[component] + + ";absolute_error=" + FiniteText(absolute_error) + + ";tolerance=" + FiniteText(tolerance); + report.warnings.push_back( + {"rotation-reference-exceedance", row_index, message}); + } + } + report.vector_metrics.push_back( + {hdf5_row.identity.instance_name, hdf5_row.identity.source_node_label, + std::hypot(errors[0U], errors[1U], errors[2U]), + std::hypot(errors[3U], errors[4U], errors[5U])}); + } + + report.metrics.reserve(kComponents.size()); + const double row_count = static_cast(hdf5.rows.size()); + for (std::size_t component = 0U; component < kComponents.size(); + ++component) { + report.metrics.push_back( + {kComponents[component], scales[component], kFixedAbsoluteTolerance, + maximum_errors[component], maximum_normalized[component], + error_norms[component] / std::sqrt(row_count), + error_norms[component], worst_rows[component]}); + } + return Result::Success(std::move(report)); + } catch (const ComparisonFailure& exception) { + return Result::Failure(FailureStatus( + reference_case.case_id, exception.Code(), exception.what())); + } catch (const std::exception& exception) { + return Result::Failure(FailureStatus( + reference_case.case_id, "comparison-failure", exception.what())); + } } -Status Mitc4ReferenceComparison::writeDeterministicJson( +Status Mitc4ReferenceComparison::WriteDeterministicJson( const Mitc4ComparisonReport& report, - const std::filesystem::path& outputJson) { - try { - std::ofstream stream{outputJson, std::ios::binary | std::ios::trunc}; - if (!stream) { - return failureStatus( - report.caseId, - "comparison-report-write-failed", - "The deterministic MITC4 JSON report cannot be opened."); - } - stream.imbue(std::locale::classic()); - stream << std::setprecision(std::numeric_limits::max_digits10); - stream << "{\"case_id\":\"" << jsonEscape(report.caseId) - << "\",\"source_element_type\":\"" - << jsonEscape(report.sourceElementType) - << "\",\"internal_formulation\":\"" - << jsonEscape(report.internalFormulation) - << "\",\"integration_rule\":\"" - << jsonEscape(report.integrationRule) << "\",\"rows\":["; - for (std::size_t index = 0U; index < report.rows.size(); ++index) { - if (index != 0U) { - stream << ','; - } - const auto& row = report.rows[index]; - stream << "{\"case_id\":\"" << jsonEscape(row.caseId) - << "\",\"instance_name\":\"" - << jsonEscape(row.instanceName) - << "\",\"source_node_label\":" << row.sourceNodeLabel - << ",\"component\":\"" << jsonEscape(row.component) - << "\",\"fesa_value\":" << row.fesaValue - << ",\"reference_value\":" << row.referenceValue - << ",\"absolute_error\":" << row.absoluteError - << ",\"tolerance\":" << row.tolerance - << ",\"normalized_error\":" << row.normalizedError - << ",\"blocking\":" << (row.blocking ? "true" : "false") - << ",\"within_tolerance\":" - << (row.withinTolerance ? "true" : "false") << '}'; - } - stream << "],\"metrics\":["; - for (std::size_t index = 0U; index < report.metrics.size(); ++index) { - if (index != 0U) { - stream << ','; - } - const auto& metric = report.metrics[index]; - stream << "{\"component\":\"" << jsonEscape(metric.component) - << "\",\"reference_scale\":" << metric.referenceScale - << ",\"tolerance\":" << metric.tolerance - << ",\"maximum_absolute_error\":" - << metric.maximumAbsoluteError - << ",\"maximum_normalized_error\":" - << metric.maximumNormalizedError - << ",\"rms_error\":" << metric.rmsError - << ",\"vector_norm_error\":" << metric.vectorNormError - << ",\"worst_row\":" << metric.worstRow << '}'; - } - stream << "],\"vector_metrics\":["; - for (std::size_t index = 0U; index < report.vectorMetrics.size(); ++index) { - if (index != 0U) { - stream << ','; - } - const auto& metric = report.vectorMetrics[index]; - stream << "{\"instance_name\":\"" - << jsonEscape(metric.instanceName) - << "\",\"source_node_label\":" << metric.sourceNodeLabel - << ",\"displacement_norm_error\":" - << metric.displacementNormError - << ",\"rotation_norm_error\":" - << metric.rotationNormError << '}'; - } - stream << "],\"warnings\":["; - for (std::size_t index = 0U; index < report.warnings.size(); ++index) { - if (index != 0U) { - stream << ','; - } - const auto& warning = report.warnings[index]; - stream << "{\"code\":\"" << jsonEscape(warning.code) - << "\",\"row\":" << warning.row - << ",\"message\":\"" << jsonEscape(warning.message) - << "\"}"; - } - stream << "],\"worst_row\":" << report.worstRow - << ",\"passed\":" << (report.passed ? "true" : "false") - << "}\n"; - stream.flush(); - if (!stream) { - return failureStatus( - report.caseId, - "comparison-report-write-failed", - "The deterministic MITC4 JSON report could not be completed."); - } - return Status::Ok(); - } catch (const std::exception& exception) { - return failureStatus( - report.caseId, "comparison-report-write-failed", exception.what()); + const std::filesystem::path& output_json) { + try { + std::ofstream stream{output_json, std::ios::binary | std::ios::trunc}; + if (!stream) { + return FailureStatus( + report.case_id, "comparison-report-write-failed", + "The deterministic MITC4 JSON report cannot be opened."); } + stream.imbue(std::locale::classic()); + stream << std::setprecision(std::numeric_limits::max_digits10); + stream << "{\"case_id\":\"" << JsonEscape(report.case_id) + << "\",\"source_element_type\":\"" + << JsonEscape(report.source_element_type) + << "\",\"internal_formulation\":\"" + << JsonEscape(report.internal_formulation) + << "\",\"integration_rule\":\"" + << JsonEscape(report.integration_rule) << "\",\"rows\":["; + for (std::size_t index = 0U; index < report.rows.size(); ++index) { + if (index != 0U) { + stream << ','; + } + const auto& row = report.rows[index]; + stream << "{\"case_id\":\"" << JsonEscape(row.case_id) + << "\",\"instance_name\":\"" << JsonEscape(row.instance_name) + << "\",\"source_node_label\":" << row.source_node_label + << ",\"component\":\"" << JsonEscape(row.component) + << "\",\"fesa_value\":" << row.fesa_value + << ",\"reference_value\":" << row.reference_value + << ",\"absolute_error\":" << row.absolute_error + << ",\"tolerance\":" << row.tolerance + << ",\"normalized_error\":" << row.normalized_error + << ",\"blocking\":" << (row.blocking ? "true" : "false") + << ",\"within_tolerance\":" + << (row.within_tolerance ? "true" : "false") << '}'; + } + stream << "],\"metrics\":["; + for (std::size_t index = 0U; index < report.metrics.size(); ++index) { + if (index != 0U) { + stream << ','; + } + const auto& metric = report.metrics[index]; + stream << "{\"component\":\"" << JsonEscape(metric.component) + << "\",\"reference_scale\":" << metric.reference_scale + << ",\"tolerance\":" << metric.tolerance + << ",\"maximum_absolute_error\":" << metric.maximum_absolute_error + << ",\"maximum_normalized_error\":" + << metric.maximum_normalized_error + << ",\"rms_error\":" << metric.rms_error + << ",\"vector_norm_error\":" << metric.vector_norm_error + << ",\"worst_row\":" << metric.worst_row << '}'; + } + stream << "],\"vector_metrics\":["; + for (std::size_t index = 0U; index < report.vector_metrics.size(); + ++index) { + if (index != 0U) { + stream << ','; + } + const auto& metric = report.vector_metrics[index]; + stream << "{\"instance_name\":\"" << JsonEscape(metric.instance_name) + << "\",\"source_node_label\":" << metric.source_node_label + << ",\"displacement_norm_error\":" + << metric.displacement_norm_error + << ",\"rotation_norm_error\":" << metric.rotation_norm_error + << '}'; + } + stream << "],\"warnings\":["; + for (std::size_t index = 0U; index < report.warnings.size(); ++index) { + if (index != 0U) { + stream << ','; + } + const auto& warning = report.warnings[index]; + stream << "{\"code\":\"" << JsonEscape(warning.code) + << "\",\"row\":" << warning.row << ",\"message\":\"" + << JsonEscape(warning.message) << "\"}"; + } + stream << "],\"worst_row\":" << report.worst_row + << ",\"passed\":" << (report.passed ? "true" : "false") << "}\n"; + stream.flush(); + if (!stream) { + return FailureStatus( + report.case_id, "comparison-report-write-failed", + "The deterministic MITC4 JSON report could not be completed."); + } + return Status::Ok(); + } catch (const std::exception& exception) { + return FailureStatus(report.case_id, "comparison-report-write-failed", + exception.what()); + } } -} // namespace fesa::test +} // namespace fesa::test diff --git a/tests/reference/mitc4_reference_comparison.h b/tests/reference/mitc4_reference_comparison.h new file mode 100644 index 0000000..2431d27 --- /dev/null +++ b/tests/reference/mitc4_reference_comparison.h @@ -0,0 +1,84 @@ +#ifndef FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_ +#define FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_ + +#include +#include +#include +#include +#include + +#include "fesa/core/status.h" + +namespace fesa::test { + +struct Mitc4ReferenceCase { + std::string case_id; + std::string expected_source_element_type; + std::filesystem::path input_path; + std::filesystem::path displacement_csv_path; + std::filesystem::path results_hdf5_path; +}; + +struct Mitc4RowDecision { + std::string case_id; + std::string instance_name; + std::int64_t source_node_label; + std::string component; + double fesa_value; + double reference_value; + double absolute_error; + double tolerance; + double normalized_error; + bool blocking; + bool within_tolerance; +}; + +struct Mitc4ComponentMetrics { + std::string component; + double reference_scale; + double tolerance; + double maximum_absolute_error; + double maximum_normalized_error; + double rms_error; + double vector_norm_error; + std::size_t worst_row; +}; + +struct Mitc4VectorMetrics { + std::string instance_name; + std::int64_t source_node_label; + double displacement_norm_error; + double rotation_norm_error; +}; + +struct Mitc4Warning { + std::string code; + std::size_t row; + std::string message; +}; + +struct Mitc4ComparisonReport { + std::string case_id; + std::string source_element_type; + std::string internal_formulation; + std::string integration_rule; + std::vector rows; + std::vector metrics; + std::vector vector_metrics; + std::vector warnings; + std::size_t worst_row; + bool passed; +}; + +class Mitc4ReferenceComparison { + public: + static Result Compare( + const Mitc4ReferenceCase& reference_case); + static Status WriteDeterministicJson( + const Mitc4ComparisonReport& report, + const std::filesystem::path& output_json); +}; + +} // namespace fesa::test + +#endif // FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_ diff --git a/tests/reference/mitc4_reference_comparison.hpp b/tests/reference/mitc4_reference_comparison.hpp deleted file mode 100644 index 18c1b9c..0000000 --- a/tests/reference/mitc4_reference_comparison.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include "fesa/core/status.h" - -#include -#include -#include -#include -#include - -namespace fesa::test { - -struct Mitc4ReferenceCase { - std::string caseId; - std::string expectedSourceElementType; - std::filesystem::path inputPath; - std::filesystem::path displacementCsvPath; - std::filesystem::path resultsHdf5Path; -}; - -struct Mitc4RowDecision { - std::string caseId; - std::string instanceName; - std::int64_t sourceNodeLabel; - std::string component; - double fesaValue; - double referenceValue; - double absoluteError; - double tolerance; - double normalizedError; - bool blocking; - bool withinTolerance; -}; - -struct Mitc4ComponentMetrics { - std::string component; - double referenceScale; - double tolerance; - double maximumAbsoluteError; - double maximumNormalizedError; - double rmsError; - double vectorNormError; - std::size_t worstRow; -}; - -struct Mitc4VectorMetrics { - std::string instanceName; - std::int64_t sourceNodeLabel; - double displacementNormError; - double rotationNormError; -}; - -struct Mitc4Warning { - std::string code; - std::size_t row; - std::string message; -}; - -struct Mitc4ComparisonReport { - std::string caseId; - std::string sourceElementType; - std::string internalFormulation; - std::string integrationRule; - std::vector rows; - std::vector metrics; - std::vector vectorMetrics; - std::vector warnings; - std::size_t worstRow; - bool passed; -}; - -class Mitc4ReferenceComparison { -public: - static Result compare( - const Mitc4ReferenceCase& referenceCase); - static Status writeDeterministicJson( - const Mitc4ComparisonReport& report, - const std::filesystem::path& outputJson); -}; - -} // namespace fesa::test diff --git a/tests/reference/mitc4_reference_comparison_test.cpp b/tests/reference/mitc4_reference_comparison_test.cpp index d3e8f5b..a2dc50b 100644 --- a/tests/reference/mitc4_reference_comparison_test.cpp +++ b/tests/reference/mitc4_reference_comparison_test.cpp @@ -1,8 +1,7 @@ -#include "mitc4_reference_comparison.hpp" - -#include +#include "mitc4_reference_comparison.h" #include +#include #include #include @@ -31,775 +30,701 @@ constexpr const char* kIntegrationRule = "2x2x2-gauss; mitc4-edge-midpoint-shear"; constexpr const char* kDisplacementPath = "/steps/Step-1/frames/0/nodal/displacement"; -constexpr std::array kComponents{ - "U1", "U2", "U3", "UR1", "UR2", "UR3"}; +constexpr std::array kComponents{"U1", "U2", "U3", + "UR1", "UR2", "UR3"}; class Hdf5Handle { -public: - using Closer = herr_t (*)(hid_t); + public: + using Closer = herr_t (*)(hid_t); - Hdf5Handle(const hid_t value, Closer closer) - : value_{value}, closer_{closer} {} - Hdf5Handle(const Hdf5Handle&) = delete; - Hdf5Handle& operator=(const Hdf5Handle&) = delete; - Hdf5Handle(Hdf5Handle&& other) noexcept - : value_{other.value_}, closer_{other.closer_} { - other.value_ = -1; - other.closer_ = nullptr; - } - ~Hdf5Handle() { - if (value_ >= 0 && closer_ != nullptr) { - (void)closer_(value_); - } + Hdf5Handle(const hid_t value, Closer closer) + : value_{value}, closer_{closer} {} + Hdf5Handle(const Hdf5Handle&) = delete; + Hdf5Handle& operator=(const Hdf5Handle&) = delete; + Hdf5Handle(Hdf5Handle&& other) noexcept + : value_{other.value_}, closer_{other.closer_} { + other.value_ = -1; + other.closer_ = nullptr; + } + ~Hdf5Handle() { + if (value_ >= 0 && closer_ != nullptr) { + (void)closer_(value_); } + } - hid_t get() const noexcept { return value_; } + hid_t Get() const noexcept { return value_; } -private: - hid_t value_; - Closer closer_; + private: + hid_t value_; + Closer closer_; }; -hid_t requireId(const hid_t value, const char* message) { - if (value < 0) { - throw std::runtime_error{message}; - } - return value; +hid_t RequireId(const hid_t value, const char* message) { + if (value < 0) { + throw std::runtime_error{message}; + } + return value; } -void requireHdf(const herr_t value, const char* message) { - if (value < 0) { - throw std::runtime_error{message}; - } +void RequireHdf(const herr_t value, const char* message) { + if (value < 0) { + throw std::runtime_error{message}; + } } -Hdf5Handle makeUtf8StringType() { - Hdf5Handle type{ - requireId(H5Tcopy(H5T_C_S1), "Unable to copy string type."), H5Tclose}; - requireHdf( - H5Tset_size(type.get(), H5T_VARIABLE), - "Unable to create variable string type."); - requireHdf( - H5Tset_cset(type.get(), H5T_CSET_UTF8), - "Unable to create UTF-8 string type."); - return type; +Hdf5Handle MakeUtf8StringType() { + Hdf5Handle type{RequireId(H5Tcopy(H5T_C_S1), "Unable to copy string type."), + H5Tclose}; + RequireHdf(H5Tset_size(type.Get(), H5T_VARIABLE), + "Unable to create variable string type."); + RequireHdf(H5Tset_cset(type.Get(), H5T_CSET_UTF8), + "Unable to create UTF-8 string type."); + return type; } -void writeStringAttribute( - const hid_t object, const char* name, const std::string& value) { - auto type = makeUtf8StringType(); - Hdf5Handle space{ - requireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."), - H5Sclose}; - Hdf5Handle attribute{ - requireId( - H5Acreate2( - object, name, type.get(), space.get(), H5P_DEFAULT, - H5P_DEFAULT), - "Unable to create string attribute."), - H5Aclose}; - const char* raw = value.c_str(); - requireHdf( - H5Awrite(attribute.get(), type.get(), &raw), - "Unable to write string attribute."); +void WriteStringAttribute(const hid_t object, const char* name, + const std::string& value) { + auto type = MakeUtf8StringType(); + Hdf5Handle space{ + RequireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."), + H5Sclose}; + Hdf5Handle attribute{ + RequireId(H5Acreate2(object, name, type.Get(), space.Get(), H5P_DEFAULT, + H5P_DEFAULT), + "Unable to create string attribute."), + H5Aclose}; + const char* raw = value.c_str(); + RequireHdf(H5Awrite(attribute.Get(), type.Get(), &raw), + "Unable to write string attribute."); } -void writeUint64Attribute( - const hid_t object, const char* name, const std::uint64_t value) { - Hdf5Handle space{ - requireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."), - H5Sclose}; - Hdf5Handle attribute{ - requireId( - H5Acreate2( - object, name, H5T_STD_U64LE, space.get(), H5P_DEFAULT, - H5P_DEFAULT), - "Unable to create integer attribute."), - H5Aclose}; - requireHdf( - H5Awrite(attribute.get(), H5T_NATIVE_UINT64, &value), - "Unable to write integer attribute."); +void WriteUint64Attribute(const hid_t object, const char* name, + const std::uint64_t value) { + Hdf5Handle space{ + RequireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."), + H5Sclose}; + Hdf5Handle attribute{ + RequireId(H5Acreate2(object, name, H5T_STD_U64LE, space.Get(), + H5P_DEFAULT, H5P_DEFAULT), + "Unable to create integer attribute."), + H5Aclose}; + RequireHdf(H5Awrite(attribute.Get(), H5T_NATIVE_UINT64, &value), + "Unable to write integer attribute."); } -Hdf5Handle createGroup(const hid_t parent, const char* path) { - return { - requireId( - H5Gcreate2( - parent, path, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), - "Unable to create HDF5 group."), - H5Gclose}; +Hdf5Handle CreateGroup(const hid_t parent, const char* path) { + return { + RequireId(H5Gcreate2(parent, path, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), + "Unable to create HDF5 group."), + H5Gclose}; } struct ComparisonValueRow { - std::string instanceName{kInstanceName}; - std::int64_t sourceNodeLabel{}; - std::array values{}; + std::string instance_name{kInstanceName}; + std::int64_t source_node_label{}; + std::array values{}; }; -std::vector defaultRows() { - return { - {kInstanceName, 1, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}}, - {kInstanceName, 2, {2.0, -4.0, 8.0, 0.1, -0.2, 0.3}}}; +std::vector DefaultRows() { + return {{kInstanceName, 1, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}}, + {kInstanceName, 2, {2.0, -4.0, 8.0, 0.1, -0.2, 0.3}}}; } struct Hdf5Options { - std::string sourceElementType{"S4"}; - std::string internalFormulation{kInternalFormulation}; - std::string integrationRule{kIntegrationRule}; - std::string displacementComponents{"UX,UY,UZ,URX,URY,URZ"}; + std::string source_element_type{"S4"}; + std::string internal_formulation{kInternalFormulation}; + std::string integration_rule{kIntegrationRule}; + std::string displacement_components{"UX,UY,UZ,URX,URY,URZ"}; }; struct NodeWriteRow { - std::uint64_t internalNodeId; - const char* instanceName; - const char* sourceLabel; - double coordinates[3]; + std::uint64_t internal_node_id; + const char* instance_name; + const char* source_label; + double coordinates[3]; }; struct ElementWriteRow { - std::uint64_t internalElementId; - const char* instanceName; - const char* sourceLabel; - const char* sourceElementType; - const char* internalFormulation; - std::uint64_t nodeInternalIds[4]; - std::uint64_t shellSectionInternalId; - std::uint64_t materialInternalId; + std::uint64_t internal_element_id; + const char* instance_name; + const char* source_label; + const char* source_element_type; + const char* internal_formulation; + std::uint64_t node_internal_ids[4]; + std::uint64_t shell_section_internal_id; + std::uint64_t material_internal_id; }; -void writeNodes( - const hid_t file, const std::vector& values) { - std::vector labels; - labels.reserve(values.size()); - for (const auto& row : values) { - labels.push_back(std::to_string(row.sourceNodeLabel)); - } - std::vector rows; - rows.reserve(values.size()); - for (std::size_t index = 0U; index < values.size(); ++index) { - rows.push_back({ - static_cast(index), - values[index].instanceName.c_str(), - labels[index].c_str(), - {static_cast(index), 0.0, 0.0}}); - } +void WriteNodes(const hid_t file, + const std::vector& values) { + std::vector labels; + labels.reserve(values.size()); + for (const auto& row : values) { + labels.push_back(std::to_string(row.source_node_label)); + } + std::vector rows; + rows.reserve(values.size()); + for (std::size_t index = 0U; index < values.size(); ++index) { + rows.push_back({static_cast(index), + values[index].instance_name.c_str(), + labels[index].c_str(), + {static_cast(index), 0.0, 0.0}}); + } - auto stringType = makeUtf8StringType(); - const hsize_t coordinateDimensions[] = {3U}; - Hdf5Handle fileCoordinates{ - requireId( - H5Tarray_create2(H5T_IEEE_F64LE, 1, coordinateDimensions), - "Unable to create coordinate file type."), - H5Tclose}; - Hdf5Handle memoryCoordinates{ - requireId( - H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), - "Unable to create coordinate memory type."), - H5Tclose}; - Hdf5Handle fileType{ - requireId( - H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), - "Unable to create node file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireId( - H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), - "Unable to create node memory type."), - H5Tclose}; - const auto insert = [&](const hid_t type, - const hid_t integerType, - const hid_t coordinateType) { - requireHdf( - H5Tinsert( - type, "internal_node_id", - HOFFSET(NodeWriteRow, internalNodeId), integerType), - "Unable to define node ID."); - requireHdf( - H5Tinsert( - type, "instance_name", HOFFSET(NodeWriteRow, instanceName), - stringType.get()), - "Unable to define node instance."); - requireHdf( - H5Tinsert( - type, "source_label", HOFFSET(NodeWriteRow, sourceLabel), - stringType.get()), - "Unable to define node label."); - requireHdf( - H5Tinsert( - type, "coordinates", HOFFSET(NodeWriteRow, coordinates), - coordinateType), - "Unable to define node coordinates."); - }; - insert(fileType.get(), H5T_STD_U64LE, fileCoordinates.get()); - insert(memoryType.get(), H5T_NATIVE_UINT64, memoryCoordinates.get()); - const hsize_t dimensions[] = {static_cast(rows.size())}; - Hdf5Handle space{ - requireId( - H5Screate_simple(1, dimensions, nullptr), - "Unable to create node space."), - H5Sclose}; - Hdf5Handle dataset{ - requireId( - H5Dcreate2( - file, "/model/nodes", fileType.get(), space.get(), - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), - "Unable to create node dataset."), - H5Dclose}; - requireHdf( - H5Dwrite( - dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, - rows.data()), - "Unable to write node dataset."); - writeStringAttribute(dataset.get(), "coordinate_system", "global-cartesian"); - writeStringAttribute(dataset.get(), "units_label", "length"); + auto string_type = MakeUtf8StringType(); + const hsize_t coordinate_dimensions[] = {3U}; + Hdf5Handle file_coordinates{ + RequireId(H5Tarray_create2(H5T_IEEE_F64LE, 1, coordinate_dimensions), + "Unable to create coordinate file type."), + H5Tclose}; + Hdf5Handle memory_coordinates{ + RequireId(H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), + "Unable to create coordinate memory type."), + H5Tclose}; + Hdf5Handle file_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), + "Unable to create node file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)), + "Unable to create node memory type."), + H5Tclose}; + const auto insert = [&](const hid_t type, const hid_t integer_type, + const hid_t coordinate_type) { + RequireHdf(H5Tinsert(type, "internal_node_id", + HOFFSET(NodeWriteRow, internal_node_id), integer_type), + "Unable to define node ID."); + RequireHdf( + H5Tinsert(type, "instance_name", HOFFSET(NodeWriteRow, instance_name), + string_type.Get()), + "Unable to define node instance."); + RequireHdf( + H5Tinsert(type, "source_label", HOFFSET(NodeWriteRow, source_label), + string_type.Get()), + "Unable to define node label."); + RequireHdf(H5Tinsert(type, "coordinates", + HOFFSET(NodeWriteRow, coordinates), coordinate_type), + "Unable to define node coordinates."); + }; + insert(file_type.Get(), H5T_STD_U64LE, file_coordinates.Get()); + insert(memory_type.Get(), H5T_NATIVE_UINT64, memory_coordinates.Get()); + const hsize_t dimensions[] = {static_cast(rows.size())}; + Hdf5Handle space{RequireId(H5Screate_simple(1, dimensions, nullptr), + "Unable to create node space."), + H5Sclose}; + Hdf5Handle dataset{ + RequireId(H5Dcreate2(file, "/model/nodes", file_type.Get(), space.Get(), + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), + "Unable to create node dataset."), + H5Dclose}; + RequireHdf(H5Dwrite(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, + H5P_DEFAULT, rows.data()), + "Unable to write node dataset."); + WriteStringAttribute(dataset.Get(), "coordinate_system", "global-cartesian"); + WriteStringAttribute(dataset.Get(), "units_label", "length"); } -void writeElements(const hid_t file, const Hdf5Options& options) { - ElementWriteRow row{ - 0U, - kInstanceName, - "1", - options.sourceElementType.c_str(), - options.internalFormulation.c_str(), - {0U, 1U, 0U, 1U}, - 0U, - 0U}; - auto stringType = makeUtf8StringType(); - const hsize_t nodeDimensions[] = {4U}; - Hdf5Handle fileNodes{ - requireId( - H5Tarray_create2(H5T_STD_U64LE, 1, nodeDimensions), - "Unable to create element node file type."), - H5Tclose}; - Hdf5Handle memoryNodes{ - requireId( - H5Tarray_create2(H5T_NATIVE_UINT64, 1, nodeDimensions), - "Unable to create element node memory type."), - H5Tclose}; - Hdf5Handle fileType{ - requireId( - H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), - "Unable to create element file type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireId( - H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), - "Unable to create element memory type."), - H5Tclose}; - const auto insert = [&](const hid_t type, - const hid_t integerType, - const hid_t nodesType) { - requireHdf( - H5Tinsert( - type, "internal_element_id", - HOFFSET(ElementWriteRow, internalElementId), integerType), - "Unable to define element ID."); - requireHdf( - H5Tinsert( - type, "instance_name", HOFFSET(ElementWriteRow, instanceName), - stringType.get()), - "Unable to define element instance."); - requireHdf( - H5Tinsert( - type, "source_label", HOFFSET(ElementWriteRow, sourceLabel), - stringType.get()), - "Unable to define element label."); - requireHdf( - H5Tinsert( - type, "source_element_type", - HOFFSET(ElementWriteRow, sourceElementType), stringType.get()), - "Unable to define source element type."); - requireHdf( - H5Tinsert( - type, "internal_formulation", - HOFFSET(ElementWriteRow, internalFormulation), stringType.get()), - "Unable to define internal formulation."); - requireHdf( - H5Tinsert( - type, "node_internal_ids", - HOFFSET(ElementWriteRow, nodeInternalIds), nodesType), - "Unable to define element connectivity."); - requireHdf( - H5Tinsert( - type, "shell_section_internal_id", - HOFFSET(ElementWriteRow, shellSectionInternalId), integerType), - "Unable to define section ID."); - requireHdf( - H5Tinsert( - type, "material_internal_id", - HOFFSET(ElementWriteRow, materialInternalId), integerType), - "Unable to define material ID."); - }; - insert(fileType.get(), H5T_STD_U64LE, fileNodes.get()); - insert(memoryType.get(), H5T_NATIVE_UINT64, memoryNodes.get()); - const hsize_t dimensions[] = {1U}; - Hdf5Handle space{ - requireId( - H5Screate_simple(1, dimensions, nullptr), - "Unable to create element space."), - H5Sclose}; - Hdf5Handle dataset{ - requireId( - H5Dcreate2( - file, "/model/elements", fileType.get(), space.get(), - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), - "Unable to create element dataset."), - H5Dclose}; - requireHdf( - H5Dwrite( - dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, - &row), - "Unable to write element dataset."); - writeStringAttribute(dataset.get(), "formulation", kInternalFormulation); +void WriteElements(const hid_t file, const Hdf5Options& options) { + ElementWriteRow row{0U, + kInstanceName, + "1", + options.source_element_type.c_str(), + options.internal_formulation.c_str(), + {0U, 1U, 0U, 1U}, + 0U, + 0U}; + auto string_type = MakeUtf8StringType(); + const hsize_t node_dimensions[] = {4U}; + Hdf5Handle file_nodes{ + RequireId(H5Tarray_create2(H5T_STD_U64LE, 1, node_dimensions), + "Unable to create element node file type."), + H5Tclose}; + Hdf5Handle memory_nodes{ + RequireId(H5Tarray_create2(H5T_NATIVE_UINT64, 1, node_dimensions), + "Unable to create element node memory type."), + H5Tclose}; + Hdf5Handle file_type{ + RequireId(H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), + "Unable to create element file type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireId(H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)), + "Unable to create element memory type."), + H5Tclose}; + const auto insert = [&](const hid_t type, const hid_t integer_type, + const hid_t nodes_type) { + RequireHdf( + H5Tinsert(type, "internal_element_id", + HOFFSET(ElementWriteRow, internal_element_id), integer_type), + "Unable to define element ID."); + RequireHdf( + H5Tinsert(type, "instance_name", + HOFFSET(ElementWriteRow, instance_name), string_type.Get()), + "Unable to define element instance."); + RequireHdf( + H5Tinsert(type, "source_label", HOFFSET(ElementWriteRow, source_label), + string_type.Get()), + "Unable to define element label."); + RequireHdf(H5Tinsert(type, "source_element_type", + HOFFSET(ElementWriteRow, source_element_type), + string_type.Get()), + "Unable to define source element type."); + RequireHdf(H5Tinsert(type, "internal_formulation", + HOFFSET(ElementWriteRow, internal_formulation), + string_type.Get()), + "Unable to define internal formulation."); + RequireHdf( + H5Tinsert(type, "node_internal_ids", + HOFFSET(ElementWriteRow, node_internal_ids), nodes_type), + "Unable to define element connectivity."); + RequireHdf(H5Tinsert(type, "shell_section_internal_id", + HOFFSET(ElementWriteRow, shell_section_internal_id), + integer_type), + "Unable to define section ID."); + RequireHdf( + H5Tinsert(type, "material_internal_id", + HOFFSET(ElementWriteRow, material_internal_id), integer_type), + "Unable to define material ID."); + }; + insert(file_type.Get(), H5T_STD_U64LE, file_nodes.Get()); + insert(memory_type.Get(), H5T_NATIVE_UINT64, memory_nodes.Get()); + const hsize_t dimensions[] = {1U}; + Hdf5Handle space{RequireId(H5Screate_simple(1, dimensions, nullptr), + "Unable to create element space."), + H5Sclose}; + Hdf5Handle dataset{ + RequireId(H5Dcreate2(file, "/model/elements", file_type.Get(), + space.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), + "Unable to create element dataset."), + H5Dclose}; + RequireHdf(H5Dwrite(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, + H5P_DEFAULT, &row), + "Unable to write element dataset."); + WriteStringAttribute(dataset.Get(), "formulation", kInternalFormulation); } -void writeDisplacement( - const hid_t file, - const std::vector& values, - const Hdf5Options& options) { - std::vector flattened; - flattened.reserve(values.size() * kComponents.size()); - for (const auto& row : values) { - flattened.insert(flattened.end(), row.values.begin(), row.values.end()); - } - const hsize_t dimensions[] = { - static_cast(values.size()), - static_cast(kComponents.size())}; - Hdf5Handle space{ - requireId( - H5Screate_simple(2, dimensions, nullptr), - "Unable to create displacement space."), - H5Sclose}; - Hdf5Handle dataset{ - requireId( - H5Dcreate2( - file, kDisplacementPath, H5T_IEEE_F64LE, space.get(), - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), - "Unable to create displacement dataset."), - H5Dclose}; - requireHdf( - H5Dwrite( - dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, flattened.data()), - "Unable to write displacement dataset."); - writeStringAttribute( - dataset.get(), "component_names", options.displacementComponents); - writeStringAttribute( - dataset.get(), "component_unit_dimensions", - "length,length,length,radian,radian,radian"); - writeStringAttribute(dataset.get(), "coordinate_system", "global-cartesian"); - writeStringAttribute(dataset.get(), "location", "nodal"); - writeStringAttribute(dataset.get(), "step_name", "Step-1"); - writeUint64Attribute(dataset.get(), "frame_index", 0U); +void WriteDisplacement(const hid_t file, + const std::vector& values, + const Hdf5Options& options) { + std::vector flattened; + flattened.reserve(values.size() * kComponents.size()); + for (const auto& row : values) { + flattened.insert(flattened.end(), row.values.begin(), row.values.end()); + } + const hsize_t dimensions[] = {static_cast(values.size()), + static_cast(kComponents.size())}; + Hdf5Handle space{RequireId(H5Screate_simple(2, dimensions, nullptr), + "Unable to create displacement space."), + H5Sclose}; + Hdf5Handle dataset{ + RequireId(H5Dcreate2(file, kDisplacementPath, H5T_IEEE_F64LE, space.Get(), + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), + "Unable to create displacement dataset."), + H5Dclose}; + RequireHdf(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, + H5P_DEFAULT, flattened.data()), + "Unable to write displacement dataset."); + WriteStringAttribute(dataset.Get(), "component_names", + options.displacement_components); + WriteStringAttribute(dataset.Get(), "component_unit_dimensions", + "length,length,length,radian,radian,radian"); + WriteStringAttribute(dataset.Get(), "coordinate_system", "global-cartesian"); + WriteStringAttribute(dataset.Get(), "location", "nodal"); + WriteStringAttribute(dataset.Get(), "step_name", "Step-1"); + WriteUint64Attribute(dataset.Get(), "frame_index", 0U); } -void writeHdf5( - const std::filesystem::path& path, - const std::filesystem::path& inputPath, - const std::vector& values, - const Hdf5Options& options = {}) { - Hdf5Handle file{ - requireId( - H5Fcreate( - path.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT), - "Unable to create comparison HDF5 fixture."), - H5Fclose}; - auto metadata = createGroup(file.get(), "/metadata"); - auto model = createGroup(file.get(), "/model"); - auto steps = createGroup(file.get(), "/steps"); - auto step = createGroup(file.get(), "/steps/Step-1"); - auto frames = createGroup(file.get(), "/steps/Step-1/frames"); - auto frame = createGroup(file.get(), "/steps/Step-1/frames/0"); - auto nodal = createGroup(file.get(), "/steps/Step-1/frames/0/nodal"); - (void)model; - (void)steps; - (void)step; - (void)frames; - (void)frame; - (void)nodal; +void WriteHdf5(const std::filesystem::path& path, + const std::filesystem::path& input_path, + const std::vector& values, + const Hdf5Options& options = {}) { + Hdf5Handle file{RequireId(H5Fcreate(path.string().c_str(), H5F_ACC_TRUNC, + H5P_DEFAULT, H5P_DEFAULT), + "Unable to create comparison HDF5 fixture."), + H5Fclose}; + auto metadata = CreateGroup(file.Get(), "/metadata"); + auto model = CreateGroup(file.Get(), "/model"); + auto steps = CreateGroup(file.Get(), "/steps"); + auto step = CreateGroup(file.Get(), "/steps/Step-1"); + auto frames = CreateGroup(file.Get(), "/steps/Step-1/frames"); + auto frame = CreateGroup(file.Get(), "/steps/Step-1/frames/0"); + auto nodal = CreateGroup(file.Get(), "/steps/Step-1/frames/0/nodal"); + (void)model; + (void)steps; + (void)step; + (void)frames; + (void)frame; + (void)nodal; - writeUint64Attribute(metadata.get(), "schema_version", 0U); - writeStringAttribute(metadata.get(), "feature_id", "linear-static-mitc4-shell"); - writeStringAttribute( - metadata.get(), "source_input_identity", - "path=" + std::filesystem::absolute(inputPath) - .lexically_normal() - .generic_u8string() + - ";content_identity=test"); - writeStringAttribute( - metadata.get(), "internal_formulation", options.internalFormulation); - writeStringAttribute( - metadata.get(), "integration_rule", options.integrationRule); - writeStringAttribute(metadata.get(), "step_name", "Step-1"); - writeUint64Attribute(metadata.get(), "frame_index", 0U); - writeNodes(file.get(), values); - writeElements(file.get(), options); - writeDisplacement(file.get(), values, options); + WriteUint64Attribute(metadata.Get(), "schema_version", 0U); + WriteStringAttribute(metadata.Get(), "feature_id", + "linear-static-mitc4-shell"); + WriteStringAttribute(metadata.Get(), "source_input_identity", + "path=" + + std::filesystem::absolute(input_path) + .lexically_normal() + .generic_u8string() + + ";content_identity=test"); + WriteStringAttribute(metadata.Get(), "internal_formulation", + options.internal_formulation); + WriteStringAttribute(metadata.Get(), "integration_rule", + options.integration_rule); + WriteStringAttribute(metadata.Get(), "step_name", "Step-1"); + WriteUint64Attribute(metadata.Get(), "frame_index", 0U); + WriteNodes(file.Get(), values); + WriteElements(file.Get(), options); + WriteDisplacement(file.Get(), values, options); } -void writeCsv( +void WriteCsv( const std::filesystem::path& path, const std::vector& rows, const std::string& header = "Part Instance Name,Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3") { - std::ofstream stream{path, std::ios::binary | std::ios::trunc}; - if (!stream) { - throw std::runtime_error{"Unable to create comparison CSV fixture."}; - } - stream << header << '\n' << std::setprecision(17); - for (const auto& row : rows) { - stream << row.instanceName << ',' << row.sourceNodeLabel; - for (const double value : row.values) { - stream << ',' << value; - } - stream << '\n'; + std::ofstream stream{path, std::ios::binary | std::ios::trunc}; + if (!stream) { + throw std::runtime_error{"Unable to create comparison CSV fixture."}; + } + stream << header << '\n' << std::setprecision(17); + for (const auto& row : rows) { + stream << row.instance_name << ',' << row.source_node_label; + for (const double value : row.values) { + stream << ',' << value; } + stream << '\n'; + } } class ContractFixture { -public: - explicit ContractFixture(const std::string& name) - : root_{std::filesystem::path{FESA_TEST_BINARY_DIR} / - "reference" / ("mitc4-comparator-" + name)}, - input_{root_ / "case.inp"}, - csv_{root_ / "displacements.csv"}, - results_{root_ / "results.h5"} { - std::error_code error; - std::filesystem::remove_all(root_, error); - error.clear(); - if (!std::filesystem::create_directories(root_, error) || error) { - throw std::runtime_error{"Unable to create comparison fixture directory."}; - } - std::ofstream inputStream{input_, std::ios::binary | std::ios::trunc}; - inputStream << "*Element, type=S4\n1,1,2,1,2\n"; - inputStream.close(); - writeCsv(csv_, defaultRows()); - writeHdf5(results_, input_, defaultRows()); + public: + explicit ContractFixture(const std::string& name) + : root_{std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / + ("mitc4-comparator-" + name)}, + input_{root_ / "case.inp"}, + csv_{root_ / "displacements.csv"}, + results_{root_ / "results.h5"} { + std::error_code error; + std::filesystem::remove_all(root_, error); + error.clear(); + if (!std::filesystem::create_directories(root_, error) || error) { + throw std::runtime_error{ + "Unable to create comparison fixture directory."}; } + std::ofstream input_stream{input_, std::ios::binary | std::ios::trunc}; + input_stream << "*Element, type=S4\n1,1,2,1,2\n"; + input_stream.close(); + WriteCsv(csv_, DefaultRows()); + WriteHdf5(results_, input_, DefaultRows()); + } - ~ContractFixture() { - std::error_code error; - std::filesystem::remove_all(root_, error); - } + ~ContractFixture() { + std::error_code error; + std::filesystem::remove_all(root_, error); + } - fesa::test::Mitc4ReferenceCase referenceCase() const { - return {"shell-contract", "S4", input_, csv_, results_}; - } + fesa::test::Mitc4ReferenceCase ReferenceCase() const { + return {"shell-contract", "S4", input_, csv_, results_}; + } - const std::filesystem::path& root() const noexcept { return root_; } - const std::filesystem::path& input() const noexcept { return input_; } - const std::filesystem::path& csv() const noexcept { return csv_; } - const std::filesystem::path& results() const noexcept { return results_; } + const std::filesystem::path& Root() const noexcept { return root_; } + const std::filesystem::path& Input() const noexcept { return input_; } + const std::filesystem::path& Csv() const noexcept { return csv_; } + const std::filesystem::path& Results() const noexcept { return results_; } -private: - std::filesystem::path root_; - std::filesystem::path input_; - std::filesystem::path csv_; - std::filesystem::path results_; + private: + std::filesystem::path root_; + std::filesystem::path input_; + std::filesystem::path csv_; + std::filesystem::path results_; }; -void expectFailureCode( +void ExpectFailureCode( const fesa::Result& result, const std::string& code) { - ASSERT_FALSE(result.HasValue()); - ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); - EXPECT_EQ(result.GetStatus().Diagnostics().front().code, code); + ASSERT_FALSE(result.HasValue()); + ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); + EXPECT_EQ(result.GetStatus().Diagnostics().front().code, code); } -const fesa::test::Mitc4RowDecision* findRow( - const fesa::test::Mitc4ComparisonReport& report, - const std::int64_t label, +const fesa::test::Mitc4RowDecision* FindRow( + const fesa::test::Mitc4ComparisonReport& report, const std::int64_t label, const std::string& component) { - const auto found = std::find_if( - report.rows.begin(), report.rows.end(), - [&](const fesa::test::Mitc4RowDecision& row) { - return row.sourceNodeLabel == label && row.component == component; - }); - return found == report.rows.end() ? nullptr : &*found; + const auto found = std::find_if(report.rows.begin(), report.rows.end(), + [&](const fesa::test::Mitc4RowDecision& row) { + return row.source_node_label == label && + row.component == component; + }); + return found == report.rows.end() ? nullptr : &*found; } -const fesa::test::Mitc4ComponentMetrics* findMetric( +const fesa::test::Mitc4ComponentMetrics* FindMetric( const fesa::test::Mitc4ComparisonReport& report, const std::string& component) { - const auto found = std::find_if( - report.metrics.begin(), report.metrics.end(), - [&](const fesa::test::Mitc4ComponentMetrics& metric) { - return metric.component == component; - }); - return found == report.metrics.end() ? nullptr : &*found; + const auto found = + std::find_if(report.metrics.begin(), report.metrics.end(), + [&](const fesa::test::Mitc4ComponentMetrics& metric) { + return metric.component == component; + }); + return found == report.metrics.end() ? nullptr : &*found; } -std::string readBytes(const std::filesystem::path& path) { - std::ifstream stream{path, std::ios::binary}; - return {std::istreambuf_iterator{stream}, - std::istreambuf_iterator{}}; +std::string ReadBytes(const std::filesystem::path& path) { + std::ifstream stream{path, std::ios::binary}; + return {std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; } // MITC4-REF-001 -TEST(Mitc4ReferenceComparison, MapsTrimmedHeaderAndSixComponentsBySourceIdentity) { - ContractFixture fixture{"mapping"}; - auto csvRows = defaultRows(); - std::reverse(csvRows.begin(), csvRows.end()); - for (auto& row : csvRows) { - row.instanceName = "part-1-1"; - } - writeCsv( - fixture.csv(), csvRows, - " Part Instance Name , Node Label , U-U1 , U-U2 , U-U3 , " - "UR-UR1 , UR-UR2 , UR-UR3 "); +TEST(Mitc4ReferenceComparison, + MapsTrimmedHeaderAndSixComponentsBySourceIdentity) { + ContractFixture fixture{"mapping"}; + auto csv_rows = DefaultRows(); + std::reverse(csv_rows.begin(), csv_rows.end()); + for (auto& row : csv_rows) { + row.instance_name = "part-1-1"; + } + WriteCsv(fixture.Csv(), csv_rows, + " Part Instance Name , Node Label , U-U1 , U-U2 , U-U3 , " + "UR-UR1 , UR-UR2 , UR-UR3 "); - auto result = fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()); - ASSERT_TRUE(result.HasValue()); - const auto& report = result.Value(); - ASSERT_TRUE(report.passed); - ASSERT_EQ(report.rows.size(), 12U); - EXPECT_EQ(report.rows[0U].sourceNodeLabel, 1); - EXPECT_EQ(report.rows[0U].component, "U1"); - EXPECT_EQ(report.rows[5U].component, "UR3"); - EXPECT_EQ(report.rows[6U].sourceNodeLabel, 2); - EXPECT_TRUE(std::all_of( - report.rows.begin(), report.rows.end(), - [](const fesa::test::Mitc4RowDecision& row) { - return row.caseId == "shell-contract" && - row.instanceName == kInstanceName && row.withinTolerance; - })); + auto result = + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()); + ASSERT_TRUE(result.HasValue()); + const auto& report = result.Value(); + ASSERT_TRUE(report.passed); + ASSERT_EQ(report.rows.size(), 12U); + EXPECT_EQ(report.rows[0U].source_node_label, 1); + EXPECT_EQ(report.rows[0U].component, "U1"); + EXPECT_EQ(report.rows[5U].component, "UR3"); + EXPECT_EQ(report.rows[6U].source_node_label, 2); + EXPECT_TRUE(std::all_of(report.rows.begin(), report.rows.end(), + [](const fesa::test::Mitc4RowDecision& row) { + return row.case_id == "shell-contract" && + row.instance_name == kInstanceName && + row.within_tolerance; + })); } // MITC4-REF-002 TEST(Mitc4ReferenceComparison, RejectsInvalidInventoryBeforeNumericComparison) { - { - ContractFixture fixture{"missing-input"}; - ASSERT_TRUE(std::filesystem::remove(fixture.input())); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "needs-reference-artifacts"); - } - { - ContractFixture fixture{"header"}; - writeCsv( - fixture.csv(), defaultRows(), - "Part Instance Name,Node Label,U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3"); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } - { - ContractFixture fixture{"missing-row"}; - auto rows = defaultRows(); - rows.pop_back(); - writeCsv(fixture.csv(), rows); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } - { - ContractFixture fixture{"extra-row"}; - auto rows = defaultRows(); - rows.push_back({kInstanceName, 3, {}}); - writeCsv(fixture.csv(), rows); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } - { - ContractFixture fixture{"duplicate-row"}; - auto rows = defaultRows(); - rows.push_back(rows.front()); - writeCsv(fixture.csv(), rows); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } - { - ContractFixture fixture{"nonfinite-csv"}; - auto rows = defaultRows(); - rows[0U].values[0U] = std::numeric_limits::quiet_NaN(); - writeCsv(fixture.csv(), rows); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } - { - ContractFixture fixture{"nonfinite-hdf5"}; - auto rows = defaultRows(); - rows[0U].values[0U] = std::numeric_limits::infinity(); - writeHdf5(fixture.results(), fixture.input(), rows); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } - { - ContractFixture fixture{"source-identity"}; - auto rows = defaultRows(); - rows[0U].instanceName = "WRONG-INSTANCE"; - writeCsv(fixture.csv(), rows); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } - { - ContractFixture fixture{"hdf5-schema"}; - Hdf5Options options; - options.displacementComponents = "U1,U2,U3,UR1,UR2,UR3"; - writeHdf5(fixture.results(), fixture.input(), defaultRows(), options); - expectFailureCode( - fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()), - "schema-mismatch"); - } + { + ContractFixture fixture{"missing-input"}; + ASSERT_TRUE(std::filesystem::remove(fixture.Input())); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "needs-reference-artifacts"); + } + { + ContractFixture fixture{"header"}; + WriteCsv(fixture.Csv(), DefaultRows(), + "Part Instance Name,Node Label,U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3"); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } + { + ContractFixture fixture{"missing-row"}; + auto rows = DefaultRows(); + rows.pop_back(); + WriteCsv(fixture.Csv(), rows); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } + { + ContractFixture fixture{"extra-row"}; + auto rows = DefaultRows(); + rows.push_back({kInstanceName, 3, {}}); + WriteCsv(fixture.Csv(), rows); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } + { + ContractFixture fixture{"duplicate-row"}; + auto rows = DefaultRows(); + rows.push_back(rows.front()); + WriteCsv(fixture.Csv(), rows); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } + { + ContractFixture fixture{"nonfinite-csv"}; + auto rows = DefaultRows(); + rows[0U].values[0U] = std::numeric_limits::quiet_NaN(); + WriteCsv(fixture.Csv(), rows); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } + { + ContractFixture fixture{"nonfinite-hdf5"}; + auto rows = DefaultRows(); + rows[0U].values[0U] = std::numeric_limits::infinity(); + WriteHdf5(fixture.Results(), fixture.Input(), rows); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } + { + ContractFixture fixture{"source-identity"}; + auto rows = DefaultRows(); + rows[0U].instance_name = "WRONG-INSTANCE"; + WriteCsv(fixture.Csv(), rows); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } + { + ContractFixture fixture{"hdf5-schema"}; + Hdf5Options options; + options.displacement_components = "U1,U2,U3,UR1,UR2,UR3"; + WriteHdf5(fixture.Results(), fixture.Input(), DefaultRows(), options); + ExpectFailureCode( + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()), + "schema-mismatch"); + } } // MITC4-REF-003 -TEST(Mitc4ReferenceComparison, AppliesFixedAbsoluteToleranceWithoutScaleClampOrRowDenominator) { - ContractFixture fixture{"tolerance"}; - auto reference = defaultRows(); - reference[0U].values[0U] = 0.0; - reference[1U].values[0U] = 2.0; - reference[0U].values[1U] = 0.0; - reference[1U].values[1U] = 0.0; - writeCsv(fixture.csv(), reference); - auto fesaValues = reference; - fesaValues[0U].values[0U] = 0.9999e-5; - fesaValues[1U].values[0U] += 1.0001e-5; - fesaValues[0U].values[1U] = 0.9999e-5; - fesaValues[1U].values[1U] = 1.0001e-5; - writeHdf5(fixture.results(), fixture.input(), fesaValues); +TEST(Mitc4ReferenceComparison, + AppliesFixedAbsoluteToleranceWithoutScaleClampOrRowDenominator) { + ContractFixture fixture{"tolerance"}; + auto reference = DefaultRows(); + reference[0U].values[0U] = 0.0; + reference[1U].values[0U] = 2.0; + reference[0U].values[1U] = 0.0; + reference[1U].values[1U] = 0.0; + WriteCsv(fixture.Csv(), reference); + auto fesa_values = reference; + fesa_values[0U].values[0U] = 0.9999e-5; + fesa_values[1U].values[0U] += 1.0001e-5; + fesa_values[0U].values[1U] = 0.9999e-5; + fesa_values[1U].values[1U] = 1.0001e-5; + WriteHdf5(fixture.Results(), fixture.Input(), fesa_values); - auto result = fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()); - ASSERT_TRUE(result.HasValue()); - const auto& report = result.Value(); - EXPECT_FALSE(report.passed); - const auto* u1Zero = findRow(report, 1, "U1"); - const auto* u1Scaled = findRow(report, 2, "U1"); - const auto* u2Near = findRow(report, 1, "U2"); - const auto* u2Over = findRow(report, 2, "U2"); - ASSERT_NE(u1Zero, nullptr); - ASSERT_NE(u1Scaled, nullptr); - ASSERT_NE(u2Near, nullptr); - ASSERT_NE(u2Over, nullptr); - EXPECT_DOUBLE_EQ(u1Zero->tolerance, 1.0e-5); - EXPECT_TRUE(u1Zero->withinTolerance); - EXPECT_NEAR(u1Zero->normalizedError, 0.9999, 1.0e-12); - EXPECT_FALSE(u1Scaled->withinTolerance); - EXPECT_NEAR(u1Scaled->normalizedError, 1.0001, 2.0e-11); - EXPECT_DOUBLE_EQ(u2Near->tolerance, 1.0e-5); - EXPECT_TRUE(u2Near->withinTolerance); - EXPECT_NEAR(u2Near->normalizedError, 0.9999, 1.0e-12); - EXPECT_FALSE(u2Over->withinTolerance); - EXPECT_NEAR(u2Over->normalizedError, 1.0001, 1.0e-12); - for (const auto& row : report.rows) { - EXPECT_DOUBLE_EQ(row.tolerance, 1.0e-5); - } - for (const auto& metric : report.metrics) { - EXPECT_DOUBLE_EQ(metric.tolerance, 1.0e-5); - } - const auto* u1Metric = findMetric(report, "U1"); - ASSERT_NE(u1Metric, nullptr); - EXPECT_DOUBLE_EQ(u1Metric->referenceScale, 2.0); + auto result = + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()); + ASSERT_TRUE(result.HasValue()); + const auto& report = result.Value(); + EXPECT_FALSE(report.passed); + const auto* u1_zero = FindRow(report, 1, "U1"); + const auto* u1_scaled = FindRow(report, 2, "U1"); + const auto* u2_near = FindRow(report, 1, "U2"); + const auto* u2_over = FindRow(report, 2, "U2"); + ASSERT_NE(u1_zero, nullptr); + ASSERT_NE(u1_scaled, nullptr); + ASSERT_NE(u2_near, nullptr); + ASSERT_NE(u2_over, nullptr); + EXPECT_DOUBLE_EQ(u1_zero->tolerance, 1.0e-5); + EXPECT_TRUE(u1_zero->within_tolerance); + EXPECT_NEAR(u1_zero->normalized_error, 0.9999, 1.0e-12); + EXPECT_FALSE(u1_scaled->within_tolerance); + EXPECT_NEAR(u1_scaled->normalized_error, 1.0001, 2.0e-11); + EXPECT_DOUBLE_EQ(u2_near->tolerance, 1.0e-5); + EXPECT_TRUE(u2_near->within_tolerance); + EXPECT_NEAR(u2_near->normalized_error, 0.9999, 1.0e-12); + EXPECT_FALSE(u2_over->within_tolerance); + EXPECT_NEAR(u2_over->normalized_error, 1.0001, 1.0e-12); + for (const auto& row : report.rows) { + EXPECT_DOUBLE_EQ(row.tolerance, 1.0e-5); + } + for (const auto& metric : report.metrics) { + EXPECT_DOUBLE_EQ(metric.tolerance, 1.0e-5); + } + const auto* u1_metric = FindMetric(report, "U1"); + ASSERT_NE(u1_metric, nullptr); + EXPECT_DOUBLE_EQ(u1_metric->reference_scale, 2.0); } // MITC4-REF-004 -TEST(Mitc4ReferenceComparison, RotationExceedanceWarnsWithoutBlockingTranslationVerdict) { - ContractFixture fixture{"warning"}; - auto fesaValues = defaultRows(); - fesaValues[0U].values[3U] = 1.0; - writeHdf5(fixture.results(), fixture.input(), fesaValues); +TEST(Mitc4ReferenceComparison, + RotationExceedanceWarnsWithoutBlockingTranslationVerdict) { + ContractFixture fixture{"warning"}; + auto fesa_values = DefaultRows(); + fesa_values[0U].values[3U] = 1.0; + WriteHdf5(fixture.Results(), fixture.Input(), fesa_values); - auto result = fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()); - ASSERT_TRUE(result.HasValue()); - const auto& report = result.Value(); - EXPECT_TRUE(report.passed); - ASSERT_EQ(report.warnings.size(), 1U); - EXPECT_EQ(report.warnings[0U].code, "rotation-reference-exceedance"); - const auto* row = findRow(report, 1, "UR1"); - ASSERT_NE(row, nullptr); - EXPECT_FALSE(row->blocking); - EXPECT_FALSE(row->withinTolerance); - EXPECT_EQ(report.warnings[0U].row, - static_cast(row - report.rows.data())); - EXPECT_NE(report.warnings[0U].message.find("shell-contract"), - std::string::npos); - EXPECT_NE(report.warnings[0U].message.find("UR1"), std::string::npos); + auto result = + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()); + ASSERT_TRUE(result.HasValue()); + const auto& report = result.Value(); + EXPECT_TRUE(report.passed); + ASSERT_EQ(report.warnings.size(), 1U); + EXPECT_EQ(report.warnings[0U].code, "rotation-reference-exceedance"); + const auto* row = FindRow(report, 1, "UR1"); + ASSERT_NE(row, nullptr); + EXPECT_FALSE(row->blocking); + EXPECT_FALSE(row->within_tolerance); + EXPECT_EQ(report.warnings[0U].row, + static_cast(row - report.rows.data())); + EXPECT_NE(report.warnings[0U].message.find("shell-contract"), + std::string::npos); + EXPECT_NE(report.warnings[0U].message.find("UR1"), std::string::npos); } // MITC4-REF-005 -TEST(Mitc4ReferenceComparison, ReportsMetricsVectorsWorstRowAndJsonDeterministically) { - ContractFixture fixture{"report"}; - auto fesaValues = defaultRows(); - fesaValues[0U].values[0U] = 0.5e-9; - fesaValues[0U].values[1U] = -0.25e-9; - fesaValues[0U].values[3U] = 0.75e-9; - writeHdf5(fixture.results(), fixture.input(), fesaValues); +TEST(Mitc4ReferenceComparison, + ReportsMetricsVectorsWorstRowAndJsonDeterministically) { + ContractFixture fixture{"report"}; + auto fesa_values = DefaultRows(); + fesa_values[0U].values[0U] = 0.5e-9; + fesa_values[0U].values[1U] = -0.25e-9; + fesa_values[0U].values[3U] = 0.75e-9; + WriteHdf5(fixture.Results(), fixture.Input(), fesa_values); - auto result = fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()); - ASSERT_TRUE(result.HasValue()); - const auto& report = result.Value(); - ASSERT_TRUE(report.passed); - ASSERT_EQ(report.metrics.size(), 6U); - ASSERT_EQ(report.vectorMetrics.size(), 2U); - EXPECT_NEAR( - report.vectorMetrics[0U].displacementNormError, - std::sqrt(0.3125) * 1.0e-9, - 1.0e-21); - EXPECT_DOUBLE_EQ( - report.vectorMetrics[0U].rotationNormError, 0.75e-9); - ASSERT_LT(report.worstRow, report.rows.size()); - EXPECT_EQ(report.rows[report.worstRow].component, "UR1"); - for (const auto& metric : report.metrics) { - EXPECT_TRUE(std::isfinite(metric.referenceScale)); - EXPECT_TRUE(std::isfinite(metric.maximumAbsoluteError)); - EXPECT_TRUE(std::isfinite(metric.maximumNormalizedError)); - EXPECT_TRUE(std::isfinite(metric.rmsError)); - EXPECT_TRUE(std::isfinite(metric.vectorNormError)); - EXPECT_LT(metric.worstRow, report.rows.size()); - } + auto result = + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()); + ASSERT_TRUE(result.HasValue()); + const auto& report = result.Value(); + ASSERT_TRUE(report.passed); + ASSERT_EQ(report.metrics.size(), 6U); + ASSERT_EQ(report.vector_metrics.size(), 2U); + EXPECT_NEAR(report.vector_metrics[0U].displacement_norm_error, + std::sqrt(0.3125) * 1.0e-9, 1.0e-21); + EXPECT_DOUBLE_EQ(report.vector_metrics[0U].rotation_norm_error, 0.75e-9); + ASSERT_LT(report.worst_row, report.rows.size()); + EXPECT_EQ(report.rows[report.worst_row].component, "UR1"); + for (const auto& metric : report.metrics) { + EXPECT_TRUE(std::isfinite(metric.reference_scale)); + EXPECT_TRUE(std::isfinite(metric.maximum_absolute_error)); + EXPECT_TRUE(std::isfinite(metric.maximum_normalized_error)); + EXPECT_TRUE(std::isfinite(metric.rms_error)); + EXPECT_TRUE(std::isfinite(metric.vector_norm_error)); + EXPECT_LT(metric.worst_row, report.rows.size()); + } - const auto jsonA = fixture.root() / "comparison-a.json"; - const auto jsonB = fixture.root() / "comparison-b.json"; - ASSERT_TRUE( - fesa::test::Mitc4ReferenceComparison::writeDeterministicJson( - report, jsonA) - .IsOk()); - ASSERT_TRUE( - fesa::test::Mitc4ReferenceComparison::writeDeterministicJson( - report, jsonB) - .IsOk()); - const std::string first = readBytes(jsonA); - EXPECT_EQ(first, readBytes(jsonB)); - for (const char* key : { - "\"rows\"", "\"metrics\"", "\"vector_metrics\"", - "\"warnings\"", "\"worst_row\"", "\"passed\""}) { - EXPECT_NE(first.find(key), std::string::npos) << key; - } + const auto json_a = fixture.Root() / "comparison-a.json"; + const auto json_b = fixture.Root() / "comparison-b.json"; + ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson( + report, json_a) + .IsOk()); + ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson( + report, json_b) + .IsOk()); + const std::string first = ReadBytes(json_a); + EXPECT_EQ(first, ReadBytes(json_b)); + for (const char* key : {"\"rows\"", "\"metrics\"", "\"vector_metrics\"", + "\"warnings\"", "\"worst_row\"", "\"passed\""}) { + EXPECT_NE(first.find(key), std::string::npos) << key; + } } // MITC4-REF-006 TEST(Mitc4ReferenceComparison, RequiresOnlyDeclaredInputCsvAndHdf5) { - ContractFixture fixture{"minimal-artifacts"}; - std::vector names; - for (const auto& entry : std::filesystem::directory_iterator{fixture.root()}) { - names.push_back(entry.path().filename().string()); - } - std::sort(names.begin(), names.end()); - EXPECT_EQ( - names, - (std::vector{"case.inp", "displacements.csv", "results.h5"})); - auto result = fesa::test::Mitc4ReferenceComparison::compare( - fixture.referenceCase()); - ASSERT_TRUE(result.HasValue()); - EXPECT_TRUE(result.Value().passed); + ContractFixture fixture{"minimal-artifacts"}; + std::vector names; + for (const auto& entry : + std::filesystem::directory_iterator{fixture.Root()}) { + names.push_back(entry.path().filename().string()); + } + std::sort(names.begin(), names.end()); + EXPECT_EQ(names, (std::vector{"case.inp", "displacements.csv", + "results.h5"})); + auto result = + fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()); + ASSERT_TRUE(result.HasValue()); + EXPECT_TRUE(result.Value().passed); } -} // namespace +} // namespace diff --git a/tests/reference/reference_comparison.cpp b/tests/reference/reference_comparison.cpp index 22bba12..e3f27b9 100644 --- a/tests/reference/reference_comparison.cpp +++ b/tests/reference/reference_comparison.cpp @@ -1,18 +1,11 @@ -#include "reference_comparison.hpp" - -#include "fesa/analysis/analysis_model.h" -#include "fesa/assembly/load_assembler.h" -#include "fesa/fem/dof_manager.h" -#include "fesa/io/abaqus/domain_mapper.hpp" -#include "fesa/io/abaqus/input_reader.hpp" -#include "fesa/results/result_recovery.h" +#include "reference_comparison.h" #include #include #include -#include #include +#include #include #include #include @@ -31,6 +24,13 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/assembly/load_assembler.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/io/abaqus/domain_mapper.h" +#include "fesa/io/abaqus/input_reader.h" +#include "fesa/results/result_recovery.h" + namespace fesa::test { namespace { @@ -44,1467 +44,1377 @@ constexpr const char* kReactionName = "cantilever beam reactions.csv"; constexpr const char* kSectionName = "cantilever beam elemental forces.csv"; constexpr const char* kDisplacementPath = "/steps/Step-1/frames/0/nodal/displacement"; -constexpr const char* kReactionPath = - "/steps/Step-1/frames/0/nodal/reaction"; +constexpr const char* kReactionPath = "/steps/Step-1/frames/0/nodal/reaction"; constexpr const char* kSectionPath = "/steps/Step-1/frames/0/element/section_resultant"; -constexpr const char* kStressPath = - "/steps/Step-1/frames/0/element/stress_s11"; +constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11"; constexpr double kKinematicFloor = 1.0e-9; constexpr double kForceMomentFloor = 1.0e-3; constexpr double kRelativeCoefficient = 1.0e-6; class ComparisonFailure final : public std::runtime_error { -public: - ComparisonFailure(std::string code, std::string message) - : std::runtime_error{std::move(message)}, code_{std::move(code)} {} + public: + ComparisonFailure(std::string code, std::string message) + : std::runtime_error{std::move(message)}, code_{std::move(code)} {} - const std::string& code() const noexcept { return code_; } + const std::string& Code() const noexcept { return code_; } -private: - std::string code_; + private: + std::string code_; }; -[[noreturn]] void fail(const std::string& code, const std::string& message) { - throw ComparisonFailure{code, message}; +[[noreturn]] void Fail(const std::string& code, const std::string& message) { + throw ComparisonFailure{code, message}; } -Status comparisonFailureStatus( - const std::string& code, const std::string& message) { - return Status::Failure( - FailureCategory::kModel, - {{Severity::kError, code, {}, "", kModelId, message}}); +Status ComparisonFailureStatus(const std::string& code, + const std::string& message) { + return Status::Failure(FailureCategory::kModel, + {{Severity::kError, code, {}, "", kModelId, message}}); } -std::string trim(const std::string& value) { - const auto isSpace = [](const unsigned char character) { - return std::isspace(character) != 0; - }; - const auto begin = std::find_if_not( - value.begin(), value.end(), [&](const char character) { - return isSpace(static_cast(character)); - }); - const auto end = std::find_if_not( - value.rbegin(), value.rend(), [&](const char character) { - return isSpace(static_cast(character)); - }).base(); - return begin < end ? std::string{begin, end} : std::string{}; +std::string Trim(const std::string& value) { + const auto is_space = [](const unsigned char character) { + return std::isspace(character) != 0; + }; + const auto begin = + std::find_if_not(value.begin(), value.end(), [&](const char character) { + return is_space(static_cast(character)); + }); + const auto end = + std::find_if_not(value.rbegin(), value.rend(), [&](const char character) { + return is_space(static_cast(character)); + }).base(); + return begin < end ? std::string{begin, end} : std::string{}; } -std::string collapseWhitespace(const std::string& value) { - std::string result; - bool pendingSpace = false; - for (const char character : trim(value)) { - if (std::isspace(static_cast(character)) != 0) { - pendingSpace = !result.empty(); - } else { - if (pendingSpace) { - result.push_back(' '); - } - result.push_back(character); - pendingSpace = false; - } +std::string CollapseWhitespace(const std::string& value) { + std::string result; + bool pending_space = false; + for (const char character : Trim(value)) { + if (std::isspace(static_cast(character)) != 0) { + pending_space = !result.empty(); + } else { + if (pending_space) { + result.push_back(' '); + } + result.push_back(character); + pending_space = false; } - return result; + } + return result; } -std::string asciiLower(std::string value) { - std::transform( - value.begin(), value.end(), value.begin(), [](const char character) { - if (character >= 'A' && character <= 'Z') { - return static_cast(character - 'A' + 'a'); - } - return character; - }); - return value; +std::string AsciiLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](const char character) { + if (character >= 'A' && character <= 'Z') { + return static_cast(character - 'A' + 'a'); + } + return character; + }); + return value; } -std::vector splitCsvLine(const std::string& line) { - std::vector fields; - std::size_t start = 0U; - while (true) { - const std::size_t comma = line.find(',', start); - fields.push_back(trim(line.substr(start, comma - start))); - if (comma == std::string::npos) { - break; - } - start = comma + 1U; +std::vector SplitCsvLine(const std::string& line) { + std::vector fields; + std::size_t start = 0U; + while (true) { + const std::size_t comma = line.find(',', start); + fields.push_back(Trim(line.substr(start, comma - start))); + if (comma == std::string::npos) { + break; } - return fields; + start = comma + 1U; + } + return fields; } -std::int64_t parsePositiveLabel(const std::string& field) { - std::int64_t value = 0; - const char* const begin = field.data(); - const char* const end = begin + field.size(); - const auto parsed = std::from_chars(begin, end, value); - if (parsed.ec != std::errc{} || parsed.ptr != end || value <= 0) { - fail("schema-mismatch", "A CSV or HDF5 source-node label is invalid."); - } - return value; +std::int64_t ParsePositiveLabel(const std::string& field) { + std::int64_t value = 0; + const char* const begin = field.data(); + const char* const end = begin + field.size(); + const auto parsed = std::from_chars(begin, end, value); + if (parsed.ec != std::errc{} || parsed.ptr != end || value <= 0) { + Fail("schema-mismatch", "A CSV or HDF5 source-node label is invalid."); + } + return value; } -double parseFiniteDouble(const std::string& field) { - if (field.empty()) { - fail("schema-mismatch", "A reference numeric field is empty."); - } - errno = 0; - char* end = nullptr; - const double value = std::strtod(field.c_str(), &end); - if (errno == ERANGE || end == field.c_str() || end == nullptr || - *end != '\0' || !std::isfinite(value)) { - fail("schema-mismatch", "A reference numeric field is nonfinite or invalid."); - } - return value; +double ParseFiniteDouble(const std::string& field) { + if (field.empty()) { + Fail("schema-mismatch", "A reference numeric field is empty."); + } + errno = 0; + char* end = nullptr; + const double value = std::strtod(field.c_str(), &end); + if (errno == ERANGE || end == field.c_str() || end == nullptr || + *end != '\0' || !std::isfinite(value)) { + Fail("schema-mismatch", + "A reference numeric field is nonfinite or invalid."); + } + return value; } struct WideReferenceRow { - std::string instanceName; - std::int64_t sourceNodeLabel; - std::vector values; + std::string instance_name; + std::int64_t source_node_label; + std::vector values; }; struct ReferenceTable { - std::vector rows; + std::vector rows; }; -ReferenceTable readReferenceCsv( +ReferenceTable ReadReferenceCsv( const std::filesystem::path& path, - const std::vector& expectedHeader) { - std::ifstream stream{path}; - if (!stream) { - fail("needs-reference-artifacts", "An approved reference CSV is missing."); - } - std::string line; - if (!std::getline(stream, line)) { - fail("schema-mismatch", "An approved reference CSV is empty."); - } + const std::vector& expected_header) { + std::ifstream stream{path}; + if (!stream) { + Fail("needs-reference-artifacts", "An approved reference CSV is missing."); + } + std::string line; + if (!std::getline(stream, line)) { + Fail("schema-mismatch", "An approved reference CSV is empty."); + } + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (SplitCsvLine(line) != expected_header) { + Fail("schema-mismatch", "An approved reference CSV header is not exact."); + } + + ReferenceTable table; + while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { - line.pop_back(); + line.pop_back(); } - if (splitCsvLine(line) != expectedHeader) { - fail("schema-mismatch", "An approved reference CSV header is not exact."); + if (line.empty()) { + Fail("schema-mismatch", "Blank reference CSV rows are not allowed."); } - - ReferenceTable table; - while (std::getline(stream, line)) { - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - if (line.empty()) { - fail("schema-mismatch", "Blank reference CSV rows are not allowed."); - } - const auto fields = splitCsvLine(line); - if (fields.size() != expectedHeader.size() || - collapseWhitespace(fields[0U]) != kFrameText || - fields[1U].empty()) { - fail("schema-mismatch", "A reference CSV row has invalid schema or frame identity."); - } - WideReferenceRow row{}; - row.instanceName = fields[1U]; - row.sourceNodeLabel = parsePositiveLabel(fields[2U]); - row.values.reserve(fields.size() - 3U); - for (std::size_t field = 3U; field < fields.size(); ++field) { - row.values.push_back(parseFiniteDouble(fields[field])); - } - const auto duplicate = std::find_if( - table.rows.begin(), - table.rows.end(), - [&](const WideReferenceRow& existing) { - return asciiLower(existing.instanceName) == - asciiLower(row.instanceName) && - existing.sourceNodeLabel == row.sourceNodeLabel; - }); - if (duplicate != table.rows.end()) { - fail("schema-mismatch", "A reference CSV row identity is duplicated."); - } - table.rows.push_back(std::move(row)); + const auto fields = SplitCsvLine(line); + if (fields.size() != expected_header.size() || + CollapseWhitespace(fields[0U]) != kFrameText || fields[1U].empty()) { + Fail("schema-mismatch", + "A reference CSV row has invalid schema or frame identity."); } - if (table.rows.empty()) { - fail("schema-mismatch", "An approved reference CSV has no data rows."); + WideReferenceRow row{}; + row.instance_name = fields[1U]; + row.source_node_label = ParsePositiveLabel(fields[2U]); + row.values.reserve(fields.size() - 3U); + for (std::size_t field = 3U; field < fields.size(); ++field) { + row.values.push_back(ParseFiniteDouble(fields[field])); } - return table; + const auto duplicate = std::find_if( + table.rows.begin(), table.rows.end(), + [&](const WideReferenceRow& existing) { + return AsciiLower(existing.instance_name) == + AsciiLower(row.instance_name) && + existing.source_node_label == row.source_node_label; + }); + if (duplicate != table.rows.end()) { + Fail("schema-mismatch", "A reference CSV row identity is duplicated."); + } + table.rows.push_back(std::move(row)); + } + if (table.rows.empty()) { + Fail("schema-mismatch", "An approved reference CSV has no data rows."); + } + return table; } -void requireExactArtifactInventory( - const std::filesystem::path& legacyDirectory) { - std::error_code error; - if (!std::filesystem::is_directory(legacyDirectory, error) || error) { - fail("needs-reference-artifacts", "The approved legacy directory is missing."); - } - std::vector names; - for (std::filesystem::directory_iterator iterator{legacyDirectory, error}, end; - iterator != end && !error; - iterator.increment(error)) { - if (!iterator->is_regular_file(error) || error) { - fail("needs-reference-artifacts", "The legacy bundle contains a non-file entry."); - } - names.push_back(iterator->path().filename().string()); - } - if (error) { - fail("needs-reference-artifacts", "The legacy bundle cannot be inspected."); - } - std::sort(names.begin(), names.end()); - std::vector expected = { - kDisplacementName, kInputName, kReactionName, kSectionName}; - std::sort(expected.begin(), expected.end()); - if (names != expected) { - fail("needs-reference-artifacts", "The legacy bundle inventory is not exact."); +void RequireExactArtifactInventory( + const std::filesystem::path& legacy_directory) { + std::error_code error; + if (!std::filesystem::is_directory(legacy_directory, error) || error) { + Fail("needs-reference-artifacts", + "The approved legacy directory is missing."); + } + std::vector names; + for (std::filesystem::directory_iterator iterator{legacy_directory, error}, + end; + iterator != end && !error; iterator.increment(error)) { + if (!iterator->is_regular_file(error) || error) { + Fail("needs-reference-artifacts", + "The legacy bundle contains a non-file entry."); } + names.push_back(iterator->path().filename().string()); + } + if (error) { + Fail("needs-reference-artifacts", "The legacy bundle cannot be inspected."); + } + std::sort(names.begin(), names.end()); + std::vector expected = {kDisplacementName, kInputName, + kReactionName, kSectionName}; + std::sort(expected.begin(), expected.end()); + if (names != expected) { + Fail("needs-reference-artifacts", + "The legacy bundle inventory is not exact."); + } } -Domain readApprovedDomain(const std::filesystem::path& inputPath) { - AbaqusInputReader reader; - auto parsed = reader.read(inputPath); - if (!parsed.HasValue()) { - fail("needs-reference-artifacts", "The approved reference input cannot be parsed."); - } - AbaqusDomainMapper mapper; - auto domain = mapper.map(parsed.Value()); - if (!domain.HasValue()) { - fail( - "needs-reference-artifacts", - "The approved reference input is not the required B33 model."); - } - return std::move(domain.Value()); +Domain ReadApprovedDomain(const std::filesystem::path& input_path) { + AbaqusInputReader reader; + auto parsed = reader.Read(input_path); + if (!parsed.HasValue()) { + Fail("needs-reference-artifacts", + "The approved reference input cannot be parsed."); + } + AbaqusDomainMapper mapper; + auto domain = mapper.Map(parsed.Value()); + if (!domain.HasValue()) { + Fail("needs-reference-artifacts", + "The approved reference input is not the required B33 model."); + } + return std::move(domain.Value()); } class Hdf5Handle { -public: - using Closer = herr_t (*)(hid_t); + public: + using Closer = herr_t (*)(hid_t); - Hdf5Handle() = default; - Hdf5Handle(const hid_t value, Closer closer) - : value_{value}, closer_{closer} {} - Hdf5Handle(const Hdf5Handle&) = delete; - Hdf5Handle& operator=(const Hdf5Handle&) = delete; - Hdf5Handle(Hdf5Handle&& other) noexcept - : value_{other.value_}, closer_{other.closer_} { - other.value_ = -1; - other.closer_ = nullptr; + Hdf5Handle() = default; + Hdf5Handle(const hid_t value, Closer closer) + : value_{value}, closer_{closer} {} + Hdf5Handle(const Hdf5Handle&) = delete; + Hdf5Handle& operator=(const Hdf5Handle&) = delete; + Hdf5Handle(Hdf5Handle&& other) noexcept + : value_{other.value_}, closer_{other.closer_} { + other.value_ = -1; + other.closer_ = nullptr; + } + Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { + if (this != &other) { + Reset(); + value_ = other.value_; + closer_ = other.closer_; + other.value_ = -1; + other.closer_ = nullptr; } - Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { - if (this != &other) { - reset(); - value_ = other.value_; - closer_ = other.closer_; - other.value_ = -1; - other.closer_ = nullptr; - } - return *this; + return *this; + } + ~Hdf5Handle() { Reset(); } + + hid_t Get() const noexcept { return value_; } + + private: + void Reset() noexcept { + if (value_ >= 0 && closer_ != nullptr) { + (void)closer_(value_); } - ~Hdf5Handle() { reset(); } + value_ = -1; + closer_ = nullptr; + } - hid_t get() const noexcept { return value_; } - -private: - void reset() noexcept { - if (value_ >= 0 && closer_ != nullptr) { - (void)closer_(value_); - } - value_ = -1; - closer_ = nullptr; - } - - hid_t value_{-1}; - Closer closer_{nullptr}; + hid_t value_{-1}; + Closer closer_{nullptr}; }; class Hdf5ErrorSilencer { -public: - Hdf5ErrorSilencer() { - if (H5Eget_auto2(H5E_DEFAULT, &callback_, &clientData_) >= 0 && - H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { - active_ = true; - } + public: + Hdf5ErrorSilencer() { + if (H5Eget_auto2(H5E_DEFAULT, &callback_, &client_data_) >= 0 && + H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { + active_ = true; } - Hdf5ErrorSilencer(const Hdf5ErrorSilencer&) = delete; - Hdf5ErrorSilencer& operator=(const Hdf5ErrorSilencer&) = delete; - ~Hdf5ErrorSilencer() { - if (active_) { - (void)H5Eset_auto2(H5E_DEFAULT, callback_, clientData_); - } + } + Hdf5ErrorSilencer(const Hdf5ErrorSilencer&) = delete; + Hdf5ErrorSilencer& operator=(const Hdf5ErrorSilencer&) = delete; + ~Hdf5ErrorSilencer() { + if (active_) { + (void)H5Eset_auto2(H5E_DEFAULT, callback_, client_data_); } + } -private: - H5E_auto2_t callback_{nullptr}; - void* clientData_{nullptr}; - bool active_{false}; + private: + H5E_auto2_t callback_{nullptr}; + void* client_data_{nullptr}; + bool active_{false}; }; class Hdf5VlenReclaimer { -public: - Hdf5VlenReclaimer( - const hid_t memoryType, - const hid_t dataSpace, - void* const data) noexcept - : memoryType_{memoryType}, dataSpace_{dataSpace}, data_{data} {} - Hdf5VlenReclaimer(const Hdf5VlenReclaimer&) = delete; - Hdf5VlenReclaimer& operator=(const Hdf5VlenReclaimer&) = delete; - ~Hdf5VlenReclaimer() { - if (active_) { - (void)H5Dvlen_reclaim( - memoryType_, dataSpace_, H5P_DEFAULT, data_); - } + public: + Hdf5VlenReclaimer(const hid_t memory_type, const hid_t data_space, + void* const data) noexcept + : memory_type_{memory_type}, data_space_{data_space}, data_{data} {} + Hdf5VlenReclaimer(const Hdf5VlenReclaimer&) = delete; + Hdf5VlenReclaimer& operator=(const Hdf5VlenReclaimer&) = delete; + ~Hdf5VlenReclaimer() { + if (active_) { + (void)H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_); } + } - herr_t reclaim() noexcept { - active_ = false; - return H5Dvlen_reclaim( - memoryType_, dataSpace_, H5P_DEFAULT, data_); - } + herr_t Reclaim() noexcept { + active_ = false; + return H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_); + } -private: - hid_t memoryType_; - hid_t dataSpace_; - void* data_; - bool active_{true}; + private: + hid_t memory_type_; + hid_t data_space_; + void* data_; + bool active_{true}; }; -hid_t requireId(const hid_t value, const char* message) { - if (value < 0) { - fail("schema-mismatch", message); - } - return value; +hid_t RequireId(const hid_t value, const char* message) { + if (value < 0) { + Fail("schema-mismatch", message); + } + return value; } -void requireHdf(const herr_t value, const char* message) { - if (value < 0) { - fail("schema-mismatch", message); - } +void RequireHdf(const herr_t value, const char* message) { + if (value < 0) { + Fail("schema-mismatch", message); + } } -Hdf5Handle openDataset(const hid_t file, const char* path) { - return {requireId(H5Dopen2(file, path, H5P_DEFAULT), - "A required HDF5 dataset is missing."), - H5Dclose}; +Hdf5Handle OpenDataset(const hid_t file, const char* path) { + return {RequireId(H5Dopen2(file, path, H5P_DEFAULT), + "A required HDF5 dataset is missing."), + H5Dclose}; } -std::vector datasetDimensions(const hid_t dataset) { - Hdf5Handle space{ - requireId(H5Dget_space(dataset), "Unable to inspect an HDF5 dataspace."), - H5Sclose}; - const int rank = H5Sget_simple_extent_ndims(space.get()); - if (rank < 0) { - fail("schema-mismatch", "Unable to inspect an HDF5 dataset rank."); - } - std::vector dimensions(static_cast(rank)); - if (rank > 0) { - requireHdf( - H5Sget_simple_extent_dims(space.get(), dimensions.data(), nullptr), - "Unable to inspect HDF5 dataset dimensions."); - } - return dimensions; +std::vector DatasetDimensions(const hid_t dataset) { + Hdf5Handle space{ + RequireId(H5Dget_space(dataset), "Unable to inspect an HDF5 dataspace."), + H5Sclose}; + const int rank = H5Sget_simple_extent_ndims(space.Get()); + if (rank < 0) { + Fail("schema-mismatch", "Unable to inspect an HDF5 dataset rank."); + } + std::vector dimensions(static_cast(rank)); + if (rank > 0) { + RequireHdf( + H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr), + "Unable to inspect HDF5 dataset dimensions."); + } + return dimensions; } -std::string readStringAttribute(const hid_t object, const char* name) { - Hdf5Handle attribute{ - requireId(H5Aopen(object, name, H5P_DEFAULT), - "A required HDF5 string attribute is missing."), - H5Aclose}; - Hdf5Handle type{ - requireId(H5Aget_type(attribute.get()), - "Unable to inspect an HDF5 string attribute."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_STRING || - H5Tis_variable_str(type.get()) <= 0 || - H5Tget_cset(type.get()) != H5T_CSET_UTF8) { - fail("schema-mismatch", "An HDF5 string attribute has the wrong type."); +std::string ReadStringAttribute(const hid_t object, const char* name) { + Hdf5Handle attribute{ + RequireId(H5Aopen(object, name, H5P_DEFAULT), + "A required HDF5 string attribute is missing."), + H5Aclose}; + Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()), + "Unable to inspect an HDF5 string attribute."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_STRING || + H5Tis_variable_str(type.Get()) <= 0 || + H5Tget_cset(type.Get()) != H5T_CSET_UTF8) { + Fail("schema-mismatch", "An HDF5 string attribute has the wrong type."); + } + char* raw = nullptr; + RequireHdf(H5Aread(attribute.Get(), type.Get(), &raw), + "Unable to read an HDF5 string attribute."); + if (raw == nullptr) { + Fail("schema-mismatch", "An HDF5 string attribute is null."); + } + const std::string value{raw}; + RequireHdf(H5free_memory(raw), "Unable to release HDF5 string memory."); + return value; +} + +std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) { + Hdf5Handle attribute{ + RequireId(H5Aopen(object, name, H5P_DEFAULT), + "A required HDF5 integer attribute is missing."), + H5Aclose}; + Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()), + "Unable to inspect an HDF5 integer attribute."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_INTEGER || + H5Tget_size(type.Get()) != sizeof(std::uint64_t) || + H5Tget_sign(type.Get()) != H5T_SGN_NONE) { + Fail("schema-mismatch", "An HDF5 integer attribute has the wrong type."); + } + std::uint64_t value = 0U; + RequireHdf(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value), + "Unable to read an HDF5 integer attribute."); + return value; +} + +void RequireStringAttribute(const hid_t object, const char* name, + const char* expected) { + if (ReadStringAttribute(object, name) != expected) { + Fail("schema-mismatch", "An HDF5 string attribute has the wrong value."); + } +} + +void RequireResultAttributes(const hid_t dataset, const char* components, + const char* units, const char* coordinate_system, + const char* location) { + RequireStringAttribute(dataset, "component_names", components); + RequireStringAttribute(dataset, "component_unit_dimensions", units); + RequireStringAttribute(dataset, "coordinate_system", coordinate_system); + RequireStringAttribute(dataset, "location", location); + RequireStringAttribute(dataset, "step_name", kStepName); + if (ReadUint64Attribute(dataset, "frame_index") != kFrameIndex) { + Fail("schema-mismatch", "An HDF5 result has the wrong frame identity."); + } +} + +std::vector ReadDoubleDataset( + const hid_t file, const char* path, + const std::vector& expected_dimensions, const char* components, + const char* units, const char* coordinate_system, const char* location) { + auto dataset = OpenDataset(file, path); + if (DatasetDimensions(dataset.Get()) != expected_dimensions) { + Fail("schema-mismatch", "An HDF5 result dataset has the wrong shape."); + } + Hdf5Handle type{RequireId(H5Dget_type(dataset.Get()), + "Unable to inspect an HDF5 result type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_FLOAT || + H5Tget_size(type.Get()) != sizeof(double) || + H5Tequal(type.Get(), H5T_IEEE_F64LE) <= 0) { + Fail("schema-mismatch", "An HDF5 result dataset is not float64."); + } + RequireResultAttributes(dataset.Get(), components, units, coordinate_system, + location); + std::size_t count = 1U; + for (const hsize_t dimension : expected_dimensions) { + if (dimension > (std::numeric_limits::max)() / count) { + Fail("schema-mismatch", "An HDF5 result shape overflows size_t."); } - char* raw = nullptr; - requireHdf( - H5Aread(attribute.get(), type.get(), &raw), - "Unable to read an HDF5 string attribute."); + count *= static_cast(dimension); + } + std::vector values(count); + if (!values.empty()) { + RequireHdf(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, + H5P_DEFAULT, values.data()), + "Unable to read an HDF5 result dataset."); + } + if (!std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); })) { + Fail("schema-mismatch", "An HDF5 comparison value is nonfinite."); + } + return values; +} + +void RequireCompoundMembers(const hid_t dataset, + const std::vector& expected) { + Hdf5Handle type{RequireId(H5Dget_type(dataset), + "Unable to inspect an HDF5 compound type."), + H5Tclose}; + if (H5Tget_class(type.Get()) != H5T_COMPOUND || + H5Tget_nmembers(type.Get()) != static_cast(expected.size())) { + Fail("schema-mismatch", "An HDF5 compound dataset has the wrong schema."); + } + for (std::size_t index = 0U; index < expected.size(); ++index) { + char* raw = H5Tget_member_name(type.Get(), static_cast(index)); if (raw == nullptr) { - fail("schema-mismatch", "An HDF5 string attribute is null."); + Fail("schema-mismatch", "Unable to inspect an HDF5 member name."); } - const std::string value{raw}; - requireHdf(H5free_memory(raw), "Unable to release HDF5 string memory."); - return value; + const std::string actual{raw}; + RequireHdf(H5free_memory(raw), "Unable to release an HDF5 member name."); + if (actual != expected[index]) { + Fail("schema-mismatch", "An HDF5 compound member has the wrong name."); + } + } } -std::uint64_t readUint64Attribute(const hid_t object, const char* name) { - Hdf5Handle attribute{ - requireId(H5Aopen(object, name, H5P_DEFAULT), - "A required HDF5 integer attribute is missing."), - H5Aclose}; - Hdf5Handle type{ - requireId(H5Aget_type(attribute.get()), - "Unable to inspect an HDF5 integer attribute."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_INTEGER || - H5Tget_size(type.get()) != sizeof(std::uint64_t) || - H5Tget_sign(type.get()) != H5T_SGN_NONE) { - fail("schema-mismatch", "An HDF5 integer attribute has the wrong type."); - } - std::uint64_t value = 0U; - requireHdf( - H5Aread(attribute.get(), H5T_NATIVE_UINT64, &value), - "Unable to read an HDF5 integer attribute."); - return value; -} - -void requireStringAttribute( - const hid_t object, const char* name, const char* expected) { - if (readStringAttribute(object, name) != expected) { - fail("schema-mismatch", "An HDF5 string attribute has the wrong value."); - } -} - -void requireResultAttributes( - const hid_t dataset, - const char* components, - const char* units, - const char* coordinateSystem, - const char* location) { - requireStringAttribute(dataset, "component_names", components); - requireStringAttribute(dataset, "component_unit_dimensions", units); - requireStringAttribute(dataset, "coordinate_system", coordinateSystem); - requireStringAttribute(dataset, "location", location); - requireStringAttribute(dataset, "step_name", kStepName); - if (readUint64Attribute(dataset, "frame_index") != kFrameIndex) { - fail("schema-mismatch", "An HDF5 result has the wrong frame identity."); - } -} - -std::vector readDoubleDataset( - const hid_t file, - const char* path, - const std::vector& expectedDimensions, - const char* components, - const char* units, - const char* coordinateSystem, - const char* location) { - auto dataset = openDataset(file, path); - if (datasetDimensions(dataset.get()) != expectedDimensions) { - fail("schema-mismatch", "An HDF5 result dataset has the wrong shape."); - } - Hdf5Handle type{ - requireId(H5Dget_type(dataset.get()), - "Unable to inspect an HDF5 result type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_FLOAT || - H5Tget_size(type.get()) != sizeof(double) || - H5Tequal(type.get(), H5T_IEEE_F64LE) <= 0) { - fail("schema-mismatch", "An HDF5 result dataset is not float64."); - } - requireResultAttributes( - dataset.get(), components, units, coordinateSystem, location); - std::size_t count = 1U; - for (const hsize_t dimension : expectedDimensions) { - if (dimension > (std::numeric_limits::max)() / count) { - fail("schema-mismatch", "An HDF5 result shape overflows size_t."); - } - count *= static_cast(dimension); - } - std::vector values(count); - if (!values.empty()) { - requireHdf( - H5Dread( - dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()), - "Unable to read an HDF5 result dataset."); - } - if (!std::all_of(values.begin(), values.end(), [](const double value) { - return std::isfinite(value); - })) { - fail("schema-mismatch", "An HDF5 comparison value is nonfinite."); - } - return values; -} - -void requireCompoundMembers( - const hid_t dataset, const std::vector& expected) { - Hdf5Handle type{ - requireId(H5Dget_type(dataset), "Unable to inspect an HDF5 compound type."), - H5Tclose}; - if (H5Tget_class(type.get()) != H5T_COMPOUND || - H5Tget_nmembers(type.get()) != static_cast(expected.size())) { - fail("schema-mismatch", "An HDF5 compound dataset has the wrong schema."); - } - for (std::size_t index = 0U; index < expected.size(); ++index) { - char* raw = H5Tget_member_name(type.get(), static_cast(index)); - if (raw == nullptr) { - fail("schema-mismatch", "Unable to inspect an HDF5 member name."); - } - const std::string actual{raw}; - requireHdf(H5free_memory(raw), "Unable to release an HDF5 member name."); - if (actual != expected[index]) { - fail("schema-mismatch", "An HDF5 compound member has the wrong name."); - } - } -} - -Hdf5Handle makeVariableStringType() { - Hdf5Handle type{ - requireId(H5Tcopy(H5T_C_S1), "Unable to create an HDF5 string type."), - H5Tclose}; - requireHdf( - H5Tset_size(type.get(), H5T_VARIABLE), - "Unable to size an HDF5 string type."); - requireHdf( - H5Tset_cset(type.get(), H5T_CSET_UTF8), - "Unable to configure an HDF5 string type."); - return type; +Hdf5Handle MakeVariableStringType() { + Hdf5Handle type{ + RequireId(H5Tcopy(H5T_C_S1), "Unable to create an HDF5 string type."), + H5Tclose}; + RequireHdf(H5Tset_size(type.Get(), H5T_VARIABLE), + "Unable to size an HDF5 string type."); + RequireHdf(H5Tset_cset(type.Get(), H5T_CSET_UTF8), + "Unable to configure an HDF5 string type."); + return type; } struct NodeMemoryRow { - std::uint64_t internalNodeId; - char* instanceName; - char* sourceLabel; - double coordinates[3]; + std::uint64_t internal_node_id; + char* instance_name; + char* source_label; + double coordinates[3]; }; struct ElementMemoryRow { - std::uint64_t internalElementId; - char* instanceName; - char* sourceLabel; - std::uint64_t nodeInternalIds[2]; - double localAxes[9]; + std::uint64_t internal_element_id; + char* instance_name; + char* source_label; + std::uint64_t node_internal_ids[2]; + double local_axes[9]; }; struct HdfNode { - std::uint64_t internalNodeId; - std::string instanceName; - std::int64_t sourceNodeLabel; - std::string sourceNodeLabelText; - std::array coordinates; + std::uint64_t internal_node_id; + std::string instance_name; + std::int64_t source_node_label; + std::string source_node_label_text; + std::array coordinates; }; struct HdfElement { - std::uint64_t internalElementId; - std::string instanceName; - std::int64_t sourceElementLabel; - std::string sourceElementLabelText; - std::array nodeInternalIds; - std::array localAxes; + std::uint64_t internal_element_id; + std::string instance_name; + std::int64_t source_element_label; + std::string source_element_label_text; + std::array node_internal_ids; + std::array local_axes; }; -std::vector readNodeRows(const hid_t file) { - auto dataset = openDataset(file, "/model/nodes"); - const auto dimensions = datasetDimensions(dataset.get()); - if (dimensions.size() != 1U || dimensions[0U] == 0U) { - fail("schema-mismatch", "The HDF5 node table has the wrong shape."); - } - requireCompoundMembers( - dataset.get(), - {"internal_node_id", "instance_name", "source_label", "coordinates"}); - requireStringAttribute(dataset.get(), "coordinate_system", "global-cartesian"); - requireStringAttribute(dataset.get(), "units_label", "length"); +std::vector ReadNodeRows(const hid_t file) { + auto dataset = OpenDataset(file, "/model/nodes"); + const auto dimensions = DatasetDimensions(dataset.Get()); + if (dimensions.size() != 1U || dimensions[0U] == 0U) { + Fail("schema-mismatch", "The HDF5 node table has the wrong shape."); + } + RequireCompoundMembers(dataset.Get(), {"internal_node_id", "instance_name", + "source_label", "coordinates"}); + RequireStringAttribute(dataset.Get(), "coordinate_system", + "global-cartesian"); + RequireStringAttribute(dataset.Get(), "units_label", "length"); - auto stringType = makeVariableStringType(); - const hsize_t coordinateDimensions[1] = {3U}; - Hdf5Handle coordinateType{ - requireId( - H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), - "Unable to create the node coordinate memory type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeMemoryRow)), - "Unable to create the node memory type."), - H5Tclose}; - requireHdf( - H5Tinsert(memoryType.get(), "internal_node_id", - HOFFSET(NodeMemoryRow, internalNodeId), H5T_NATIVE_UINT64), - "Unable to define the node ID memory field."); - requireHdf( - H5Tinsert(memoryType.get(), "instance_name", - HOFFSET(NodeMemoryRow, instanceName), stringType.get()), - "Unable to define the node instance memory field."); - requireHdf( - H5Tinsert(memoryType.get(), "source_label", - HOFFSET(NodeMemoryRow, sourceLabel), stringType.get()), - "Unable to define the node label memory field."); - requireHdf( - H5Tinsert(memoryType.get(), "coordinates", - HOFFSET(NodeMemoryRow, coordinates), coordinateType.get()), - "Unable to define the node coordinate memory field."); + auto string_type = MakeVariableStringType(); + const hsize_t coordinate_dimensions[1] = {3U}; + Hdf5Handle coordinate_type{ + RequireId(H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), + "Unable to create the node coordinate memory type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeMemoryRow)), + "Unable to create the node memory type."), + H5Tclose}; + RequireHdf( + H5Tinsert(memory_type.Get(), "internal_node_id", + HOFFSET(NodeMemoryRow, internal_node_id), H5T_NATIVE_UINT64), + "Unable to define the node ID memory field."); + RequireHdf( + H5Tinsert(memory_type.Get(), "instance_name", + HOFFSET(NodeMemoryRow, instance_name), string_type.Get()), + "Unable to define the node instance memory field."); + RequireHdf(H5Tinsert(memory_type.Get(), "source_label", + HOFFSET(NodeMemoryRow, source_label), string_type.Get()), + "Unable to define the node label memory field."); + RequireHdf( + H5Tinsert(memory_type.Get(), "coordinates", + HOFFSET(NodeMemoryRow, coordinates), coordinate_type.Get()), + "Unable to define the node coordinate memory field."); - std::vector raw(static_cast(dimensions[0U])); - Hdf5Handle space{ - requireId(H5Dget_space(dataset.get()), - "Unable to reopen the node dataspace."), - H5Sclose}; - requireHdf( - H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, - H5P_DEFAULT, raw.data()), - "Unable to read the HDF5 node table."); - Hdf5VlenReclaimer strings{memoryType.get(), space.get(), raw.data()}; - std::vector rows; - rows.reserve(raw.size()); - for (const auto& row : raw) { - if (row.instanceName == nullptr || row.sourceLabel == nullptr) { - fail("schema-mismatch", "An HDF5 node identity is null."); - } - const std::array coordinates = { - row.coordinates[0U], row.coordinates[1U], row.coordinates[2U]}; - if (!std::all_of( - coordinates.begin(), coordinates.end(), [](const double value) { - return std::isfinite(value); - })) { - fail("schema-mismatch", "An HDF5 node coordinate is nonfinite."); - } - rows.push_back({ - row.internalNodeId, - row.instanceName, - parsePositiveLabel(row.sourceLabel), - row.sourceLabel, - coordinates}); + std::vector raw(static_cast(dimensions[0U])); + Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()), + "Unable to reopen the node dataspace."), + H5Sclose}; + RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, + H5P_DEFAULT, raw.data()), + "Unable to read the HDF5 node table."); + Hdf5VlenReclaimer strings{memory_type.Get(), space.Get(), raw.data()}; + std::vector rows; + rows.reserve(raw.size()); + for (const auto& row : raw) { + if (row.instance_name == nullptr || row.source_label == nullptr) { + Fail("schema-mismatch", "An HDF5 node identity is null."); } - requireHdf( - strings.reclaim(), - "Unable to reclaim HDF5 node strings."); - return rows; + const std::array coordinates = { + row.coordinates[0U], row.coordinates[1U], row.coordinates[2U]}; + if (!std::all_of(coordinates.begin(), coordinates.end(), + [](const double value) { return std::isfinite(value); })) { + Fail("schema-mismatch", "An HDF5 node coordinate is nonfinite."); + } + rows.push_back({row.internal_node_id, row.instance_name, + ParsePositiveLabel(row.source_label), row.source_label, + coordinates}); + } + RequireHdf(strings.Reclaim(), "Unable to reclaim HDF5 node strings."); + return rows; } -std::vector readElementRows(const hid_t file) { - auto dataset = openDataset(file, "/model/elements"); - const auto dimensions = datasetDimensions(dataset.get()); - if (dimensions.size() != 1U || dimensions[0U] == 0U) { - fail("schema-mismatch", "The HDF5 element table has the wrong shape."); - } - requireCompoundMembers( - dataset.get(), - {"internal_element_id", "instance_name", "source_label", - "node_internal_ids", "local_axes"}); - requireStringAttribute(dataset.get(), "formulation", "B33-3D-Euler-Bernoulli"); +std::vector ReadElementRows(const hid_t file) { + auto dataset = OpenDataset(file, "/model/elements"); + const auto dimensions = DatasetDimensions(dataset.Get()); + if (dimensions.size() != 1U || dimensions[0U] == 0U) { + Fail("schema-mismatch", "The HDF5 element table has the wrong shape."); + } + RequireCompoundMembers( + dataset.Get(), {"internal_element_id", "instance_name", "source_label", + "node_internal_ids", "local_axes"}); + RequireStringAttribute(dataset.Get(), "formulation", + "B33-3D-Euler-Bernoulli"); - auto stringType = makeVariableStringType(); - const hsize_t connectivityDimensions[1] = {2U}; - const hsize_t axesDimensions[2] = {3U, 3U}; - Hdf5Handle connectivityType{ - requireId( - H5Tarray_create2(H5T_NATIVE_UINT64, 1, connectivityDimensions), - "Unable to create the connectivity memory type."), - H5Tclose}; - Hdf5Handle axesType{ - requireId( - H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axesDimensions), - "Unable to create the local-axis memory type."), - H5Tclose}; - Hdf5Handle memoryType{ - requireId(H5Tcreate(H5T_COMPOUND, sizeof(ElementMemoryRow)), - "Unable to create the element memory type."), - H5Tclose}; - requireHdf( - H5Tinsert(memoryType.get(), "internal_element_id", - HOFFSET(ElementMemoryRow, internalElementId), - H5T_NATIVE_UINT64), - "Unable to define the element ID memory field."); - requireHdf( - H5Tinsert(memoryType.get(), "instance_name", - HOFFSET(ElementMemoryRow, instanceName), stringType.get()), - "Unable to define the element instance memory field."); - requireHdf( - H5Tinsert(memoryType.get(), "source_label", - HOFFSET(ElementMemoryRow, sourceLabel), stringType.get()), - "Unable to define the element label memory field."); - requireHdf( - H5Tinsert(memoryType.get(), "node_internal_ids", - HOFFSET(ElementMemoryRow, nodeInternalIds), - connectivityType.get()), - "Unable to define the connectivity memory field."); - requireHdf( - H5Tinsert(memoryType.get(), "local_axes", - HOFFSET(ElementMemoryRow, localAxes), axesType.get()), - "Unable to define the local-axis memory field."); + auto string_type = MakeVariableStringType(); + const hsize_t connectivity_dimensions[1] = {2U}; + const hsize_t axes_dimensions[2] = {3U, 3U}; + Hdf5Handle connectivity_type{ + RequireId(H5Tarray_create2(H5T_NATIVE_UINT64, 1, connectivity_dimensions), + "Unable to create the connectivity memory type."), + H5Tclose}; + Hdf5Handle axes_type{ + RequireId(H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axes_dimensions), + "Unable to create the local-axis memory type."), + H5Tclose}; + Hdf5Handle memory_type{ + RequireId(H5Tcreate(H5T_COMPOUND, sizeof(ElementMemoryRow)), + "Unable to create the element memory type."), + H5Tclose}; + RequireHdf(H5Tinsert(memory_type.Get(), "internal_element_id", + HOFFSET(ElementMemoryRow, internal_element_id), + H5T_NATIVE_UINT64), + "Unable to define the element ID memory field."); + RequireHdf( + H5Tinsert(memory_type.Get(), "instance_name", + HOFFSET(ElementMemoryRow, instance_name), string_type.Get()), + "Unable to define the element instance memory field."); + RequireHdf( + H5Tinsert(memory_type.Get(), "source_label", + HOFFSET(ElementMemoryRow, source_label), string_type.Get()), + "Unable to define the element label memory field."); + RequireHdf(H5Tinsert(memory_type.Get(), "node_internal_ids", + HOFFSET(ElementMemoryRow, node_internal_ids), + connectivity_type.Get()), + "Unable to define the connectivity memory field."); + RequireHdf(H5Tinsert(memory_type.Get(), "local_axes", + HOFFSET(ElementMemoryRow, local_axes), axes_type.Get()), + "Unable to define the local-axis memory field."); - std::vector raw(static_cast(dimensions[0U])); - Hdf5Handle space{ - requireId(H5Dget_space(dataset.get()), - "Unable to reopen the element dataspace."), - H5Sclose}; - requireHdf( - H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, - H5P_DEFAULT, raw.data()), - "Unable to read the HDF5 element table."); - Hdf5VlenReclaimer strings{memoryType.get(), space.get(), raw.data()}; - std::vector rows; - rows.reserve(raw.size()); - for (const auto& row : raw) { - if (row.instanceName == nullptr || row.sourceLabel == nullptr) { - fail("schema-mismatch", "An HDF5 element identity is null."); - } - HdfElement converted{}; - converted.internalElementId = row.internalElementId; - converted.instanceName = row.instanceName; - converted.sourceElementLabel = parsePositiveLabel(row.sourceLabel); - converted.sourceElementLabelText = row.sourceLabel; - converted.nodeInternalIds = { - row.nodeInternalIds[0U], row.nodeInternalIds[1U]}; - std::copy( - std::begin(row.localAxes), std::end(row.localAxes), - converted.localAxes.begin()); - if (!std::all_of( - converted.localAxes.begin(), converted.localAxes.end(), - [](const double value) { return std::isfinite(value); })) { - fail("schema-mismatch", "An HDF5 local axis is nonfinite."); - } - rows.push_back(std::move(converted)); + std::vector raw(static_cast(dimensions[0U])); + Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()), + "Unable to reopen the element dataspace."), + H5Sclose}; + RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, + H5P_DEFAULT, raw.data()), + "Unable to read the HDF5 element table."); + Hdf5VlenReclaimer strings{memory_type.Get(), space.Get(), raw.data()}; + std::vector rows; + rows.reserve(raw.size()); + for (const auto& row : raw) { + if (row.instance_name == nullptr || row.source_label == nullptr) { + Fail("schema-mismatch", "An HDF5 element identity is null."); } - requireHdf( - strings.reclaim(), - "Unable to reclaim HDF5 element strings."); - return rows; + HdfElement converted{}; + converted.internal_element_id = row.internal_element_id; + converted.instance_name = row.instance_name; + converted.source_element_label = ParsePositiveLabel(row.source_label); + converted.source_element_label_text = row.source_label; + converted.node_internal_ids = {row.node_internal_ids[0U], + row.node_internal_ids[1U]}; + std::copy(std::begin(row.local_axes), std::end(row.local_axes), + converted.local_axes.begin()); + if (!std::all_of(converted.local_axes.begin(), converted.local_axes.end(), + [](const double value) { return std::isfinite(value); })) { + Fail("schema-mismatch", "An HDF5 local axis is nonfinite."); + } + rows.push_back(std::move(converted)); + } + RequireHdf(strings.Reclaim(), "Unable to reclaim HDF5 element strings."); + return rows; } -void requireFiniteStress(const hid_t file) { - auto dataset = openDataset(file, kStressPath); - const auto dimensions = datasetDimensions(dataset.get()); - if (dimensions.size() != 1U || dimensions[0U] == 0U) { - fail("schema-mismatch", "The mandatory stress dataset has no rows."); - } - requireCompoundMembers( - dataset.get(), - {"internal_element_id", "gauss_point_index", "section_point_index", - "x1", "x2", "source", "S11"}); - requireResultAttributes( - dataset.get(), "S11", "force/length^2", "beam-local", "section-point"); - struct StressValue { - double s11; - }; - Hdf5Handle memoryType{ - requireId(H5Tcreate(H5T_COMPOUND, sizeof(StressValue)), - "Unable to create a stress memory type."), - H5Tclose}; - requireHdf( - H5Tinsert(memoryType.get(), "S11", HOFFSET(StressValue, s11), - H5T_NATIVE_DOUBLE), - "Unable to define the stress memory field."); - std::vector values(static_cast(dimensions[0U])); - requireHdf( - H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()), - "Unable to read the stress dataset."); - if (!std::all_of(values.begin(), values.end(), [](const StressValue& value) { - return std::isfinite(value.s11); - })) { - fail("schema-mismatch", "The mandatory stress dataset is nonfinite."); - } +void RequireFiniteStress(const hid_t file) { + auto dataset = OpenDataset(file, kStressPath); + const auto dimensions = DatasetDimensions(dataset.Get()); + if (dimensions.size() != 1U || dimensions[0U] == 0U) { + Fail("schema-mismatch", "The mandatory stress dataset has no rows."); + } + RequireCompoundMembers(dataset.Get(), + {"internal_element_id", "gauss_point_index", + "section_point_index", "x1", "x2", "source", "S11"}); + RequireResultAttributes(dataset.Get(), "S11", "force/length^2", "beam-local", + "section-point"); + struct StressValue { + double s11; + }; + Hdf5Handle memory_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(StressValue)), + "Unable to create a stress memory type."), + H5Tclose}; + RequireHdf(H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressValue, s11), + H5T_NATIVE_DOUBLE), + "Unable to define the stress memory field."); + std::vector values(static_cast(dimensions[0U])); + RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, + H5P_DEFAULT, values.data()), + "Unable to read the stress dataset."); + if (!std::all_of(values.begin(), values.end(), [](const StressValue& value) { + return std::isfinite(value.s11); + })) { + Fail("schema-mismatch", "The mandatory stress dataset is nonfinite."); + } } -std::array expectedLocalAxes( - const Domain& domain, const EulerBeam3DDefinition& element) { - const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; - const auto& second = domain.Nodes()[element.node_indices[1U]].coordinates; - const auto& guide = domain.Sections()[element.section_index].first_axis; - const std::array delta = { - second[0U] - first[0U], - second[1U] - first[1U], - second[2U] - first[2U]}; - const double length = std::hypot(delta[0U], delta[1U], delta[2U]); - const std::array x = { - delta[0U] / length, delta[1U] / length, delta[2U] / length}; - const double projection = - guide[0U] * x[0U] + guide[1U] * x[1U] + guide[2U] * x[2U]; - const std::array yTrial = { - guide[0U] - projection * x[0U], - guide[1U] - projection * x[1U], - guide[2U] - projection * x[2U]}; - const double yNorm = std::hypot(yTrial[0U], yTrial[1U], yTrial[2U]); - const std::array y = { - yTrial[0U] / yNorm, yTrial[1U] / yNorm, yTrial[2U] / yNorm}; - const std::array z = { - x[1U] * y[2U] - x[2U] * y[1U], - x[2U] * y[0U] - x[0U] * y[2U], - x[0U] * y[1U] - x[1U] * y[0U]}; - return { - x[0U], x[1U], x[2U], - y[0U], y[1U], y[2U], - z[0U], z[1U], z[2U]}; +std::array ExpectedLocalAxes(const Domain& domain, + const EulerBeam3DDefinition& element) { + const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; + const auto& second = domain.Nodes()[element.node_indices[1U]].coordinates; + const auto& guide = domain.Sections()[element.section_index].first_axis; + const std::array delta = { + second[0U] - first[0U], second[1U] - first[1U], second[2U] - first[2U]}; + const double length = std::hypot(delta[0U], delta[1U], delta[2U]); + const std::array x = {delta[0U] / length, delta[1U] / length, + delta[2U] / length}; + const double projection = + guide[0U] * x[0U] + guide[1U] * x[1U] + guide[2U] * x[2U]; + const std::array y_trial = {guide[0U] - projection * x[0U], + guide[1U] - projection * x[1U], + guide[2U] - projection * x[2U]}; + const double y_norm = std::hypot(y_trial[0U], y_trial[1U], y_trial[2U]); + const std::array y = {y_trial[0U] / y_norm, y_trial[1U] / y_norm, + y_trial[2U] / y_norm}; + const std::array z = {x[1U] * y[2U] - x[2U] * y[1U], + x[2U] * y[0U] - x[0U] * y[2U], + x[0U] * y[1U] - x[1U] * y[0U]}; + return {x[0U], x[1U], x[2U], y[0U], y[1U], y[2U], z[0U], z[1U], z[2U]}; } struct HdfProjection { - std::vector nodes; - std::vector elements; - std::vector displacement; - std::vector reaction; - std::vector sectionResultants; + std::vector nodes; + std::vector elements; + std::vector displacement; + std::vector reaction; + std::vector section_resultants; }; -HdfProjection readHdfProjection( - const std::filesystem::path& results, - const std::filesystem::path& input, - const Domain& domain) { - std::error_code error; - if (!std::filesystem::is_regular_file(results, error) || error) { - fail("needs-solver-results", "The authoritative FESA results.h5 is missing."); - } - Hdf5ErrorSilencer silence; - if (H5Fis_hdf5(results.string().c_str()) <= 0) { - fail("schema-mismatch", "The solver result is not an HDF5 file."); - } - Hdf5Handle file{ - requireId(H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), - "Unable to open the solver HDF5 file read-only."), - H5Fclose}; - Hdf5Handle metadata{ - requireId(H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), - "The HDF5 metadata group is missing."), - H5Gclose}; - if (readUint64Attribute(metadata.get(), "schema_version") != 0U || - readUint64Attribute(metadata.get(), "frame_index") != kFrameIndex) { - fail("schema-mismatch", "The HDF5 schema or frame version is wrong."); - } - requireStringAttribute( - metadata.get(), "feature_id", "linear-static-3d-euler-beam"); - requireStringAttribute( - metadata.get(), "unit_system_label", "user-consistent-unspecified"); - requireStringAttribute( - metadata.get(), "coordinate_convention", - "global-cartesian; beam-local=(t,n1,t-cross-n1)"); - requireStringAttribute( - metadata.get(), "element_formulation", "B33-3D-Euler-Bernoulli"); - requireStringAttribute(metadata.get(), "step_name", kStepName); - const std::string sourceIdentity = - readStringAttribute(metadata.get(), "source_input_identity"); - const std::string normalizedInput = - std::filesystem::absolute(input).lexically_normal().generic_u8string(); - const std::string expectedIdentity = - "path=" + normalizedInput + - ";content_identity=" + domain.SourceContentIdentity(); - if (sourceIdentity != expectedIdentity) { - fail("schema-mismatch", "The HDF5 source-input identity is inconsistent."); - } +HdfProjection ReadHdfProjection(const std::filesystem::path& results, + const std::filesystem::path& input, + const Domain& domain) { + std::error_code error; + if (!std::filesystem::is_regular_file(results, error) || error) { + Fail("needs-solver-results", + "The authoritative FESA results.h5 is missing."); + } + Hdf5ErrorSilencer silence; + if (H5Fis_hdf5(results.string().c_str()) <= 0) { + Fail("schema-mismatch", "The solver result is not an HDF5 file."); + } + Hdf5Handle file{ + RequireId(H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), + "Unable to open the solver HDF5 file read-only."), + H5Fclose}; + Hdf5Handle metadata{RequireId(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), + "The HDF5 metadata group is missing."), + H5Gclose}; + if (ReadUint64Attribute(metadata.Get(), "schema_version") != 0U || + ReadUint64Attribute(metadata.Get(), "frame_index") != kFrameIndex) { + Fail("schema-mismatch", "The HDF5 schema or frame version is wrong."); + } + RequireStringAttribute(metadata.Get(), "feature_id", + "linear-static-3d-euler-beam"); + RequireStringAttribute(metadata.Get(), "unit_system_label", + "user-consistent-unspecified"); + RequireStringAttribute(metadata.Get(), "coordinate_convention", + "global-cartesian; beam-local=(t,n1,t-cross-n1)"); + RequireStringAttribute(metadata.Get(), "element_formulation", + "B33-3D-Euler-Bernoulli"); + RequireStringAttribute(metadata.Get(), "step_name", kStepName); + const std::string source_identity = + ReadStringAttribute(metadata.Get(), "source_input_identity"); + const std::string normalized_input = + std::filesystem::absolute(input).lexically_normal().generic_u8string(); + const std::string expected_identity = + "path=" + normalized_input + + ";content_identity=" + domain.SourceContentIdentity(); + if (source_identity != expected_identity) { + Fail("schema-mismatch", "The HDF5 source-input identity is inconsistent."); + } - HdfProjection projection{}; - projection.nodes = readNodeRows(file.get()); - projection.elements = readElementRows(file.get()); - if (projection.nodes.size() != domain.Nodes().size() || - projection.elements.size() != domain.Elements().size()) { - fail("schema-mismatch", "HDF5 model identity counts do not match the input."); + HdfProjection projection{}; + projection.nodes = ReadNodeRows(file.Get()); + projection.elements = ReadElementRows(file.Get()); + if (projection.nodes.size() != domain.Nodes().size() || + projection.elements.size() != domain.Elements().size()) { + Fail("schema-mismatch", + "HDF5 model identity counts do not match the input."); + } + for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { + const auto& actual = projection.nodes[node]; + const auto& expected = domain.Nodes()[node]; + if (actual.internal_node_id != node || + actual.instance_name != expected.source_id.instance_name || + actual.source_node_label != expected.source_id.source_label || + actual.source_node_label_text != expected.source_id.source_label_text || + actual.coordinates != expected.coordinates) { + Fail("schema-mismatch", + "An HDF5 node identity does not match the input."); } - for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { - const auto& actual = projection.nodes[node]; - const auto& expected = domain.Nodes()[node]; - if (actual.internalNodeId != node || - actual.instanceName != expected.source_id.instance_name || - actual.sourceNodeLabel != expected.source_id.source_label || - actual.sourceNodeLabelText != expected.source_id.source_label_text || - actual.coordinates != expected.coordinates) { - fail("schema-mismatch", "An HDF5 node identity does not match the input."); - } + } + for (std::size_t element = 0U; element < projection.elements.size(); + ++element) { + const auto& actual = projection.elements[element]; + const auto& expected = domain.Elements()[element]; + if (actual.internal_element_id != element || + actual.instance_name != expected.source_id.instance_name || + actual.source_element_label != expected.source_id.source_label || + actual.source_element_label_text != + expected.source_id.source_label_text || + actual.node_internal_ids[0U] != expected.node_indices[0U] || + actual.node_internal_ids[1U] != expected.node_indices[1U]) { + Fail("schema-mismatch", + "An HDF5 element identity does not match the input."); } - for (std::size_t element = 0U; element < projection.elements.size(); ++element) { - const auto& actual = projection.elements[element]; - const auto& expected = domain.Elements()[element]; - if (actual.internalElementId != element || - actual.instanceName != expected.source_id.instance_name || - actual.sourceElementLabel != expected.source_id.source_label || - actual.sourceElementLabelText != expected.source_id.source_label_text || - actual.nodeInternalIds[0U] != expected.node_indices[0U] || - actual.nodeInternalIds[1U] != expected.node_indices[1U]) { - fail("schema-mismatch", "An HDF5 element identity does not match the input."); - } - const auto axes = expectedLocalAxes(domain, expected); - for (std::size_t component = 0U; component < axes.size(); ++component) { - if (std::abs(actual.localAxes[component] - axes[component]) > 1.0e-12) { - fail("schema-mismatch", "An HDF5 local axis does not match the input."); - } - } + const auto axes = ExpectedLocalAxes(domain, expected); + for (std::size_t component = 0U; component < axes.size(); ++component) { + if (std::abs(actual.local_axes[component] - axes[component]) > 1.0e-12) { + Fail("schema-mismatch", "An HDF5 local axis does not match the input."); + } } + } - const hsize_t nodeCount = static_cast(projection.nodes.size()); - const hsize_t elementCount = static_cast(projection.elements.size()); - projection.displacement = readDoubleDataset( - file.get(), kDisplacementPath, {nodeCount, 6U}, - "UX,UY,UZ,URX,URY,URZ", - "length,length,length,radian,radian,radian", - "global-cartesian", "nodal"); - projection.reaction = readDoubleDataset( - file.get(), kReactionPath, {nodeCount, 6U}, - "RF1,RF2,RF3,RM1,RM2,RM3", - "force,force,force,force*length,force*length,force*length", - "global-cartesian", "nodal"); - projection.sectionResultants = readDoubleDataset( - file.get(), kSectionPath, {elementCount, 2U, 4U}, - "N,T,My,Mz", - "force,force*length,force*length,force*length", - "beam-local", "endpoint-positive-local-x-section-cut"); - requireFiniteStress(file.get()); - return projection; + const hsize_t node_count = static_cast(projection.nodes.size()); + const hsize_t element_count = + static_cast(projection.elements.size()); + projection.displacement = ReadDoubleDataset( + file.Get(), kDisplacementPath, {node_count, 6U}, "UX,UY,UZ,URX,URY,URZ", + "length,length,length,radian,radian,radian", "global-cartesian", "nodal"); + projection.reaction = ReadDoubleDataset( + file.Get(), kReactionPath, {node_count, 6U}, "RF1,RF2,RF3,RM1,RM2,RM3", + "force,force,force,force*length,force*length,force*length", + "global-cartesian", "nodal"); + projection.section_resultants = ReadDoubleDataset( + file.Get(), kSectionPath, {element_count, 2U, 4U}, "N,T,My,Mz", + "force,force*length,force*length,force*length", "beam-local", + "endpoint-positive-local-x-section-cut"); + RequireFiniteStress(file.Get()); + return projection; } -std::vector orderedRows( +std::vector OrderedRows( const ReferenceTable& table, const std::vector& nodes) { - if (table.rows.size() != nodes.size()) { - fail("schema-mismatch", "The FESA/reference projected row sets differ."); - } - std::vector ordered; - ordered.reserve(nodes.size()); - for (const auto& node : nodes) { - const auto found = std::find_if( - table.rows.begin(), table.rows.end(), [&](const WideReferenceRow& row) { - return asciiLower(row.instanceName) == - asciiLower(node.instanceName) && - row.sourceNodeLabel == node.sourceNodeLabel; - }); - if (found == table.rows.end() || found->instanceName != node.instanceName) { - fail("schema-mismatch", "A reference row identity does not match HDF5."); - } - ordered.push_back(&*found); - } - return ordered; -} - -double tableScale(const ReferenceTable& table, const std::size_t valueIndex) { - double scale = 0.0; - for (const auto& row : table.rows) { - if (valueIndex >= row.values.size()) { - fail("schema-mismatch", "A reference row has the wrong component arity."); - } - scale = (std::max)(scale, std::abs(row.values[valueIndex])); - } - return scale; -} - -std::vector normalizeStations( - const Domain& domain, - const HdfProjection& hdf, - const ReferenceTable& sectionTable) { - auto modelResult = AnalysisModel::Create(domain); - if (!modelResult.HasValue()) { - fail("schema-mismatch", "The approved input cannot create an analysis view."); - } - const AnalysisModel model = std::move(modelResult.Value()); - const std::array tolerances = { - kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 0U), - kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 3U), - kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 1U), - kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 2U)}; - std::vector endpoints; - endpoints.reserve(hdf.elements.size() * 2U); - for (std::size_t element = 0U; element < hdf.elements.size(); ++element) { - for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { - const auto node = static_cast( - hdf.elements[element].nodeInternalIds[endpoint]); - std::array values{}; - for (std::size_t component = 0U; component < values.size(); ++component) { - values[component] = - hdf.sectionResultants[(element * 2U + endpoint) * 4U + component]; - } - endpoints.push_back({ - static_cast(element), - static_cast(endpoint), - domain.Nodes()[node].source_id, - {}, - values}); - } - } - auto normalized = ResultRecovery::NormalizeSectionResultantsToNodeStations( - model, endpoints, tolerances); - if (!normalized.HasValue()) { - const auto& diagnostics = normalized.GetStatus().Diagnostics(); - const std::string code = diagnostics.empty() ? std::string{} : diagnostics[0U].code; - if (code == "node-station-tolerance-failure") { - fail("tolerance-failure", "Interior endpoint section resultants disagree."); - } - fail("schema-mismatch", "A node station is not eligible for legacy projection."); - } - return std::move(normalized.Value()); -} - -const NodeStationResultRow& findStation( - const std::vector& stations, - const HdfNode& node) { + if (table.rows.size() != nodes.size()) { + Fail("schema-mismatch", "The FESA/reference projected row sets differ."); + } + std::vector ordered; + ordered.reserve(nodes.size()); + for (const auto& node : nodes) { const auto found = std::find_if( - stations.begin(), stations.end(), [&](const NodeStationResultRow& row) { - return row.node.instance_name == node.instanceName && - row.node.source_label == node.sourceNodeLabel; + table.rows.begin(), table.rows.end(), [&](const WideReferenceRow& row) { + return AsciiLower(row.instance_name) == + AsciiLower(node.instance_name) && + row.source_node_label == node.source_node_label; }); - if (found == stations.end()) { - fail("schema-mismatch", "A projected HDF5 node station is missing."); + if (found == table.rows.end() || + found->instance_name != node.instance_name) { + Fail("schema-mismatch", "A reference row identity does not match HDF5."); } - return *found; + ordered.push_back(&*found); + } + return ordered; } -CanonicalComparisonRow canonicalRow( - const HdfNode& node, - const ComparisonQuantity quantity, - std::string component, - const double value, - std::string unit, - std::string coordinateSystem, - std::string datasetPath) { - return { - kModelId, - kStepName, - kFrameIndex, - node.instanceName, - node.sourceNodeLabel, - quantity, - std::move(component), - value, - std::move(unit), - std::move(coordinateSystem), - std::move(datasetPath)}; +double TableScale(const ReferenceTable& table, const std::size_t value_index) { + double scale = 0.0; + for (const auto& row : table.rows) { + if (value_index >= row.values.size()) { + Fail("schema-mismatch", "A reference row has the wrong component arity."); + } + scale = (std::max)(scale, std::abs(row.values[value_index])); + } + return scale; } -void appendNodalRows( - ComparisonReport& report, - const HdfProjection& hdf, - const std::vector& reference, - const ComparisonQuantity quantity, - const std::array& components, - const std::array& units, - const std::vector& fesaValues, - const char* datasetPath) { - for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) { - for (std::size_t component = 0U; component < components.size(); ++component) { - auto fesa = canonicalRow( - hdf.nodes[node], quantity, components[component], - fesaValues[node * 6U + component], units[component], - "global-cartesian", datasetPath); - auto abaqus = canonicalRow( - hdf.nodes[node], quantity, components[component], - reference[node]->values[component], units[component], - "global-cartesian", datasetPath); - report.rows.push_back( - {std::move(fesa), std::move(abaqus), 0.0, 0.0, false}); +std::vector NormalizeStations( + const Domain& domain, const HdfProjection& hdf, + const ReferenceTable& section_table) { + auto model_result = AnalysisModel::Create(domain); + if (!model_result.HasValue()) { + Fail("schema-mismatch", + "The approved input cannot create an analysis view."); + } + const AnalysisModel model = std::move(model_result.Value()); + const std::array tolerances = { + kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 0U), + kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 3U), + kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 1U), + kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 2U)}; + std::vector endpoints; + endpoints.reserve(hdf.elements.size() * 2U); + for (std::size_t element = 0U; element < hdf.elements.size(); ++element) { + for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { + const auto node = static_cast( + hdf.elements[element].node_internal_ids[endpoint]); + std::array values{}; + for (std::size_t component = 0U; component < values.size(); ++component) { + values[component] = + hdf.section_resultants[(element * 2U + endpoint) * 4U + component]; + } + endpoints.push_back({static_cast(element), + static_cast(endpoint), + domain.Nodes()[node].source_id, + {}, + values}); + } + } + auto normalized = ResultRecovery::NormalizeSectionResultantsToNodeStations( + model, endpoints, tolerances); + if (!normalized.HasValue()) { + const auto& diagnostics = normalized.GetStatus().Diagnostics(); + const std::string code = + diagnostics.empty() ? std::string{} : diagnostics[0U].code; + if (code == "node-station-tolerance-failure") { + Fail("tolerance-failure", + "Interior endpoint section resultants disagree."); + } + Fail("schema-mismatch", + "A node station is not eligible for legacy projection."); + } + return std::move(normalized.Value()); +} + +const NodeStationResultRow& FindStation( + const std::vector& stations, const HdfNode& node) { + const auto found = std::find_if( + stations.begin(), stations.end(), [&](const NodeStationResultRow& row) { + return row.node.instance_name == node.instance_name && + row.node.source_label == node.source_node_label; + }); + if (found == stations.end()) { + Fail("schema-mismatch", "A projected HDF5 node station is missing."); + } + return *found; +} + +CanonicalComparisonRow CanonicalRow(const HdfNode& node, + const ComparisonQuantity quantity, + std::string component, const double value, + std::string unit, + std::string coordinate_system, + std::string dataset_path) { + return {kModelId, + kStepName, + kFrameIndex, + node.instance_name, + node.source_node_label, + quantity, + std::move(component), + value, + std::move(unit), + std::move(coordinate_system), + std::move(dataset_path)}; +} + +void AppendNodalRows(ComparisonReport& report, const HdfProjection& hdf, + const std::vector& reference, + const ComparisonQuantity quantity, + const std::array& components, + const std::array& units, + const std::vector& fesa_values, + const char* dataset_path) { + for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) { + for (std::size_t component = 0U; component < components.size(); + ++component) { + auto fesa = + CanonicalRow(hdf.nodes[node], quantity, components[component], + fesa_values[node * 6U + component], units[component], + "global-cartesian", dataset_path); + auto abaqus = + CanonicalRow(hdf.nodes[node], quantity, components[component], + reference[node]->values[component], units[component], + "global-cartesian", dataset_path); + report.rows.push_back( + {std::move(fesa), std::move(abaqus), 0.0, 0.0, false}); + } + } +} + +void AppendSectionRows(ComparisonReport& report, const HdfProjection& hdf, + const std::vector& reference, + const std::vector& stations) { + const std::array components = {"N", "T", "My", "Mz"}; + const std::array units = {"force", "force*length", + "force*length", "force*length"}; + const std::array reference_columns = {0U, 3U, 1U, 2U}; + for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) { + const auto& station = FindStation(stations, hdf.nodes[node]); + for (std::size_t component = 0U; component < components.size(); + ++component) { + auto fesa = CanonicalRow( + hdf.nodes[node], ComparisonQuantity::kSectionResultant, + components[component], station.section_resultant[component], + units[component], "beam-local", kSectionPath); + auto abaqus = + CanonicalRow(hdf.nodes[node], ComparisonQuantity::kSectionResultant, + components[component], + reference[node]->values[reference_columns[component]], + units[component], "beam-local", kSectionPath); + report.rows.push_back( + {std::move(fesa), std::move(abaqus), 0.0, 0.0, false}); + } + } +} + +double AbsoluteFloor(const ComparisonQuantity quantity, const std::string&) { + return quantity == ComparisonQuantity::kDisplacement ? kKinematicFloor + : kForceMomentFloor; +} + +void EvaluateGroup(ComparisonReport& report, const ComparisonQuantity quantity, + const std::string& component) { + std::vector row_indices; + for (std::size_t index = 0U; index < report.rows.size(); ++index) { + if (report.rows[index].reference.quantity == quantity && + report.rows[index].reference.component == component) { + row_indices.push_back(index); + } + } + if (row_indices.empty()) { + Fail("schema-mismatch", "A canonical comparison component has no rows."); + } + double reference_scale = 0.0; + for (const std::size_t index : row_indices) { + reference_scale = (std::max)(reference_scale, + std::abs(report.rows[index].reference.value)); + } + const double tolerance = AbsoluteFloor(quantity, component) + + kRelativeCoefficient * reference_scale; + double maximum_absolute = -1.0; + double maximum_normalized = 0.0; + std::size_t worst_row = row_indices.front(); + long double squared_error = 0.0L; + for (const std::size_t index : row_indices) { + auto& row = report.rows[index]; + row.absolute_error = std::abs(row.fesa.value - row.reference.value); + if (!std::isfinite(row.absolute_error)) { + Fail("schema-mismatch", "A canonical row error is nonfinite."); + } + row.tolerance = tolerance; + row.passed = row.absolute_error <= tolerance; + report.passed = report.passed && row.passed; + const double normalized = row.absolute_error / tolerance; + if (row.absolute_error > maximum_absolute) { + maximum_absolute = row.absolute_error; + worst_row = index; + } + maximum_normalized = (std::max)(maximum_normalized, normalized); + const long double error = static_cast(row.absolute_error); + squared_error += error * error; + } + const double norm_error = std::sqrt(static_cast(squared_error)); + const double rms_error = std::sqrt(static_cast( + squared_error / static_cast(row_indices.size()))); + if (!std::isfinite(norm_error) || !std::isfinite(rms_error)) { + Fail("schema-mismatch", "A component aggregate error is nonfinite."); + } + report.metrics.push_back({quantity, component, reference_scale, + maximum_absolute, maximum_normalized, rms_error, + norm_error, worst_row}); +} + +PhysicsEvidence MakePhysicsEvidence(const Domain& domain, + const HdfProjection& hdf) { + auto model_result = AnalysisModel::Create(domain); + if (!model_result.HasValue()) { + Fail("schema-mismatch", + "The approved input cannot create physics evidence."); + } + const AnalysisModel model = std::move(model_result.Value()); + auto dofs_result = DofManager::Create(model); + if (!dofs_result.HasValue()) { + Fail("schema-mismatch", "The approved input cannot create a DOF map."); + } + const DofManager dofs = std::move(dofs_result.Value()); + auto load_result = LoadAssembler::AssembleFullNodalLoad(model, dofs); + if (!load_result.HasValue()) { + Fail("schema-mismatch", "The approved input load cannot be assembled."); + } + const Vector load = std::move(load_result.Value()); + if (load.Size() != hdf.reaction.size()) { + Fail("schema-mismatch", "The load and reaction spaces are inconsistent."); + } + + PhysicsEvidence evidence{}; + long double residual_squared = 0.0L; + for (const std::size_t free_dof : dofs.FreeDofs()) { + const long double value = static_cast(hdf.reaction[free_dof]); + residual_squared += value * value; + } + evidence.free_residual_norm = + std::sqrt(static_cast(residual_squared)); + for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { + const auto& coordinates = domain.Nodes()[node].coordinates; + const std::array applied = { + load[node * 6U + 0U], load[node * 6U + 1U], load[node * 6U + 2U]}; + const std::array reaction = {hdf.reaction[node * 6U + 0U], + hdf.reaction[node * 6U + 1U], + hdf.reaction[node * 6U + 2U]}; + for (std::size_t component = 0U; component < 3U; ++component) { + evidence.applied_force[component] += applied[component]; + evidence.reaction_force[component] += reaction[component]; + evidence.applied_moment_about_origin[component] += + load[node * 6U + 3U + component]; + evidence.reaction_moment_about_origin[component] += + hdf.reaction[node * 6U + 3U + component]; + } + evidence.applied_moment_about_origin[0U] += + coordinates[1U] * applied[2U] - coordinates[2U] * applied[1U]; + evidence.applied_moment_about_origin[1U] += + coordinates[2U] * applied[0U] - coordinates[0U] * applied[2U]; + evidence.applied_moment_about_origin[2U] += + coordinates[0U] * applied[1U] - coordinates[1U] * applied[0U]; + evidence.reaction_moment_about_origin[0U] += + coordinates[1U] * reaction[2U] - coordinates[2U] * reaction[1U]; + evidence.reaction_moment_about_origin[1U] += + coordinates[2U] * reaction[0U] - coordinates[0U] * reaction[2U]; + evidence.reaction_moment_about_origin[2U] += + coordinates[0U] * reaction[1U] - coordinates[1U] * reaction[0U]; + } + evidence.endpoint_consistency_passed = true; + return evidence; +} + +const char* QuantityName(const ComparisonQuantity quantity) { + switch (quantity) { + case ComparisonQuantity::kDisplacement: + return "displacement"; + case ComparisonQuantity::kReaction: + return "reaction"; + case ComparisonQuantity::kSectionResultant: + return "section_resultant"; + } + return "unknown"; +} + +void WriteJsonString(std::ostream& stream, const std::string& value) { + static constexpr char kDigits[] = "0123456789abcdef"; + stream.put('"'); + for (const unsigned char character : value) { + switch (character) { + case '"': + stream << "\\\""; + break; + case '\\': + stream << "\\\\"; + break; + case '\b': + stream << "\\b"; + break; + case '\f': + stream << "\\f"; + break; + case '\n': + stream << "\\n"; + break; + case '\r': + stream << "\\r"; + break; + case '\t': + stream << "\\t"; + break; + default: + if (character < 0x20U) { + stream << "\\u00" << kDigits[character >> 4U] + << kDigits[character & 0x0fU]; + } else { + stream.put(static_cast(character)); } + break; } + } + stream.put('"'); } -void appendSectionRows( - ComparisonReport& report, - const HdfProjection& hdf, - const std::vector& reference, - const std::vector& stations) { - const std::array components = {"N", "T", "My", "Mz"}; - const std::array units = { - "force", "force*length", "force*length", "force*length"}; - const std::array referenceColumns = {0U, 3U, 1U, 2U}; - for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) { - const auto& station = findStation(stations, hdf.nodes[node]); - for (std::size_t component = 0U; component < components.size(); ++component) { - auto fesa = canonicalRow( - hdf.nodes[node], ComparisonQuantity::sectionResultant, - components[component], station.section_resultant[component], - units[component], "beam-local", kSectionPath); - auto abaqus = canonicalRow( - hdf.nodes[node], ComparisonQuantity::sectionResultant, - components[component], - reference[node]->values[referenceColumns[component]], - units[component], "beam-local", kSectionPath); - report.rows.push_back( - {std::move(fesa), std::move(abaqus), 0.0, 0.0, false}); - } - } +void WriteCanonicalRow(std::ostream& stream, + const CanonicalComparisonRow& row) { + stream << "{\"model_id\":"; + WriteJsonString(stream, row.model_id); + stream << ",\"step_name\":"; + WriteJsonString(stream, row.step_name); + stream << ",\"frame_index\":" << row.frame_index << ",\"instance_name\":"; + WriteJsonString(stream, row.instance_name); + stream << ",\"source_node_label\":" << row.source_node_label + << ",\"quantity\":"; + WriteJsonString(stream, QuantityName(row.quantity)); + stream << ",\"component\":"; + WriteJsonString(stream, row.component); + stream << ",\"value\":" << row.value << ",\"unit_dimension\":"; + WriteJsonString(stream, row.unit_dimension); + stream << ",\"coordinate_system\":"; + WriteJsonString(stream, row.coordinate_system); + stream << ",\"hdf5_dataset_path\":"; + WriteJsonString(stream, row.hdf5_dataset_path); + stream << '}'; } -double absoluteFloor( - const ComparisonQuantity quantity, const std::string&) { - return quantity == ComparisonQuantity::displacement - ? kKinematicFloor - : kForceMomentFloor; +void WriteArray(std::ostream& stream, const std::array& values) { + stream << '[' << values[0U] << ',' << values[1U] << ',' << values[2U] << ']'; } -void evaluateGroup( - ComparisonReport& report, - const ComparisonQuantity quantity, - const std::string& component) { - std::vector rowIndices; - for (std::size_t index = 0U; index < report.rows.size(); ++index) { - if (report.rows[index].reference.quantity == quantity && - report.rows[index].reference.component == component) { - rowIndices.push_back(index); - } +bool FiniteReport(const ComparisonReport& report) { + const auto finite_array = [](const std::array& values) { + return std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); }); + }; + if (!std::isfinite(report.physics_evidence.free_residual_norm) || + !finite_array(report.physics_evidence.applied_force) || + !finite_array(report.physics_evidence.reaction_force) || + !finite_array(report.physics_evidence.applied_moment_about_origin) || + !finite_array(report.physics_evidence.reaction_moment_about_origin)) { + return false; + } + for (const auto& row : report.rows) { + if (!std::isfinite(row.fesa.value) || !std::isfinite(row.reference.value) || + !std::isfinite(row.absolute_error) || !std::isfinite(row.tolerance)) { + return false; } - if (rowIndices.empty()) { - fail("schema-mismatch", "A canonical comparison component has no rows."); - } - double referenceScale = 0.0; - for (const std::size_t index : rowIndices) { - referenceScale = (std::max)( - referenceScale, std::abs(report.rows[index].reference.value)); - } - const double tolerance = - absoluteFloor(quantity, component) + - kRelativeCoefficient * referenceScale; - double maximumAbsolute = -1.0; - double maximumNormalized = 0.0; - std::size_t worstRow = rowIndices.front(); - long double squaredError = 0.0L; - for (const std::size_t index : rowIndices) { - auto& row = report.rows[index]; - row.absoluteError = std::abs(row.fesa.value - row.reference.value); - if (!std::isfinite(row.absoluteError)) { - fail("schema-mismatch", "A canonical row error is nonfinite."); - } - row.tolerance = tolerance; - row.passed = row.absoluteError <= tolerance; - report.passed = report.passed && row.passed; - const double normalized = row.absoluteError / tolerance; - if (row.absoluteError > maximumAbsolute) { - maximumAbsolute = row.absoluteError; - worstRow = index; - } - maximumNormalized = (std::max)(maximumNormalized, normalized); - const long double error = static_cast(row.absoluteError); - squaredError += error * error; - } - const double normError = std::sqrt(static_cast(squaredError)); - const double rmsError = std::sqrt( - static_cast(squaredError / - static_cast(rowIndices.size()))); - if (!std::isfinite(normError) || !std::isfinite(rmsError)) { - fail("schema-mismatch", "A component aggregate error is nonfinite."); - } - report.metrics.push_back({ - quantity, - component, - referenceScale, - maximumAbsolute, - maximumNormalized, - rmsError, - normError, - worstRow}); + } + return std::all_of(report.metrics.begin(), report.metrics.end(), + [](const ComponentMetrics& metric) { + return std::isfinite(metric.reference_scale) && + std::isfinite(metric.maximum_absolute_error) && + std::isfinite(metric.maximum_normalized_error) && + std::isfinite(metric.rms_error) && + std::isfinite(metric.norm_error); + }); } -PhysicsEvidence makePhysicsEvidence( - const Domain& domain, - const HdfProjection& hdf) { - auto modelResult = AnalysisModel::Create(domain); - if (!modelResult.HasValue()) { - fail("schema-mismatch", "The approved input cannot create physics evidence."); - } - const AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = DofManager::Create(model); - if (!dofsResult.HasValue()) { - fail("schema-mismatch", "The approved input cannot create a DOF map."); - } - const DofManager dofs = std::move(dofsResult.Value()); - auto loadResult = LoadAssembler::AssembleFullNodalLoad(model, dofs); - if (!loadResult.HasValue()) { - fail("schema-mismatch", "The approved input load cannot be assembled."); - } - const Vector load = std::move(loadResult.Value()); - if (load.Size() != hdf.reaction.size()) { - fail("schema-mismatch", "The load and reaction spaces are inconsistent."); +} // namespace + +Result ReferenceComparison::Compare( + const std::filesystem::path& results_hdf5, + const std::filesystem::path& legacy_reference_directory) { + try { + RequireExactArtifactInventory(legacy_reference_directory); + const auto input = legacy_reference_directory / kInputName; + Domain domain = ReadApprovedDomain(input); + const ReferenceTable displacement = + ReadReferenceCsv(legacy_reference_directory / kDisplacementName, + {"Frame", "Part Instance Name", "Node Label", "U-U1", + "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"}); + const ReferenceTable reaction = + ReadReferenceCsv(legacy_reference_directory / kReactionName, + {"Frame", "Part Instance Name", "Node Label", "RF-RF1", + "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"}); + const ReferenceTable section = + ReadReferenceCsv(legacy_reference_directory / kSectionName, + {"Frame", "Part Instance Name", "Node Label", "SF-SF1", + "SM-SM1", "SM-SM2", "SM-SM3"}); + HdfProjection hdf = ReadHdfProjection(results_hdf5, input, domain); + const auto displacement_rows = OrderedRows(displacement, hdf.nodes); + const auto reaction_rows = OrderedRows(reaction, hdf.nodes); + const auto section_rows = OrderedRows(section, hdf.nodes); + const auto stations = NormalizeStations(domain, hdf, section); + if (stations.size() != hdf.nodes.size()) { + Fail("schema-mismatch", "The HDF5 node-station row set is incomplete."); } - PhysicsEvidence evidence{}; - long double residualSquared = 0.0L; - for (const std::size_t freeDof : dofs.FreeDofs()) { - const long double value = - static_cast(hdf.reaction[freeDof]); - residualSquared += value * value; + ComparisonReport report{}; + report.passed = true; + AppendNodalRows( + report, hdf, displacement_rows, ComparisonQuantity::kDisplacement, + {"UX", "UY", "UZ", "URX", "URY", "URZ"}, + {"length", "length", "length", "radian", "radian", "radian"}, + hdf.displacement, kDisplacementPath); + AppendNodalRows(report, hdf, reaction_rows, ComparisonQuantity::kReaction, + {"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}, + {"force", "force", "force", "force*length", "force*length", + "force*length"}, + hdf.reaction, kReactionPath); + AppendSectionRows(report, hdf, section_rows, stations); + + for (const std::string& component : + {"UX", "UY", "UZ", "URX", "URY", "URZ"}) { + EvaluateGroup(report, ComparisonQuantity::kDisplacement, component); } - evidence.freeResidualNorm = - std::sqrt(static_cast(residualSquared)); - for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { - const auto& coordinates = domain.Nodes()[node].coordinates; - const std::array applied = { - load[node * 6U + 0U], - load[node * 6U + 1U], - load[node * 6U + 2U]}; - const std::array reaction = { - hdf.reaction[node * 6U + 0U], - hdf.reaction[node * 6U + 1U], - hdf.reaction[node * 6U + 2U]}; - for (std::size_t component = 0U; component < 3U; ++component) { - evidence.appliedForce[component] += applied[component]; - evidence.reactionForce[component] += reaction[component]; - evidence.appliedMomentAboutOrigin[component] += - load[node * 6U + 3U + component]; - evidence.reactionMomentAboutOrigin[component] += - hdf.reaction[node * 6U + 3U + component]; - } - evidence.appliedMomentAboutOrigin[0U] += - coordinates[1U] * applied[2U] - coordinates[2U] * applied[1U]; - evidence.appliedMomentAboutOrigin[1U] += - coordinates[2U] * applied[0U] - coordinates[0U] * applied[2U]; - evidence.appliedMomentAboutOrigin[2U] += - coordinates[0U] * applied[1U] - coordinates[1U] * applied[0U]; - evidence.reactionMomentAboutOrigin[0U] += - coordinates[1U] * reaction[2U] - coordinates[2U] * reaction[1U]; - evidence.reactionMomentAboutOrigin[1U] += - coordinates[2U] * reaction[0U] - coordinates[0U] * reaction[2U]; - evidence.reactionMomentAboutOrigin[2U] += - coordinates[0U] * reaction[1U] - coordinates[1U] * reaction[0U]; + for (const std::string& component : + {"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}) { + EvaluateGroup(report, ComparisonQuantity::kReaction, component); } - evidence.endpointConsistencyPassed = true; - return evidence; + for (const std::string& component : {"N", "T", "My", "Mz"}) { + EvaluateGroup(report, ComparisonQuantity::kSectionResultant, component); + } + report.physics_evidence = MakePhysicsEvidence(domain, hdf); + report.stress_comparison_applicable = false; + report.stress_comparison_reason = + "Abaqus beam stress comparison is N/A; analytical/unit and HDF5 " + "schema tests provide stress evidence."; + return Result::Success(std::move(report)); + } catch (const ComparisonFailure& failure) { + return Result::Failure( + ComparisonFailureStatus(failure.Code(), failure.what())); + } catch (const std::exception& failure) { + return Result::Failure( + ComparisonFailureStatus("schema-mismatch", failure.what())); + } } -const char* quantityName(const ComparisonQuantity quantity) { - switch (quantity) { - case ComparisonQuantity::displacement: - return "displacement"; - case ComparisonQuantity::reaction: - return "reaction"; - case ComparisonQuantity::sectionResultant: - return "section_resultant"; +Status ReferenceComparison::WriteDeterministicJson( + const ComparisonReport& report, const std::filesystem::path& output_json) { + if (output_json.empty() || output_json.filename().empty() || + !FiniteReport(report)) { + return Status::Failure( + FailureCategory::kOutput, + {{Severity::kError, + "comparison-json-write-failure", + {}, + "", + kModelId, + "The deterministic comparison report or output path is invalid."}}); + } + std::ofstream stream{output_json, std::ios::binary | std::ios::trunc}; + if (!stream) { + return Status::Failure( + FailureCategory::kOutput, + {{Severity::kError, + "comparison-json-write-failure", + {}, + "", + kModelId, + "The deterministic comparison JSON cannot be opened."}}); + } + stream.imbue(std::locale::classic()); + stream << std::setprecision(std::numeric_limits::max_digits10); + stream << "{\"rows\":["; + for (std::size_t index = 0U; index < report.rows.size(); ++index) { + if (index != 0U) { + stream << ','; } - return "unknown"; -} - -void writeJsonString(std::ostream& stream, const std::string& value) { - static constexpr char digits[] = "0123456789abcdef"; - stream.put('"'); - for (const unsigned char character : value) { - switch (character) { - case '"': - stream << "\\\""; - break; - case '\\': - stream << "\\\\"; - break; - case '\b': - stream << "\\b"; - break; - case '\f': - stream << "\\f"; - break; - case '\n': - stream << "\\n"; - break; - case '\r': - stream << "\\r"; - break; - case '\t': - stream << "\\t"; - break; - default: - if (character < 0x20U) { - stream << "\\u00" << digits[character >> 4U] - << digits[character & 0x0fU]; - } else { - stream.put(static_cast(character)); - } - break; - } + const auto& row = report.rows[index]; + stream << "{\"fesa\":"; + WriteCanonicalRow(stream, row.fesa); + stream << ",\"reference\":"; + WriteCanonicalRow(stream, row.reference); + stream << ",\"absolute_error\":" << row.absolute_error + << ",\"tolerance\":" << row.tolerance + << ",\"passed\":" << (row.passed ? "true" : "false") << '}'; + } + stream << "],\"metrics\":["; + for (std::size_t index = 0U; index < report.metrics.size(); ++index) { + if (index != 0U) { + stream << ','; } - stream.put('"'); -} - -void writeCanonicalRow( - std::ostream& stream, const CanonicalComparisonRow& row) { - stream << "{\"model_id\":"; - writeJsonString(stream, row.modelId); - stream << ",\"step_name\":"; - writeJsonString(stream, row.stepName); - stream << ",\"frame_index\":" << row.frameIndex - << ",\"instance_name\":"; - writeJsonString(stream, row.instanceName); - stream << ",\"source_node_label\":" << row.sourceNodeLabel - << ",\"quantity\":"; - writeJsonString(stream, quantityName(row.quantity)); + const auto& metric = report.metrics[index]; + stream << "{\"quantity\":"; + WriteJsonString(stream, QuantityName(metric.quantity)); stream << ",\"component\":"; - writeJsonString(stream, row.component); - stream << ",\"value\":" << row.value << ",\"unit_dimension\":"; - writeJsonString(stream, row.unitDimension); - stream << ",\"coordinate_system\":"; - writeJsonString(stream, row.coordinateSystem); - stream << ",\"hdf5_dataset_path\":"; - writeJsonString(stream, row.hdf5DatasetPath); - stream << '}'; + WriteJsonString(stream, metric.component); + stream << ",\"reference_scale\":" << metric.reference_scale + << ",\"maximum_absolute_error\":" << metric.maximum_absolute_error + << ",\"maximum_normalized_error\":" + << metric.maximum_normalized_error + << ",\"rms_error\":" << metric.rms_error + << ",\"norm_error\":" << metric.norm_error + << ",\"worst_row\":" << metric.worst_row << '}'; + } + stream << "],\"physics_evidence\":{\"free_residual_norm\":" + << report.physics_evidence.free_residual_norm << ",\"applied_force\":"; + WriteArray(stream, report.physics_evidence.applied_force); + stream << ",\"reaction_force\":"; + WriteArray(stream, report.physics_evidence.reaction_force); + stream << ",\"applied_moment_about_origin\":"; + WriteArray(stream, report.physics_evidence.applied_moment_about_origin); + stream << ",\"reaction_moment_about_origin\":"; + WriteArray(stream, report.physics_evidence.reaction_moment_about_origin); + stream << ",\"endpoint_consistency_passed\":" + << (report.physics_evidence.endpoint_consistency_passed ? "true" + : "false") + << "},\"stress_comparison_applicable\":" + << (report.stress_comparison_applicable ? "true" : "false") + << ",\"stress_comparison_reason\":"; + WriteJsonString(stream, report.stress_comparison_reason); + stream << ",\"passed\":" << (report.passed ? "true" : "false") << "}\n"; + if (!stream) { + return Status::Failure( + FailureCategory::kOutput, + {{Severity::kError, + "comparison-json-write-failure", + {}, + "", + kModelId, + "The deterministic comparison JSON write failed."}}); + } + return Status::Ok(); } -void writeArray(std::ostream& stream, const std::array& values) { - stream << '[' << values[0U] << ',' << values[1U] << ',' << values[2U] - << ']'; -} - -bool finiteReport(const ComparisonReport& report) { - const auto finiteArray = [](const std::array& values) { - return std::all_of(values.begin(), values.end(), [](const double value) { - return std::isfinite(value); - }); - }; - if (!std::isfinite(report.physicsEvidence.freeResidualNorm) || - !finiteArray(report.physicsEvidence.appliedForce) || - !finiteArray(report.physicsEvidence.reactionForce) || - !finiteArray(report.physicsEvidence.appliedMomentAboutOrigin) || - !finiteArray(report.physicsEvidence.reactionMomentAboutOrigin)) { - return false; - } - for (const auto& row : report.rows) { - if (!std::isfinite(row.fesa.value) || - !std::isfinite(row.reference.value) || - !std::isfinite(row.absoluteError) || - !std::isfinite(row.tolerance)) { - return false; - } - } - return std::all_of( - report.metrics.begin(), report.metrics.end(), - [](const ComponentMetrics& metric) { - return std::isfinite(metric.referenceScale) && - std::isfinite(metric.maximumAbsoluteError) && - std::isfinite(metric.maximumNormalizedError) && - std::isfinite(metric.rmsError) && - std::isfinite(metric.normError); - }); -} - -} // namespace - -Result ReferenceComparison::compare( - const std::filesystem::path& resultsHdf5, - const std::filesystem::path& legacyReferenceDirectory) { - try { - requireExactArtifactInventory(legacyReferenceDirectory); - const auto input = legacyReferenceDirectory / kInputName; - Domain domain = readApprovedDomain(input); - const ReferenceTable displacement = readReferenceCsv( - legacyReferenceDirectory / kDisplacementName, - {"Frame", "Part Instance Name", "Node Label", "U-U1", "U-U2", - "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"}); - const ReferenceTable reaction = readReferenceCsv( - legacyReferenceDirectory / kReactionName, - {"Frame", "Part Instance Name", "Node Label", "RF-RF1", "RF-RF2", - "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"}); - const ReferenceTable section = readReferenceCsv( - legacyReferenceDirectory / kSectionName, - {"Frame", "Part Instance Name", "Node Label", "SF-SF1", "SM-SM1", - "SM-SM2", "SM-SM3"}); - HdfProjection hdf = readHdfProjection(resultsHdf5, input, domain); - const auto displacementRows = orderedRows(displacement, hdf.nodes); - const auto reactionRows = orderedRows(reaction, hdf.nodes); - const auto sectionRows = orderedRows(section, hdf.nodes); - const auto stations = normalizeStations(domain, hdf, section); - if (stations.size() != hdf.nodes.size()) { - fail("schema-mismatch", "The HDF5 node-station row set is incomplete."); - } - - ComparisonReport report{}; - report.passed = true; - appendNodalRows( - report, - hdf, - displacementRows, - ComparisonQuantity::displacement, - {"UX", "UY", "UZ", "URX", "URY", "URZ"}, - {"length", "length", "length", "radian", "radian", "radian"}, - hdf.displacement, - kDisplacementPath); - appendNodalRows( - report, - hdf, - reactionRows, - ComparisonQuantity::reaction, - {"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}, - {"force", "force", "force", "force*length", "force*length", - "force*length"}, - hdf.reaction, - kReactionPath); - appendSectionRows(report, hdf, sectionRows, stations); - - for (const std::string& component : - {"UX", "UY", "UZ", "URX", "URY", "URZ"}) { - evaluateGroup(report, ComparisonQuantity::displacement, component); - } - for (const std::string& component : - {"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}) { - evaluateGroup(report, ComparisonQuantity::reaction, component); - } - for (const std::string& component : {"N", "T", "My", "Mz"}) { - evaluateGroup(report, ComparisonQuantity::sectionResultant, component); - } - report.physicsEvidence = makePhysicsEvidence(domain, hdf); - report.stressComparisonApplicable = false; - report.stressComparisonReason = - "Abaqus beam stress comparison is N/A; analytical/unit and HDF5 " - "schema tests provide stress evidence."; - return Result::Success(std::move(report)); - } catch (const ComparisonFailure& failure) { - return Result::Failure( - comparisonFailureStatus(failure.code(), failure.what())); - } catch (const std::exception& failure) { - return Result::Failure(comparisonFailureStatus( - "schema-mismatch", failure.what())); - } -} - -Status ReferenceComparison::writeDeterministicJson( - const ComparisonReport& report, - const std::filesystem::path& outputJson) { - if (outputJson.empty() || outputJson.filename().empty() || - !finiteReport(report)) { - return Status::Failure( - FailureCategory::kOutput, - {{Severity::kError, - "comparison-json-write-failure", - {}, - "", - kModelId, - "The deterministic comparison report or output path is invalid."}}); - } - std::ofstream stream{outputJson, std::ios::binary | std::ios::trunc}; - if (!stream) { - return Status::Failure( - FailureCategory::kOutput, - {{Severity::kError, - "comparison-json-write-failure", - {}, - "", - kModelId, - "The deterministic comparison JSON cannot be opened."}}); - } - stream.imbue(std::locale::classic()); - stream << std::setprecision(std::numeric_limits::max_digits10); - stream << "{\"rows\":["; - for (std::size_t index = 0U; index < report.rows.size(); ++index) { - if (index != 0U) { - stream << ','; - } - const auto& row = report.rows[index]; - stream << "{\"fesa\":"; - writeCanonicalRow(stream, row.fesa); - stream << ",\"reference\":"; - writeCanonicalRow(stream, row.reference); - stream << ",\"absolute_error\":" << row.absoluteError - << ",\"tolerance\":" << row.tolerance - << ",\"passed\":" << (row.passed ? "true" : "false") << '}'; - } - stream << "],\"metrics\":["; - for (std::size_t index = 0U; index < report.metrics.size(); ++index) { - if (index != 0U) { - stream << ','; - } - const auto& metric = report.metrics[index]; - stream << "{\"quantity\":"; - writeJsonString(stream, quantityName(metric.quantity)); - stream << ",\"component\":"; - writeJsonString(stream, metric.component); - stream << ",\"reference_scale\":" << metric.referenceScale - << ",\"maximum_absolute_error\":" - << metric.maximumAbsoluteError - << ",\"maximum_normalized_error\":" - << metric.maximumNormalizedError - << ",\"rms_error\":" << metric.rmsError - << ",\"norm_error\":" << metric.normError - << ",\"worst_row\":" << metric.worstRow << '}'; - } - stream << "],\"physics_evidence\":{\"free_residual_norm\":" - << report.physicsEvidence.freeResidualNorm - << ",\"applied_force\":"; - writeArray(stream, report.physicsEvidence.appliedForce); - stream << ",\"reaction_force\":"; - writeArray(stream, report.physicsEvidence.reactionForce); - stream << ",\"applied_moment_about_origin\":"; - writeArray(stream, report.physicsEvidence.appliedMomentAboutOrigin); - stream << ",\"reaction_moment_about_origin\":"; - writeArray(stream, report.physicsEvidence.reactionMomentAboutOrigin); - stream << ",\"endpoint_consistency_passed\":" - << (report.physicsEvidence.endpointConsistencyPassed ? "true" : "false") - << "},\"stress_comparison_applicable\":" - << (report.stressComparisonApplicable ? "true" : "false") - << ",\"stress_comparison_reason\":"; - writeJsonString(stream, report.stressComparisonReason); - stream << ",\"passed\":" << (report.passed ? "true" : "false") << "}\n"; - if (!stream) { - return Status::Failure( - FailureCategory::kOutput, - {{Severity::kError, - "comparison-json-write-failure", - {}, - "", - kModelId, - "The deterministic comparison JSON write failed."}}); - } - return Status::Ok(); -} - -} // namespace fesa::test +} // namespace fesa::test diff --git a/tests/reference/reference_comparison.h b/tests/reference/reference_comparison.h new file mode 100644 index 0000000..4451b43 --- /dev/null +++ b/tests/reference/reference_comparison.h @@ -0,0 +1,81 @@ +#ifndef FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_ +#define FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_ + +#include +#include +#include +#include +#include +#include + +#include "fesa/core/status.h" + +namespace fesa::test { + +enum class ComparisonQuantity { kDisplacement, kReaction, kSectionResultant }; + +struct CanonicalComparisonRow { + std::string model_id; + std::string step_name; + std::size_t frame_index; + std::string instance_name; + std::int64_t source_node_label; + ComparisonQuantity quantity; + std::string component; + double value; + std::string unit_dimension; + std::string coordinate_system; + std::string hdf5_dataset_path; +}; + +struct RowDecision { + CanonicalComparisonRow fesa; + CanonicalComparisonRow reference; + double absolute_error; + double tolerance; + bool passed; +}; + +struct ComponentMetrics { + ComparisonQuantity quantity; + std::string component; + double reference_scale; + double maximum_absolute_error; + double maximum_normalized_error; + double rms_error; + double norm_error; + std::size_t worst_row; +}; + +struct PhysicsEvidence { + double free_residual_norm; + std::array applied_force; + std::array reaction_force; + std::array applied_moment_about_origin; + std::array reaction_moment_about_origin; + bool endpoint_consistency_passed; +}; + +struct ComparisonReport { + std::vector rows; + std::vector metrics; + PhysicsEvidence physics_evidence; + bool stress_comparison_applicable; + std::string stress_comparison_reason; + bool passed; +}; + +// Test-only comparison support keeps Abaqus artifacts read-only and exposes no +// backend handles to the implementation or downstream verification Steps. +class ReferenceComparison { + public: + static Result Compare( + const std::filesystem::path& results_hdf5, + const std::filesystem::path& legacy_reference_directory); + static Status WriteDeterministicJson( + const ComparisonReport& report, const std::filesystem::path& output_json); +}; + +} // namespace fesa::test + +#endif // FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_ diff --git a/tests/reference/reference_comparison.hpp b/tests/reference/reference_comparison.hpp deleted file mode 100644 index e7c056b..0000000 --- a/tests/reference/reference_comparison.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include "fesa/core/status.h" - -#include -#include -#include -#include -#include -#include - -namespace fesa::test { - -enum class ComparisonQuantity { - displacement, - reaction, - sectionResultant -}; - -struct CanonicalComparisonRow { - std::string modelId; - std::string stepName; - std::size_t frameIndex; - std::string instanceName; - std::int64_t sourceNodeLabel; - ComparisonQuantity quantity; - std::string component; - double value; - std::string unitDimension; - std::string coordinateSystem; - std::string hdf5DatasetPath; -}; - -struct RowDecision { - CanonicalComparisonRow fesa; - CanonicalComparisonRow reference; - double absoluteError; - double tolerance; - bool passed; -}; - -struct ComponentMetrics { - ComparisonQuantity quantity; - std::string component; - double referenceScale; - double maximumAbsoluteError; - double maximumNormalizedError; - double rmsError; - double normError; - std::size_t worstRow; -}; - -struct PhysicsEvidence { - double freeResidualNorm; - std::array appliedForce; - std::array reactionForce; - std::array appliedMomentAboutOrigin; - std::array reactionMomentAboutOrigin; - bool endpointConsistencyPassed; -}; - -struct ComparisonReport { - std::vector rows; - std::vector metrics; - PhysicsEvidence physicsEvidence; - bool stressComparisonApplicable; - std::string stressComparisonReason; - bool passed; -}; - -// Test-only comparison support keeps Abaqus artifacts read-only and exposes no -// backend handles to the implementation or downstream verification Steps. -class ReferenceComparison { -public: - static Result compare( - const std::filesystem::path& resultsHdf5, - const std::filesystem::path& legacyReferenceDirectory); - static Status writeDeterministicJson( - const ComparisonReport& report, - const std::filesystem::path& outputJson); -}; - -} // namespace fesa::test diff --git a/tests/reference/reference_comparison_test.cpp b/tests/reference/reference_comparison_test.cpp index 9ec0267..bcf5845 100644 --- a/tests/reference/reference_comparison_test.cpp +++ b/tests/reference/reference_comparison_test.cpp @@ -1,11 +1,4 @@ -#include "reference_comparison.hpp" - -#include "fesa/analysis/analysis_model.h" -#include "fesa/analysis/analysis_state.h" -#include "fesa/fem/dof_manager.h" -#include "fesa/io/abaqus/input_reader.hpp" -#include "fesa/io/hdf5/hdf5_results_writer.hpp" -#include "fesa/model/domain.h" +#include "reference_comparison.h" #include @@ -24,6 +17,13 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/analysis/analysis_state.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/io/abaqus/input_reader.h" +#include "fesa/io/hdf5/hdf5_results_writer.h" +#include "fesa/model/domain.h" + #ifndef FESA_TEST_SOURCE_DIR #error FESA_TEST_SOURCE_DIR must identify the repository root. #endif @@ -49,682 +49,598 @@ using EndpointValues = std::array, 2>, kElementCount>; struct ComparisonValues { - NodalValues displacement{}; - NodalValues reaction{}; - EndpointValues sectionResultants{}; + NodalValues displacement{}; + NodalValues reaction{}; + EndpointValues section_resultants{}; }; -const std::filesystem::path& sourceRoot() { - static const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; - return root; +const std::filesystem::path& SourceRoot() { + static const std::filesystem::path kRoot{FESA_TEST_SOURCE_DIR}; + return kRoot; } -const std::filesystem::path& binaryRoot() { - static const std::filesystem::path root{FESA_TEST_BINARY_DIR}; - return root; +const std::filesystem::path& BinaryRoot() { + static const std::filesystem::path kRoot{FESA_TEST_BINARY_DIR}; + return kRoot; } -std::string readBytes(const std::filesystem::path& path) { - std::ifstream stream{path, std::ios::binary}; - if (!stream) { - throw std::runtime_error{"Unable to read fixture: " + path.string()}; - } - return {std::istreambuf_iterator{stream}, - std::istreambuf_iterator{}}; +std::string ReadBytes(const std::filesystem::path& path) { + std::ifstream stream{path, std::ios::binary}; + if (!stream) { + throw std::runtime_error{"Unable to read fixture: " + path.string()}; + } + return {std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; } -void writeBytes(const std::filesystem::path& path, const std::string& contents) { - std::ofstream stream{path, std::ios::binary | std::ios::trunc}; - if (!stream) { - throw std::runtime_error{"Unable to write build-local fixture: " + - path.string()}; - } - stream.write(contents.data(), static_cast(contents.size())); - if (!stream) { - throw std::runtime_error{"Unable to finish build-local fixture write."}; - } +void WriteBytes(const std::filesystem::path& path, + const std::string& contents) { + std::ofstream stream{path, std::ios::binary | std::ios::trunc}; + if (!stream) { + throw std::runtime_error{"Unable to write build-local fixture: " + + path.string()}; + } + stream.write(contents.data(), static_cast(contents.size())); + if (!stream) { + throw std::runtime_error{"Unable to finish build-local fixture write."}; + } } -std::vector readLines(const std::filesystem::path& path) { - std::ifstream stream{path}; - if (!stream) { - throw std::runtime_error{"Unable to read fixture lines."}; +std::vector ReadLines(const std::filesystem::path& path) { + std::ifstream stream{path}; + if (!stream) { + throw std::runtime_error{"Unable to read fixture lines."}; + } + std::vector lines; + for (std::string line; std::getline(stream, line);) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); } - std::vector lines; - for (std::string line; std::getline(stream, line);) { - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - lines.push_back(std::move(line)); - } - return lines; + lines.push_back(std::move(line)); + } + return lines; } -void writeLines( - const std::filesystem::path& path, - const std::vector& lines) { - std::ofstream stream{path, std::ios::trunc}; - if (!stream) { - throw std::runtime_error{"Unable to write build-local fixture lines."}; - } - for (const auto& line : lines) { - stream << line << '\n'; - } - if (!stream) { - throw std::runtime_error{"Unable to finish build-local line write."}; - } +void WriteLines(const std::filesystem::path& path, + const std::vector& lines) { + std::ofstream stream{path, std::ios::trunc}; + if (!stream) { + throw std::runtime_error{"Unable to write build-local fixture lines."}; + } + for (const auto& line : lines) { + stream << line << '\n'; + } + if (!stream) { + throw std::runtime_error{"Unable to finish build-local line write."}; + } } -void replaceFirst( - std::string& contents, - const std::string& from, - const std::string& to) { - const std::size_t position = contents.find(from); - if (position == std::string::npos) { - throw std::runtime_error{"Fixture token was not found: " + from}; - } - contents.replace(position, from.size(), to); +void ReplaceFirst(std::string& contents, const std::string& from, + const std::string& to) { + const std::size_t position = contents.find(from); + if (position == std::string::npos) { + throw std::runtime_error{"Fixture token was not found: " + from}; + } + contents.replace(position, from.size(), to); } -ComparisonValues referenceValues() { - ComparisonValues values{}; - const std::array uz = { - -1.0e-30, - -2.761905780e-4, - -1.066667140e-3, - -2.314286540e-3, - -3.961906300e-3, - -5.952383390e-3, - -8.228574880e-3, - -1.073333810e-2, - -1.340952890e-2, - -1.620000600e-2, - -1.904762720e-2}; - const std::array ury = { - 1.0e-29, - 5.428573350e-4, - 1.028571860e-3, - 1.457143460e-3, - 1.828572130e-3, - 2.142857990e-3, - 2.400001050e-3, - 2.600000940e-3, - 2.742858140e-3, - 2.828572640e-3, - 2.857143990e-3}; - std::array, kNodeCount> stations{}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - values.displacement[node][2U] = uz[node]; - values.displacement[node][4U] = ury[node]; - stations[node][2U] = node < kElementCount - ? 1.0e7 - 1.0e6 * static_cast(node) - : -1.56e-2; - } - values.reaction[0U][2U] = 1.0e6; - values.reaction[0U][4U] = -1.0e7; - for (std::size_t element = 0U; element < kElementCount; ++element) { - values.sectionResultants[element][0U] = stations[element]; - values.sectionResultants[element][1U] = stations[element + 1U]; - } - return values; +ComparisonValues ReferenceValues() { + ComparisonValues values{}; + const std::array uz = { + -1.0e-30, -2.761905780e-4, -1.066667140e-3, -2.314286540e-3, + -3.961906300e-3, -5.952383390e-3, -8.228574880e-3, -1.073333810e-2, + -1.340952890e-2, -1.620000600e-2, -1.904762720e-2}; + const std::array ury = { + 1.0e-29, 5.428573350e-4, 1.028571860e-3, 1.457143460e-3, + 1.828572130e-3, 2.142857990e-3, 2.400001050e-3, 2.600000940e-3, + 2.742858140e-3, 2.828572640e-3, 2.857143990e-3}; + std::array, kNodeCount> stations{}; + for (std::size_t node = 0U; node < kNodeCount; ++node) { + values.displacement[node][2U] = uz[node]; + values.displacement[node][4U] = ury[node]; + stations[node][2U] = node < kElementCount + ? 1.0e7 - 1.0e6 * static_cast(node) + : -1.56e-2; + } + values.reaction[0U][2U] = 1.0e6; + values.reaction[0U][4U] = -1.0e7; + for (std::size_t element = 0U; element < kElementCount; ++element) { + values.section_resultants[element][0U] = stations[element]; + values.section_resultants[element][1U] = stations[element + 1U]; + } + return values; } -fesa::ModelDefinition makeDefinition( - const std::filesystem::path& input, - std::string sourceContentIdentity) { - fesa::ModelDefinition definition{}; - definition.source_path = input; - definition.source_content_identity = std::move(sourceContentIdentity); - for (std::size_t node = 0U; node < kNodeCount; ++node) { - const auto label = static_cast(node + 1U); - definition.nodes.push_back({ - {kInstanceName, label, std::to_string(label)}, - {static_cast(node), 0.0, 0.0}, - {input, node + 1U}}); - } - definition.materials.push_back( - {"Material-1", 2.1e11, 0.3, {input, 20U}}); - definition.sections.push_back({ - "Section-1", - 1.0, - 0.0833333, - 0.0, - 0.0833333, - 0.140833, - {0.0, 1.0, 0.0}, - {}, - {input, 30U}}); - for (std::size_t element = 0U; element < kElementCount; ++element) { - const auto label = static_cast(element + 1U); - definition.elements.push_back({ - {kInstanceName, label, std::to_string(label)}, - {static_cast(element), - static_cast(element + 1U)}, - 0U, - 0U, - {input, 40U + element}}); - } - definition.steps.push_back( - {"Step-1", {}, {}, 1.0, 1.0, 1.0e-5, 1.0, {input, 60U}}); - return definition; +fesa::ModelDefinition MakeDefinition(const std::filesystem::path& input, + std::string source_content_identity) { + fesa::ModelDefinition definition{}; + definition.source_path = input; + definition.source_content_identity = std::move(source_content_identity); + for (std::size_t node = 0U; node < kNodeCount; ++node) { + const auto label = static_cast(node + 1U); + definition.nodes.push_back({{kInstanceName, label, std::to_string(label)}, + {static_cast(node), 0.0, 0.0}, + {input, node + 1U}}); + } + definition.materials.push_back({"Material-1", 2.1e11, 0.3, {input, 20U}}); + definition.sections.push_back({"Section-1", + 1.0, + 0.0833333, + 0.0, + 0.0833333, + 0.140833, + {0.0, 1.0, 0.0}, + {}, + {input, 30U}}); + for (std::size_t element = 0U; element < kElementCount; ++element) { + const auto label = static_cast(element + 1U); + definition.elements.push_back( + {{kInstanceName, label, std::to_string(label)}, + {static_cast(element), + static_cast(element + 1U)}, + 0U, + 0U, + {input, 40U + element}}); + } + definition.steps.push_back( + {"Step-1", {}, {}, 1.0, 1.0, 1.0e-5, 1.0, {input, 60U}}); + return definition; } -void writeResultsFixture( - const std::filesystem::path& output, - const std::filesystem::path& input, - const ComparisonValues& values) { - auto parsedInput = fesa::AbaqusInputReader{}.read(input); - if (!parsedInput.HasValue()) { - throw std::runtime_error{"Reference fixture input identity read failed."}; - } - auto domainResult = fesa::Domain::Create( - makeDefinition(input, parsedInput.Value().sourceContentIdentity)); - if (!domainResult.HasValue()) { - throw std::runtime_error{"Reference fixture Domain construction failed."}; - } - fesa::Domain domain = std::move(domainResult.Value()); - auto modelResult = fesa::AnalysisModel::Create(domain); - if (!modelResult.HasValue()) { - throw std::runtime_error{"Reference fixture AnalysisModel construction failed."}; - } - fesa::AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = fesa::DofManager::Create(model); - if (!dofsResult.HasValue()) { - throw std::runtime_error{"Reference fixture DofManager construction failed."}; - } - fesa::DofManager dofs = std::move(dofsResult.Value()); - fesa::AnalysisState state = - fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); +void WriteResultsFixture(const std::filesystem::path& output, + const std::filesystem::path& input, + const ComparisonValues& values) { + auto parsed_input = fesa::AbaqusInputReader{}.Read(input); + if (!parsed_input.HasValue()) { + throw std::runtime_error{"Reference fixture input identity read failed."}; + } + auto domain_result = fesa::Domain::Create( + MakeDefinition(input, parsed_input.Value().source_content_identity)); + if (!domain_result.HasValue()) { + throw std::runtime_error{"Reference fixture Domain construction failed."}; + } + fesa::Domain domain = std::move(domain_result.Value()); + auto model_result = fesa::AnalysisModel::Create(domain); + if (!model_result.HasValue()) { + throw std::runtime_error{ + "Reference fixture AnalysisModel construction failed."}; + } + fesa::AnalysisModel model = std::move(model_result.Value()); + auto dofs_result = fesa::DofManager::Create(model); + if (!dofs_result.HasValue()) { + throw std::runtime_error{ + "Reference fixture DofManager construction failed."}; + } + fesa::DofManager dofs = std::move(dofs_result.Value()); + fesa::AnalysisState state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); - for (std::size_t node = 0U; node < kNodeCount; ++node) { - for (std::size_t component = 0U; component < 6U; ++component) { - const std::size_t index = node * 6U + component; - state.Displacement()[index] = values.displacement[node][component]; - state.Reaction()[index] = values.reaction[node][component]; - state.Residual()[index] = values.reaction[node][component]; - } + for (std::size_t node = 0U; node < kNodeCount; ++node) { + for (std::size_t component = 0U; component < 6U; ++component) { + const std::size_t index = node * 6U + component; + state.Displacement()[index] = values.displacement[node][component]; + state.Reaction()[index] = values.reaction[node][component]; + state.Residual()[index] = values.reaction[node][component]; } - for (std::size_t element = 0U; element < kElementCount; ++element) { - for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { - const std::size_t node = element + endpoint; - state.EndpointResults().push_back({ - static_cast(element), - static_cast(endpoint), - domain.Nodes()[node].source_id, - {}, - values.sectionResultants[element][endpoint]}); - } - state.GaussResults().push_back( - {static_cast(element), 1, {}, {}}); - state.GaussResults().push_back( - {static_cast(element), 2, {}, {}}); - state.StressResults().push_back({ - static_cast(element), - 1, - 0U, - 0.0, - 0.0, - 0.0, - "fesa-default"}); - state.StressResults().push_back({ - static_cast(element), - 2, - 0U, - 0.0, - 0.0, - 0.0, - "fesa-default"}); + } + for (std::size_t element = 0U; element < kElementCount; ++element) { + for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { + const std::size_t node = element + endpoint; + state.EndpointResults().push_back( + {static_cast(element), + static_cast(endpoint), + domain.Nodes()[node].source_id, + {}, + values.section_resultants[element][endpoint]}); } + state.GaussResults().push_back( + {static_cast(element), 1, {}, {}}); + state.GaussResults().push_back( + {static_cast(element), 2, {}, {}}); + state.StressResults().push_back({static_cast(element), 1, + 0U, 0.0, 0.0, 0.0, "fesa-default"}); + state.StressResults().push_back({static_cast(element), 2, + 0U, 0.0, 0.0, 0.0, "fesa-default"}); + } - fesa::Hdf5ResultsWriter writer; - const fesa::Status status = writer.Write(output, domain, state, {}); - if (!status.IsOk()) { - throw std::runtime_error{"Reference fixture HDF5 write failed."}; - } + fesa::Hdf5ResultsWriter writer; + const fesa::Status status = writer.Write(output, domain, state, {}); + if (!status.IsOk()) { + throw std::runtime_error{"Reference fixture HDF5 write failed."}; + } } class ContractFixture { -public: - ContractFixture(std::string label, const ComparisonValues& values) { - static std::atomic sequence{0U}; - root_ = binaryRoot() / "reference" / "contract-fixtures" / - (std::move(label) + "-" + - std::to_string(sequence.fetch_add(1U))); - legacy_ = root_ / "cantilever beam"; - std::error_code error; - std::filesystem::remove_all(root_, error); - error.clear(); - if (!std::filesystem::create_directories(legacy_, error) || error) { - throw std::runtime_error{"Unable to create contract fixture directory."}; - } - const auto approved = sourceRoot() / "reference" / "cantilever beam"; - for (const char* name : - {kInputName, kDisplacementName, kReactionName, kSectionName}) { - std::filesystem::copy_file( - approved / name, - legacy_ / name, - std::filesystem::copy_options::overwrite_existing); - } - results_ = root_ / "results.h5"; - writeResultsFixture(results_, legacy_ / kInputName, values); + public: + ContractFixture(std::string label, const ComparisonValues& values) { + static std::atomic sequence{0U}; + root_ = BinaryRoot() / "reference" / "contract-fixtures" / + (std::move(label) + "-" + std::to_string(sequence.fetch_add(1U))); + legacy_ = root_ / "cantilever beam"; + std::error_code error; + std::filesystem::remove_all(root_, error); + error.clear(); + if (!std::filesystem::create_directories(legacy_, error) || error) { + throw std::runtime_error{"Unable to create contract fixture directory."}; } - - ContractFixture(const ContractFixture&) = delete; - ContractFixture& operator=(const ContractFixture&) = delete; - - ~ContractFixture() { - std::error_code ignored; - std::filesystem::remove_all(root_, ignored); + const auto approved = SourceRoot() / "reference" / "cantilever beam"; + for (const char* name : + {kInputName, kDisplacementName, kReactionName, kSectionName}) { + std::filesystem::copy_file( + approved / name, legacy_ / name, + std::filesystem::copy_options::overwrite_existing); } + results_ = root_ / "results.h5"; + WriteResultsFixture(results_, legacy_ / kInputName, values); + } - const std::filesystem::path& root() const noexcept { return root_; } - const std::filesystem::path& legacy() const noexcept { return legacy_; } - const std::filesystem::path& results() const noexcept { return results_; } + ContractFixture(const ContractFixture&) = delete; + ContractFixture& operator=(const ContractFixture&) = delete; -private: - std::filesystem::path root_; - std::filesystem::path legacy_; - std::filesystem::path results_; + ~ContractFixture() { + std::error_code ignored; + std::filesystem::remove_all(root_, ignored); + } + + const std::filesystem::path& Root() const noexcept { return root_; } + const std::filesystem::path& Legacy() const noexcept { return legacy_; } + const std::filesystem::path& Results() const noexcept { return results_; } + + private: + std::filesystem::path root_; + std::filesystem::path legacy_; + std::filesystem::path results_; }; -void expectFailureCode( - const fesa::Result& result, - const std::string& expectedCode) { - ASSERT_FALSE(result.HasValue()); - ASSERT_FALSE(result.GetStatus().IsOk()); - ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); - EXPECT_EQ(result.GetStatus().Diagnostics().front().code, expectedCode); +void ExpectFailureCode(const fesa::Result& result, + const std::string& expected_code) { + ASSERT_FALSE(result.HasValue()); + ASSERT_FALSE(result.GetStatus().IsOk()); + ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); + EXPECT_EQ(result.GetStatus().Diagnostics().front().code, expected_code); } -const fesa::test::RowDecision* findRow( +const fesa::test::RowDecision* FindRow( const fesa::test::ComparisonReport& report, const fesa::test::ComparisonQuantity quantity, - const std::int64_t sourceNodeLabel, - const std::string& component) { - const auto found = std::find_if( - report.rows.begin(), - report.rows.end(), - [&](const fesa::test::RowDecision& row) { - return row.reference.quantity == quantity && - row.reference.sourceNodeLabel == sourceNodeLabel && - row.reference.component == component; - }); - return found == report.rows.end() ? nullptr : &*found; + const std::int64_t source_node_label, const std::string& component) { + const auto found = std::find_if(report.rows.begin(), report.rows.end(), + [&](const fesa::test::RowDecision& row) { + return row.reference.quantity == quantity && + row.reference.source_node_label == + source_node_label && + row.reference.component == component; + }); + return found == report.rows.end() ? nullptr : &*found; } -const fesa::test::ComponentMetrics* findMetric( +const fesa::test::ComponentMetrics* FindMetric( const fesa::test::ComparisonReport& report, const fesa::test::ComparisonQuantity quantity, const std::string& component) { - const auto found = std::find_if( - report.metrics.begin(), - report.metrics.end(), - [&](const fesa::test::ComponentMetrics& metric) { - return metric.quantity == quantity && metric.component == component; - }); - return found == report.metrics.end() ? nullptr : &*found; + const auto found = std::find_if( + report.metrics.begin(), report.metrics.end(), + [&](const fesa::test::ComponentMetrics& metric) { + return metric.quantity == quantity && metric.component == component; + }); + return found == report.metrics.end() ? nullptr : &*found; } -void expectExactRowInventory(const fesa::test::ComparisonReport& report) { - ASSERT_EQ(report.rows.size(), kExpectedRowCount); - std::size_t rowIndex = 0U; - const auto expectRow = [&](const fesa::test::ComparisonQuantity quantity, - const std::size_t node, - const std::string& component, - const std::string& unit, - const std::string& coordinateSystem, - const std::string& datasetPath) { - ASSERT_LT(rowIndex, report.rows.size()); - const auto& row = report.rows[rowIndex++]; - for (const auto* side : {&row.fesa, &row.reference}) { - EXPECT_EQ(side->modelId, "cantilever-beam-b33"); - EXPECT_EQ(side->stepName, "Step-1"); - EXPECT_EQ(side->frameIndex, 0U); - EXPECT_EQ(side->instanceName, kInstanceName); - EXPECT_EQ( - side->sourceNodeLabel, - static_cast(node + 1U)); - EXPECT_EQ(side->quantity, quantity); - EXPECT_EQ(side->component, component); - EXPECT_EQ(side->unitDimension, unit); - EXPECT_EQ(side->coordinateSystem, coordinateSystem); - EXPECT_EQ(side->hdf5DatasetPath, datasetPath); - } - }; - - const std::array displacementComponents = { - "UX", "UY", "UZ", "URX", "URY", "URZ"}; - const std::array displacementUnits = { - "length", "length", "length", "radian", "radian", "radian"}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - for (std::size_t component = 0U; - component < displacementComponents.size(); - ++component) { - expectRow( - fesa::test::ComparisonQuantity::displacement, - node, - displacementComponents[component], - displacementUnits[component], - "global-cartesian", - "/steps/Step-1/frames/0/nodal/displacement"); - } +void ExpectExactRowInventory(const fesa::test::ComparisonReport& report) { + ASSERT_EQ(report.rows.size(), kExpectedRowCount); + std::size_t row_index = 0U; + const auto expect_row = [&](const fesa::test::ComparisonQuantity quantity, + const std::size_t node, + const std::string& component, + const std::string& unit, + const std::string& coordinate_system, + const std::string& dataset_path) { + ASSERT_LT(row_index, report.rows.size()); + const auto& row = report.rows[row_index++]; + for (const auto* side : {&row.fesa, &row.reference}) { + EXPECT_EQ(side->model_id, "cantilever-beam-b33"); + EXPECT_EQ(side->step_name, "Step-1"); + EXPECT_EQ(side->frame_index, 0U); + EXPECT_EQ(side->instance_name, kInstanceName); + EXPECT_EQ(side->source_node_label, static_cast(node + 1U)); + EXPECT_EQ(side->quantity, quantity); + EXPECT_EQ(side->component, component); + EXPECT_EQ(side->unit_dimension, unit); + EXPECT_EQ(side->coordinate_system, coordinate_system); + EXPECT_EQ(side->hdf5_dataset_path, dataset_path); } + }; - const std::array reactionComponents = { - "RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}; - const std::array reactionUnits = { - "force", - "force", - "force", - "force*length", - "force*length", - "force*length"}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - for (std::size_t component = 0U; - component < reactionComponents.size(); - ++component) { - expectRow( - fesa::test::ComparisonQuantity::reaction, - node, - reactionComponents[component], - reactionUnits[component], - "global-cartesian", - "/steps/Step-1/frames/0/nodal/reaction"); - } + const std::array displacement_components = { + "UX", "UY", "UZ", "URX", "URY", "URZ"}; + const std::array displacement_units = { + "length", "length", "length", "radian", "radian", "radian"}; + for (std::size_t node = 0U; node < kNodeCount; ++node) { + for (std::size_t component = 0U; component < displacement_components.size(); + ++component) { + expect_row(fesa::test::ComparisonQuantity::kDisplacement, node, + displacement_components[component], + displacement_units[component], "global-cartesian", + "/steps/Step-1/frames/0/nodal/displacement"); } + } - const std::array sectionComponents = { - "N", "T", "My", "Mz"}; - const std::array sectionUnits = { - "force", "force*length", "force*length", "force*length"}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - for (std::size_t component = 0U; - component < sectionComponents.size(); - ++component) { - expectRow( - fesa::test::ComparisonQuantity::sectionResultant, - node, - sectionComponents[component], - sectionUnits[component], - "beam-local", - "/steps/Step-1/frames/0/element/section_resultant"); - } + const std::array reaction_components = {"RF1", "RF2", "RF3", + "RM1", "RM2", "RM3"}; + const std::array reaction_units = { + "force", "force", "force", + "force*length", "force*length", "force*length"}; + for (std::size_t node = 0U; node < kNodeCount; ++node) { + for (std::size_t component = 0U; component < reaction_components.size(); + ++component) { + expect_row(fesa::test::ComparisonQuantity::kReaction, node, + reaction_components[component], reaction_units[component], + "global-cartesian", "/steps/Step-1/frames/0/nodal/reaction"); } - EXPECT_EQ(rowIndex, report.rows.size()); + } + + const std::array section_components = {"N", "T", "My", "Mz"}; + const std::array section_units = { + "force", "force*length", "force*length", "force*length"}; + for (std::size_t node = 0U; node < kNodeCount; ++node) { + for (std::size_t component = 0U; component < section_components.size(); + ++component) { + expect_row(fesa::test::ComparisonQuantity::kSectionResultant, node, + section_components[component], section_units[component], + "beam-local", + "/steps/Step-1/frames/0/element/section_resultant"); + } + } + EXPECT_EQ(row_index, report.rows.size()); } -} // namespace +} // namespace TEST(ReferenceComparisonContract, PrecheckRejectsMissingSchemaDuplicateAndNonfiniteRows) { - auto mismatchedValues = referenceValues(); - mismatchedValues.displacement[0U][0U] = 1.0; + auto mismatched_values = ReferenceValues(); + mismatched_values.displacement[0U][0U] = 1.0; - { - ContractFixture fixture{"missing-file", mismatchedValues}; - ASSERT_TRUE(std::filesystem::remove( - fixture.legacy() / kDisplacementName)); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "needs-reference-artifacts"); - } - { - ContractFixture fixture{"b31", mismatchedValues}; - auto input = readBytes(fixture.legacy() / kInputName); - replaceFirst(input, "type=B33", "type=B31"); - writeBytes(fixture.legacy() / kInputName, input); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "needs-reference-artifacts"); - } - { - ContractFixture fixture{"header", mismatchedValues}; - auto csv = readBytes(fixture.legacy() / kDisplacementName); - replaceFirst(csv, "U-U1", "U1"); - writeBytes(fixture.legacy() / kDisplacementName, csv); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "schema-mismatch"); - } - { - ContractFixture fixture{"missing-row", mismatchedValues}; - auto lines = readLines(fixture.legacy() / kDisplacementName); - ASSERT_EQ(lines.size(), kNodeCount + 1U); - lines.pop_back(); - writeLines(fixture.legacy() / kDisplacementName, lines); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "schema-mismatch"); - } - { - ContractFixture fixture{"extra-row", mismatchedValues}; - auto lines = readLines(fixture.legacy() / kDisplacementName); - ASSERT_EQ(lines.size(), kNodeCount + 1U); - std::string extra = lines.back(); - replaceFirst(extra, ",PART-1_1-1,11,", ",PART-1_1-1,12,"); - lines.push_back(std::move(extra)); - writeLines(fixture.legacy() / kDisplacementName, lines); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "schema-mismatch"); - } - { - ContractFixture fixture{"duplicate-row", mismatchedValues}; - auto lines = readLines(fixture.legacy() / kReactionName); - ASSERT_EQ(lines.size(), kNodeCount + 1U); - lines.push_back(lines[1U]); - writeLines(fixture.legacy() / kReactionName, lines); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "schema-mismatch"); - } - { - ContractFixture fixture{"nonfinite-row", mismatchedValues}; - auto csv = readBytes(fixture.legacy() / kReactionName); - replaceFirst(csv, "0.000000000E+00", "NaN"); - writeBytes(fixture.legacy() / kReactionName, csv); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "schema-mismatch"); - } - { - ContractFixture fixture{"identity", mismatchedValues}; - auto csv = readBytes(fixture.legacy() / kSectionName); - replaceFirst(csv, "PART-1_1-1", "WRONG-INSTANCE"); - writeBytes(fixture.legacy() / kSectionName, csv); - expectFailureCode( - fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()), - "schema-mismatch"); - } + { + ContractFixture fixture{"missing-file", mismatched_values}; + ASSERT_TRUE(std::filesystem::remove(fixture.Legacy() / kDisplacementName)); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "needs-reference-artifacts"); + } + { + ContractFixture fixture{"b31", mismatched_values}; + auto input = ReadBytes(fixture.Legacy() / kInputName); + ReplaceFirst(input, "type=B33", "type=B31"); + WriteBytes(fixture.Legacy() / kInputName, input); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "needs-reference-artifacts"); + } + { + ContractFixture fixture{"header", mismatched_values}; + auto csv = ReadBytes(fixture.Legacy() / kDisplacementName); + ReplaceFirst(csv, "U-U1", "U1"); + WriteBytes(fixture.Legacy() / kDisplacementName, csv); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "schema-mismatch"); + } + { + ContractFixture fixture{"missing-row", mismatched_values}; + auto lines = ReadLines(fixture.Legacy() / kDisplacementName); + ASSERT_EQ(lines.size(), kNodeCount + 1U); + lines.pop_back(); + WriteLines(fixture.Legacy() / kDisplacementName, lines); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "schema-mismatch"); + } + { + ContractFixture fixture{"extra-row", mismatched_values}; + auto lines = ReadLines(fixture.Legacy() / kDisplacementName); + ASSERT_EQ(lines.size(), kNodeCount + 1U); + std::string extra = lines.back(); + ReplaceFirst(extra, ",PART-1_1-1,11,", ",PART-1_1-1,12,"); + lines.push_back(std::move(extra)); + WriteLines(fixture.Legacy() / kDisplacementName, lines); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "schema-mismatch"); + } + { + ContractFixture fixture{"duplicate-row", mismatched_values}; + auto lines = ReadLines(fixture.Legacy() / kReactionName); + ASSERT_EQ(lines.size(), kNodeCount + 1U); + lines.push_back(lines[1U]); + WriteLines(fixture.Legacy() / kReactionName, lines); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "schema-mismatch"); + } + { + ContractFixture fixture{"nonfinite-row", mismatched_values}; + auto csv = ReadBytes(fixture.Legacy() / kReactionName); + ReplaceFirst(csv, "0.000000000E+00", "NaN"); + WriteBytes(fixture.Legacy() / kReactionName, csv); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "schema-mismatch"); + } + { + ContractFixture fixture{"identity", mismatched_values}; + auto csv = ReadBytes(fixture.Legacy() / kSectionName); + ReplaceFirst(csv, "PART-1_1-1", "WRONG-INSTANCE"); + WriteBytes(fixture.Legacy() / kSectionName, csv); + ExpectFailureCode(fesa::test::ReferenceComparison::Compare( + fixture.Results(), fixture.Legacy()), + "schema-mismatch"); + } } TEST(ReferenceComparisonContract, AppliesAbaqusOnlyComponentScaleWithoutClampOrDrop) { - auto values = referenceValues(); - values.displacement[1U][0U] = 0.999e-9; - values.displacement[2U][0U] = 1.001e-9; - values.sectionResultants[0U][0U][2U] = 1.0e7 + 9.0; - values.sectionResultants[9U][1U][2U] = 0.0; - ContractFixture fixture{"tolerance", values}; + auto values = ReferenceValues(); + values.displacement[1U][0U] = 0.999e-9; + values.displacement[2U][0U] = 1.001e-9; + values.section_resultants[0U][0U][2U] = 1.0e7 + 9.0; + values.section_resultants[9U][1U][2U] = 0.0; + ContractFixture fixture{"tolerance", values}; - auto result = fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()); - ASSERT_TRUE(result.HasValue()); - const auto& report = result.Value(); - EXPECT_FALSE(report.passed); - EXPECT_EQ(report.rows.size(), kExpectedRowCount); - EXPECT_EQ(report.metrics.size(), kExpectedMetricCount); + auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(), + fixture.Legacy()); + ASSERT_TRUE(result.HasValue()); + const auto& report = result.Value(); + EXPECT_FALSE(report.passed); + EXPECT_EQ(report.rows.size(), kExpectedRowCount); + EXPECT_EQ(report.metrics.size(), kExpectedMetricCount); - const auto* zero = findRow( - report, fesa::test::ComparisonQuantity::displacement, 1, "UX"); - const auto* nearZero = findRow( - report, fesa::test::ComparisonQuantity::displacement, 2, "UX"); - const auto* deliberateFailure = findRow( - report, fesa::test::ComparisonQuantity::displacement, 3, "UX"); - ASSERT_NE(zero, nullptr); - ASSERT_NE(nearZero, nullptr); - ASSERT_NE(deliberateFailure, nullptr); - EXPECT_DOUBLE_EQ(zero->reference.value, 0.0); - EXPECT_DOUBLE_EQ(zero->fesa.value, 0.0); - EXPECT_DOUBLE_EQ(zero->tolerance, 1.0e-9); - EXPECT_TRUE(zero->passed); - EXPECT_TRUE(nearZero->passed); - EXPECT_FALSE(deliberateFailure->passed); + const auto* zero = + FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 1, "UX"); + const auto* near_zero = + FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 2, "UX"); + const auto* deliberate_failure = + FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 3, "UX"); + ASSERT_NE(zero, nullptr); + ASSERT_NE(near_zero, nullptr); + ASSERT_NE(deliberate_failure, nullptr); + EXPECT_DOUBLE_EQ(zero->reference.value, 0.0); + EXPECT_DOUBLE_EQ(zero->fesa.value, 0.0); + EXPECT_DOUBLE_EQ(zero->tolerance, 1.0e-9); + EXPECT_TRUE(zero->passed); + EXPECT_TRUE(near_zero->passed); + EXPECT_FALSE(deliberate_failure->passed); - const auto* myMetric = findMetric( - report, fesa::test::ComparisonQuantity::sectionResultant, "My"); - ASSERT_NE(myMetric, nullptr); - EXPECT_DOUBLE_EQ(myMetric->referenceScale, 1.0e7); - const auto* scaled = findRow( - report, fesa::test::ComparisonQuantity::sectionResultant, 1, "My"); - const auto* residue = findRow( - report, fesa::test::ComparisonQuantity::sectionResultant, 11, "My"); - ASSERT_NE(scaled, nullptr); - ASSERT_NE(residue, nullptr); - EXPECT_DOUBLE_EQ(scaled->tolerance, 10.001); - EXPECT_DOUBLE_EQ(scaled->absoluteError, 9.0); - EXPECT_TRUE(scaled->passed); - EXPECT_DOUBLE_EQ(residue->reference.value, -1.56e-2); - EXPECT_DOUBLE_EQ(residue->fesa.value, 0.0); - EXPECT_DOUBLE_EQ(residue->absoluteError, 1.56e-2); - EXPECT_DOUBLE_EQ(residue->tolerance, 10.001); - EXPECT_TRUE(residue->passed); + const auto* my_metric = FindMetric( + report, fesa::test::ComparisonQuantity::kSectionResultant, "My"); + ASSERT_NE(my_metric, nullptr); + EXPECT_DOUBLE_EQ(my_metric->reference_scale, 1.0e7); + const auto* scaled = FindRow( + report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "My"); + const auto* residue = FindRow( + report, fesa::test::ComparisonQuantity::kSectionResultant, 11, "My"); + ASSERT_NE(scaled, nullptr); + ASSERT_NE(residue, nullptr); + EXPECT_DOUBLE_EQ(scaled->tolerance, 10.001); + EXPECT_DOUBLE_EQ(scaled->absolute_error, 9.0); + EXPECT_TRUE(scaled->passed); + EXPECT_DOUBLE_EQ(residue->reference.value, -1.56e-2); + EXPECT_DOUBLE_EQ(residue->fesa.value, 0.0); + EXPECT_DOUBLE_EQ(residue->absolute_error, 1.56e-2); + EXPECT_DOUBLE_EQ(residue->tolerance, 10.001); + EXPECT_TRUE(residue->passed); } TEST(ReferenceComparisonContract, ReportsEveryRowAndAggregateMetricDeterministically) { - auto values = referenceValues(); - values.displacement[0U][0U] = 0.5e-9; - values.displacement[1U][0U] = -1.0e-9; - ContractFixture fixture{"metrics", values}; + auto values = ReferenceValues(); + values.displacement[0U][0U] = 0.5e-9; + values.displacement[1U][0U] = -1.0e-9; + ContractFixture fixture{"metrics", values}; - auto result = fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()); - ASSERT_TRUE(result.HasValue()); - const auto& report = result.Value(); - ASSERT_TRUE(report.passed); - expectExactRowInventory(report); - ASSERT_EQ(report.metrics.size(), kExpectedMetricCount); - EXPECT_TRUE(std::all_of( - report.rows.begin(), - report.rows.end(), - [](const fesa::test::RowDecision& row) { return row.passed; })); + auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(), + fixture.Legacy()); + ASSERT_TRUE(result.HasValue()); + const auto& report = result.Value(); + ASSERT_TRUE(report.passed); + ExpectExactRowInventory(report); + ASSERT_EQ(report.metrics.size(), kExpectedMetricCount); + EXPECT_TRUE(std::all_of( + report.rows.begin(), report.rows.end(), + [](const fesa::test::RowDecision& row) { return row.passed; })); - const auto* metric = findMetric( - report, fesa::test::ComparisonQuantity::displacement, "UX"); - const auto* worst = findRow( - report, fesa::test::ComparisonQuantity::displacement, 2, "UX"); - ASSERT_NE(metric, nullptr); - ASSERT_NE(worst, nullptr); - EXPECT_DOUBLE_EQ(metric->referenceScale, 0.0); - EXPECT_DOUBLE_EQ(metric->maximumAbsoluteError, 1.0e-9); - EXPECT_DOUBLE_EQ(metric->maximumNormalizedError, 1.0); - EXPECT_NEAR( - metric->rmsError, - std::sqrt(1.25 / static_cast(kNodeCount)) * 1.0e-9, - 1.0e-21); - EXPECT_NEAR(metric->normError, std::sqrt(1.25) * 1.0e-9, 1.0e-21); - EXPECT_EQ( - metric->worstRow, - static_cast(worst - report.rows.data())); + const auto* metric = + FindMetric(report, fesa::test::ComparisonQuantity::kDisplacement, "UX"); + const auto* worst = + FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 2, "UX"); + ASSERT_NE(metric, nullptr); + ASSERT_NE(worst, nullptr); + EXPECT_DOUBLE_EQ(metric->reference_scale, 0.0); + EXPECT_DOUBLE_EQ(metric->maximum_absolute_error, 1.0e-9); + EXPECT_DOUBLE_EQ(metric->maximum_normalized_error, 1.0); + EXPECT_NEAR(metric->rms_error, + std::sqrt(1.25 / static_cast(kNodeCount)) * 1.0e-9, + 1.0e-21); + EXPECT_NEAR(metric->norm_error, std::sqrt(1.25) * 1.0e-9, 1.0e-21); + EXPECT_EQ(metric->worst_row, + static_cast(worst - report.rows.data())); - EXPECT_FALSE(report.stressComparisonApplicable); - EXPECT_NE(report.stressComparisonReason.find("N/A"), std::string::npos); - EXPECT_NE(report.stressComparisonReason.find("HDF5"), std::string::npos); - EXPECT_DOUBLE_EQ(report.physicsEvidence.freeResidualNorm, 0.0); - EXPECT_EQ( - report.physicsEvidence.appliedForce, - (std::array{0.0, 0.0, -1.0e6})); - EXPECT_EQ( - report.physicsEvidence.reactionForce, - (std::array{0.0, 0.0, 1.0e6})); - EXPECT_EQ( - report.physicsEvidence.appliedMomentAboutOrigin, - (std::array{0.0, 1.0e7, 0.0})); - EXPECT_EQ( - report.physicsEvidence.reactionMomentAboutOrigin, - (std::array{0.0, -1.0e7, 0.0})); - EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed); + EXPECT_FALSE(report.stress_comparison_applicable); + EXPECT_NE(report.stress_comparison_reason.find("N/A"), std::string::npos); + EXPECT_NE(report.stress_comparison_reason.find("HDF5"), std::string::npos); + EXPECT_DOUBLE_EQ(report.physics_evidence.free_residual_norm, 0.0); + EXPECT_EQ(report.physics_evidence.applied_force, + (std::array{0.0, 0.0, -1.0e6})); + EXPECT_EQ(report.physics_evidence.reaction_force, + (std::array{0.0, 0.0, 1.0e6})); + EXPECT_EQ(report.physics_evidence.applied_moment_about_origin, + (std::array{0.0, 1.0e7, 0.0})); + EXPECT_EQ(report.physics_evidence.reaction_moment_about_origin, + (std::array{0.0, -1.0e7, 0.0})); + EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed); - const auto jsonA = fixture.root() / "comparison-a.json"; - const auto jsonB = fixture.root() / "comparison-b.json"; - ASSERT_TRUE( - fesa::test::ReferenceComparison::writeDeterministicJson(report, jsonA) - .IsOk()); - ASSERT_TRUE( - fesa::test::ReferenceComparison::writeDeterministicJson(report, jsonB) - .IsOk()); - const std::string first = readBytes(jsonA); - EXPECT_EQ(first, readBytes(jsonB)); - for (const char* required : { - "\"rows\"", - "\"metrics\"", - "\"stress_comparison_applicable\":false", - "\"stress_comparison_reason\"", - "\"physics_evidence\"", - "\"free_residual_norm\"", - "\"applied_force\"", - "\"reaction_force\"", - "\"applied_moment_about_origin\"", - "\"reaction_moment_about_origin\"", - "\"endpoint_consistency_passed\""}) { - EXPECT_NE(first.find(required), std::string::npos) << required; - } + const auto json_a = fixture.Root() / "comparison-a.json"; + const auto json_b = fixture.Root() / "comparison-b.json"; + ASSERT_TRUE( + fesa::test::ReferenceComparison::WriteDeterministicJson(report, json_a) + .IsOk()); + ASSERT_TRUE( + fesa::test::ReferenceComparison::WriteDeterministicJson(report, json_b) + .IsOk()); + const std::string first = ReadBytes(json_a); + EXPECT_EQ(first, ReadBytes(json_b)); + for (const char* required : + {"\"rows\"", "\"metrics\"", "\"stress_comparison_applicable\":false", + "\"stress_comparison_reason\"", "\"physics_evidence\"", + "\"free_residual_norm\"", "\"applied_force\"", "\"reaction_force\"", + "\"applied_moment_about_origin\"", "\"reaction_moment_about_origin\"", + "\"endpoint_consistency_passed\""}) { + EXPECT_NE(first.find(required), std::string::npos) << required; + } } -TEST(ReferenceComparisonContract, - NormalizesEligibleStationsWithoutAveraging) { - auto values = referenceValues(); - values.sectionResultants[0U][0U] = {2.0e-4, 3.0e-4, 1.0e7, 4.0e-4}; - values.sectionResultants[0U][1U][2U] = 9.0e6 - 5.0; - values.sectionResultants[1U][0U][2U] = 9.0e6 + 5.0; - ContractFixture fixture{"stations", values}; +TEST(ReferenceComparisonContract, NormalizesEligibleStationsWithoutAveraging) { + auto values = ReferenceValues(); + values.section_resultants[0U][0U] = {2.0e-4, 3.0e-4, 1.0e7, 4.0e-4}; + values.section_resultants[0U][1U][2U] = 9.0e6 - 5.0; + values.section_resultants[1U][0U][2U] = 9.0e6 + 5.0; + ContractFixture fixture{"stations", values}; - auto result = fesa::test::ReferenceComparison::compare( - fixture.results(), fixture.legacy()); - ASSERT_TRUE(result.HasValue()); - const auto& report = result.Value(); - ASSERT_TRUE(report.passed); - EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed); + auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(), + fixture.Legacy()); + ASSERT_TRUE(result.HasValue()); + const auto& report = result.Value(); + ASSERT_TRUE(report.passed); + EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed); - const auto* n = findRow( - report, fesa::test::ComparisonQuantity::sectionResultant, 1, "N"); - const auto* t = findRow( - report, fesa::test::ComparisonQuantity::sectionResultant, 1, "T"); - const auto* my = findRow( - report, fesa::test::ComparisonQuantity::sectionResultant, 1, "My"); - const auto* mz = findRow( - report, fesa::test::ComparisonQuantity::sectionResultant, 1, "Mz"); - ASSERT_NE(n, nullptr); - ASSERT_NE(t, nullptr); - ASSERT_NE(my, nullptr); - ASSERT_NE(mz, nullptr); - EXPECT_DOUBLE_EQ(n->fesa.value, 2.0e-4); - EXPECT_DOUBLE_EQ(t->fesa.value, 3.0e-4); - EXPECT_DOUBLE_EQ(my->fesa.value, 1.0e7); - EXPECT_DOUBLE_EQ(mz->fesa.value, 4.0e-4); + const auto* n = FindRow( + report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "N"); + const auto* t = FindRow( + report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "T"); + const auto* my = FindRow( + report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "My"); + const auto* mz = FindRow( + report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "Mz"); + ASSERT_NE(n, nullptr); + ASSERT_NE(t, nullptr); + ASSERT_NE(my, nullptr); + ASSERT_NE(mz, nullptr); + EXPECT_DOUBLE_EQ(n->fesa.value, 2.0e-4); + EXPECT_DOUBLE_EQ(t->fesa.value, 3.0e-4); + EXPECT_DOUBLE_EQ(my->fesa.value, 1.0e7); + EXPECT_DOUBLE_EQ(mz->fesa.value, 4.0e-4); - const auto* interior = findRow( - report, fesa::test::ComparisonQuantity::sectionResultant, 2, "My"); - ASSERT_NE(interior, nullptr); - EXPECT_DOUBLE_EQ(interior->fesa.value, 9.0e6 - 5.0); - EXPECT_DOUBLE_EQ(interior->reference.value, 9.0e6); - EXPECT_DOUBLE_EQ(interior->absoluteError, 5.0); + const auto* interior = FindRow( + report, fesa::test::ComparisonQuantity::kSectionResultant, 2, "My"); + ASSERT_NE(interior, nullptr); + EXPECT_DOUBLE_EQ(interior->fesa.value, 9.0e6 - 5.0); + EXPECT_DOUBLE_EQ(interior->reference.value, 9.0e6); + EXPECT_DOUBLE_EQ(interior->absolute_error, 5.0); - auto mismatchValues = referenceValues(); - mismatchValues.sectionResultants[0U][1U][2U] = 9.0e6 - 6.0; - mismatchValues.sectionResultants[1U][0U][2U] = 9.0e6 + 6.0; - ContractFixture mismatch{"station-mismatch", mismatchValues}; - expectFailureCode( - fesa::test::ReferenceComparison::compare( - mismatch.results(), mismatch.legacy()), - "tolerance-failure"); + auto mismatch_values = ReferenceValues(); + mismatch_values.section_resultants[0U][1U][2U] = 9.0e6 - 6.0; + mismatch_values.section_resultants[1U][0U][2U] = 9.0e6 + 6.0; + ContractFixture mismatch{"station-mismatch", mismatch_values}; + ExpectFailureCode(fesa::test::ReferenceComparison::Compare(mismatch.Results(), + mismatch.Legacy()), + "tolerance-failure"); } diff --git a/tests/unit/io/abaqus/domain_mapper_test.cpp b/tests/unit/io/abaqus/domain_mapper_test.cpp index 9dab454..546042b 100644 --- a/tests/unit/io/abaqus/domain_mapper_test.cpp +++ b/tests/unit/io/abaqus/domain_mapper_test.cpp @@ -1,5 +1,4 @@ -#include "fesa/io/abaqus/domain_mapper.hpp" -#include "fesa/io/abaqus/input_reader.hpp" +#include "fesa/io/abaqus/domain_mapper.h" #include @@ -13,88 +12,82 @@ #include #include +#include "fesa/io/abaqus/input_reader.h" + namespace { class TemporaryInputFile { -public: - TemporaryInputFile(const std::string& stem, const std::string& content) - : path_{std::filesystem::temp_directory_path() / - ("fesa-domain-mapper-" + stem + ".inp")} { - std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; - stream.write(content.data(), static_cast(content.size())); - if (!stream) { - throw std::runtime_error{"Unable to create domain mapper fixture."}; - } + public: + TemporaryInputFile(const std::string& stem, const std::string& content) + : path_{std::filesystem::temp_directory_path() / + ("fesa-domain-mapper-" + stem + ".inp")} { + std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; + stream.write(content.data(), static_cast(content.size())); + if (!stream) { + throw std::runtime_error{"Unable to create domain mapper fixture."}; } + } - ~TemporaryInputFile() { - std::error_code error; - std::filesystem::remove(path_, error); - } + ~TemporaryInputFile() { + std::error_code error; + std::filesystem::remove(path_, error); + } - const std::filesystem::path& path() const noexcept { - return path_; - } + const std::filesystem::path& Path() const noexcept { return path_; } -private: - std::filesystem::path path_; + private: + std::filesystem::path path_; }; -fesa::Result mapText( - const std::string& stem, - const std::string& content) { - const TemporaryInputFile input{stem, content}; - auto parsed = fesa::AbaqusInputReader{}.read(input.path()); - if (!parsed.HasValue()) { - return fesa::Result::Failure(parsed.GetStatus()); - } - return fesa::AbaqusDomainMapper{}.map(parsed.Value()); +fesa::Result MapText(const std::string& stem, + const std::string& content) { + const TemporaryInputFile input{stem, content}; + auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); + if (!parsed.HasValue()) { + return fesa::Result::Failure(parsed.GetStatus()); + } + return fesa::AbaqusDomainMapper{}.Map(parsed.Value()); } -std::string readExactBytes(const std::filesystem::path& path) { - std::ifstream stream{path, std::ios::binary}; - if (!stream) { - throw std::runtime_error{"Unable to read legacy mapper fixture."}; - } - return std::string{ - std::istreambuf_iterator{stream}, - std::istreambuf_iterator{}}; +std::string ReadExactBytes(const std::filesystem::path& path) { + std::ifstream stream{path, std::ios::binary}; + if (!stream) { + throw std::runtime_error{"Unable to read legacy mapper fixture."}; + } + return std::string{std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; } -std::filesystem::path repositoryRoot() { - auto path = std::filesystem::path{__FILE__}.parent_path(); - for (int parent = 0; parent < 4; ++parent) { - path = path.parent_path(); - } - return path; +std::filesystem::path RepositoryRoot() { + auto path = std::filesystem::path{__FILE__}.parent_path(); + for (int parent = 0; parent < 4; ++parent) { + path = path.parent_path(); + } + return path; } -const fesa::Diagnostic* findDiagnostic( - const fesa::Status& status, - const std::string& code) { - const auto found = std::find_if( - status.Diagnostics().begin(), - status.Diagnostics().end(), - [&code](const fesa::Diagnostic& diagnostic) { - return diagnostic.code == code; - }); - return found == status.Diagnostics().end() ? nullptr : &*found; +const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status, + const std::string& code) { + const auto found = + std::find_if(status.Diagnostics().begin(), status.Diagnostics().end(), + [&code](const fesa::Diagnostic& diagnostic) { + return diagnostic.code == code; + }); + return found == status.Diagnostics().end() ? nullptr : &*found; } -std::string replaceOnce( - std::string text, - const std::string& from, - const std::string& to) { - const auto position = text.find(from); - if (position == std::string::npos) { - throw std::logic_error{"Mapper test mutation source was not found."}; - } - text.replace(position, from.size(), to); - return text; +std::string ReplaceOnce(std::string text, const std::string& from, + const std::string& to) { + const auto position = text.find(from); + if (position == std::string::npos) { + throw std::logic_error{"Mapper test mutation source was not found."}; + } + text.replace(position, from.size(), to); + return text; } -std::string minimalDeck() { - return R"inp(*Part, name=BeamPart +std::string MinimalDeck() { + return R"inp(*Part, name=BeamPart *Node 1, 0., 0., 0. 2, 1., 0., 0. @@ -128,8 +121,8 @@ Tip, 2, -1. )inp"; } -std::string shellDeck() { - return R"inp(*Part, name=ShellPart +std::string ShellDeck() { + return R"inp(*Part, name=ShellPart *Node 1, 0., 0., 0. 2, 1., 0., 0. @@ -177,15 +170,14 @@ TipSecond, 6, 1. )inp"; } -std::string supportedInventoryDeck(bool includeNoOps) { - const std::string preprint = includeNoOps - ? "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n" - : ""; - const std::string shear = includeNoOps - ? "*Transverse Shear Stiffness\n1., 2., 3.\n" - : ""; - const std::string outputs = includeNoOps - ? R"inp(*Restart, write, frequency=0 +std::string SupportedInventoryDeck(bool include_no_ops) { + const std::string preprint = + include_no_ops ? "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n" + : ""; + const std::string shear = + include_no_ops ? "*Transverse Shear Stiffness\n1., 2., 3.\n" : ""; + const std::string outputs = include_no_ops + ? R"inp(*Restart, write, frequency=0 *Output, field *Node Output U, RF @@ -194,11 +186,12 @@ S, SF *Contact Output, variable=PRESELECT *Output, history, variable=PRESELECT )inp" - : ""; + : ""; - return std::string{R"inp(*hEaDiNg + return std::string{R"inp(*hEaDiNg Every supported keyword -)inp"} + preprint + R"inp(*pArT, NaMe=BeamPart +)inp"} + preprint + + R"inp(*pArT, NaMe=BeamPart *nOdE 0001, 0., 0., 0. 0002, 2., 0., 0. @@ -218,7 +211,8 @@ Every supported keyword *Section Points -0.5, 0.25 0.5, -0.25 -)inp" + shear + R"inp(*eNd PaRt +)inp" + shear + + R"inp(*eNd PaRt *aSsEmBlY, name=Assembly *iNsTaNcE, NAME=Beam-1, PART=beampart *eNd InStAnCe @@ -239,86 +233,87 @@ rootassembly, 1, 1, 0.125 RootAssembly, 2, 2 *cLoAd RootAssembly, 6, -12.5 -)inp" + outputs + "*eNd StEp\n"; +)inp" + outputs + + "*eNd StEp\n"; } -} // namespace +} // namespace TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) { - auto result = mapText("supported-inventory", supportedInventoryDeck(true)); - ASSERT_TRUE(result.HasValue()); - const fesa::Domain& domain = result.Value(); + auto result = MapText("supported-inventory", SupportedInventoryDeck(true)); + ASSERT_TRUE(result.HasValue()); + const fesa::Domain& domain = result.Value(); - ASSERT_EQ(domain.Nodes().size(), 2U); - EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "Beam-1"); - EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 1); - EXPECT_EQ(domain.Nodes()[0].source_id.source_label_text, "0001"); - EXPECT_EQ(domain.Nodes()[1].source_id.source_label_text, "0002"); - EXPECT_DOUBLE_EQ(domain.Nodes()[1].coordinates[0], 2.0); + ASSERT_EQ(domain.Nodes().size(), 2U); + EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "Beam-1"); + EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 1); + EXPECT_EQ(domain.Nodes()[0].source_id.source_label_text, "0001"); + EXPECT_EQ(domain.Nodes()[1].source_id.source_label_text, "0002"); + EXPECT_DOUBLE_EQ(domain.Nodes()[1].coordinates[0], 2.0); - ASSERT_EQ(domain.Elements().size(), 1U); - EXPECT_EQ(domain.Elements()[0].source_id.source_label_text, "0007"); - EXPECT_EQ(domain.Elements()[0].node_indices[0], 0U); - EXPECT_EQ(domain.Elements()[0].node_indices[1], 1U); + ASSERT_EQ(domain.Elements().size(), 1U); + EXPECT_EQ(domain.Elements()[0].source_id.source_label_text, "0007"); + EXPECT_EQ(domain.Elements()[0].node_indices[0], 0U); + EXPECT_EQ(domain.Elements()[0].node_indices[1], 1U); - ASSERT_EQ(domain.Materials().size(), 1U); - EXPECT_EQ(domain.Materials()[0].name, "Steel"); - EXPECT_DOUBLE_EQ(domain.Materials()[0].youngs_modulus, 210000.0); - EXPECT_DOUBLE_EQ(domain.Materials()[0].poisson_ratio, 0.75); + ASSERT_EQ(domain.Materials().size(), 1U); + EXPECT_EQ(domain.Materials()[0].name, "Steel"); + EXPECT_DOUBLE_EQ(domain.Materials()[0].youngs_modulus, 210000.0); + EXPECT_DOUBLE_EQ(domain.Materials()[0].poisson_ratio, 0.75); - ASSERT_EQ(domain.Sections().size(), 1U); - const auto& section = domain.Sections()[0]; - EXPECT_DOUBLE_EQ(section.area, 2.0); - EXPECT_DOUBLE_EQ(section.i11, 3.0); - EXPECT_DOUBLE_EQ(section.i12, 0.0); - EXPECT_DOUBLE_EQ(section.i22, 4.0); - EXPECT_DOUBLE_EQ(section.torsional_constant, 5.0); - EXPECT_EQ(section.first_axis, (std::array{0.0, 1.0, 0.0})); - EXPECT_EQ( - section.section_points, - (std::vector>{{-0.5, 0.25}, {0.5, -0.25}})); - EXPECT_EQ(domain.Elements()[0].material_index, 0U); - EXPECT_EQ(domain.Elements()[0].section_index, 0U); + ASSERT_EQ(domain.Sections().size(), 1U); + const auto& section = domain.Sections()[0]; + EXPECT_DOUBLE_EQ(section.area, 2.0); + EXPECT_DOUBLE_EQ(section.i11, 3.0); + EXPECT_DOUBLE_EQ(section.i12, 0.0); + EXPECT_DOUBLE_EQ(section.i22, 4.0); + EXPECT_DOUBLE_EQ(section.torsional_constant, 5.0); + EXPECT_EQ(section.first_axis, (std::array{0.0, 1.0, 0.0})); + EXPECT_EQ(section.section_points, + (std::vector>{{-0.5, 0.25}, {0.5, -0.25}})); + EXPECT_EQ(domain.Elements()[0].material_index, 0U); + EXPECT_EQ(domain.Elements()[0].section_index, 0U); - ASSERT_EQ(domain.Steps().size(), 1U); - const auto& step = domain.Steps()[0]; - EXPECT_EQ(step.name, "Step-1"); - EXPECT_DOUBLE_EQ(step.initial_increment, 0.25); - EXPECT_DOUBLE_EQ(step.time_period, 1.5); - EXPECT_DOUBLE_EQ(step.minimum_increment, 0.01); - EXPECT_DOUBLE_EQ(step.maximum_increment, 1.5); - ASSERT_EQ(step.boundaries.size(), 2U); - EXPECT_EQ(step.boundaries[0].target, "rootassembly"); - EXPECT_EQ(step.boundaries[0].first_dof, 1); - EXPECT_DOUBLE_EQ(step.boundaries[0].value, 0.125); - EXPECT_EQ(step.boundaries[1].last_dof, 2); - EXPECT_DOUBLE_EQ(step.boundaries[1].value, 0.0); - ASSERT_EQ(step.loads.size(), 1U); - EXPECT_EQ(step.loads[0].target, "RootAssembly"); - EXPECT_EQ(step.loads[0].dof, 6); - EXPECT_DOUBLE_EQ(step.loads[0].magnitude, -12.5); - EXPECT_EQ(domain.Warnings().size(), 8U); + ASSERT_EQ(domain.Steps().size(), 1U); + const auto& step = domain.Steps()[0]; + EXPECT_EQ(step.name, "Step-1"); + EXPECT_DOUBLE_EQ(step.initial_increment, 0.25); + EXPECT_DOUBLE_EQ(step.time_period, 1.5); + EXPECT_DOUBLE_EQ(step.minimum_increment, 0.01); + EXPECT_DOUBLE_EQ(step.maximum_increment, 1.5); + ASSERT_EQ(step.boundaries.size(), 2U); + EXPECT_EQ(step.boundaries[0].target, "rootassembly"); + EXPECT_EQ(step.boundaries[0].first_dof, 1); + EXPECT_DOUBLE_EQ(step.boundaries[0].value, 0.125); + EXPECT_EQ(step.boundaries[1].last_dof, 2); + EXPECT_DOUBLE_EQ(step.boundaries[1].value, 0.0); + ASSERT_EQ(step.loads.size(), 1U); + EXPECT_EQ(step.loads[0].target, "RootAssembly"); + EXPECT_EQ(step.loads[0].dof, 6); + EXPECT_DOUBLE_EQ(step.loads[0].magnitude, -12.5); + EXPECT_EQ(domain.Warnings().size(), 8U); - const auto legacyPath = repositoryRoot() / "reference" / - "cantilever beam" / "cantilever beam.inp"; - const auto bytesBefore = readExactBytes(legacyPath); - const auto timestampBefore = std::filesystem::last_write_time(legacyPath); - auto parsedLegacy = fesa::AbaqusInputReader{}.read(legacyPath); - ASSERT_TRUE(parsedLegacy.HasValue()); - auto legacy = fesa::AbaqusDomainMapper{}.map(parsedLegacy.Value()); - ASSERT_TRUE(legacy.HasValue()); - EXPECT_EQ(legacy.Value().Nodes().size(), 11U); - EXPECT_EQ(legacy.Value().Elements().size(), 10U); - EXPECT_EQ(legacy.Value().Materials().size(), 1U); - EXPECT_EQ(legacy.Value().Sections().size(), 1U); - EXPECT_EQ(legacy.Value().Steps().size(), 1U); - EXPECT_EQ(legacy.Value().Warnings().size(), 7U); - EXPECT_EQ(readExactBytes(legacyPath), bytesBefore); - EXPECT_EQ(std::filesystem::last_write_time(legacyPath), timestampBefore); + const auto legacy_path = RepositoryRoot() / "reference" / "cantilever beam" / + "cantilever beam.inp"; + const auto bytes_before = ReadExactBytes(legacy_path); + const auto timestamp_before = std::filesystem::last_write_time(legacy_path); + auto parsed_legacy = fesa::AbaqusInputReader{}.Read(legacy_path); + ASSERT_TRUE(parsed_legacy.HasValue()); + auto legacy = fesa::AbaqusDomainMapper{}.Map(parsed_legacy.Value()); + ASSERT_TRUE(legacy.HasValue()); + EXPECT_EQ(legacy.Value().Nodes().size(), 11U); + EXPECT_EQ(legacy.Value().Elements().size(), 10U); + EXPECT_EQ(legacy.Value().Materials().size(), 1U); + EXPECT_EQ(legacy.Value().Sections().size(), 1U); + EXPECT_EQ(legacy.Value().Steps().size(), 1U); + EXPECT_EQ(legacy.Value().Warnings().size(), 7U); + EXPECT_EQ(ReadExactBytes(legacy_path), bytes_before); + EXPECT_EQ(std::filesystem::last_write_time(legacy_path), timestamp_before); } -TEST(InpDomainMapping, ExpandsSetsAndMultipleIdentityInstancesDeterministically) { - const std::string deck = R"inp(*Part, name=P +TEST(InpDomainMapping, + ExpandsSetsAndMultipleIdentityInstancesDeterministically) { + const std::string deck = R"inp(*Part, name=P *Node 10, 0., 0., 0. 20, 1., 0., 0. @@ -355,577 +350,604 @@ OnlySecond, 2, 5. *End Step )inp"; - auto result = mapText("multiple-instances", deck); - ASSERT_TRUE(result.HasValue()); - const fesa::Domain& domain = result.Value(); + auto result = MapText("multiple-instances", deck); + ASSERT_TRUE(result.HasValue()); + const fesa::Domain& domain = result.Value(); - ASSERT_EQ(domain.Nodes().size(), 4U); - EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "First"); - EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 10); - EXPECT_EQ(domain.Nodes()[1].source_id.instance_name, "First"); - EXPECT_EQ(domain.Nodes()[1].source_id.source_label, 20); - EXPECT_EQ(domain.Nodes()[2].source_id.instance_name, "Second"); - EXPECT_EQ(domain.Nodes()[2].source_id.source_label, 10); - EXPECT_EQ(domain.Nodes()[3].source_id.instance_name, "Second"); - EXPECT_EQ(domain.Nodes()[3].source_id.source_label, 20); + ASSERT_EQ(domain.Nodes().size(), 4U); + EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "First"); + EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 10); + EXPECT_EQ(domain.Nodes()[1].source_id.instance_name, "First"); + EXPECT_EQ(domain.Nodes()[1].source_id.source_label, 20); + EXPECT_EQ(domain.Nodes()[2].source_id.instance_name, "Second"); + EXPECT_EQ(domain.Nodes()[2].source_id.source_label, 10); + EXPECT_EQ(domain.Nodes()[3].source_id.instance_name, "Second"); + EXPECT_EQ(domain.Nodes()[3].source_id.source_label, 20); - ASSERT_EQ(domain.Elements().size(), 2U); - EXPECT_EQ(domain.Elements()[0].source_id.instance_name, "First"); - EXPECT_EQ(domain.Elements()[0].node_indices, - (std::array{0U, 1U})); - EXPECT_EQ(domain.Elements()[1].source_id.instance_name, "Second"); - EXPECT_EQ(domain.Elements()[1].node_indices, - (std::array{2U, 3U})); + ASSERT_EQ(domain.Elements().size(), 2U); + EXPECT_EQ(domain.Elements()[0].source_id.instance_name, "First"); + EXPECT_EQ(domain.Elements()[0].node_indices, + (std::array{0U, 1U})); + EXPECT_EQ(domain.Elements()[1].source_id.instance_name, "Second"); + EXPECT_EQ(domain.Elements()[1].node_indices, + (std::array{2U, 3U})); - ASSERT_EQ(domain.NodeSets().size(), 3U); - EXPECT_EQ(domain.NodeSets()[0].name, "Ends"); - EXPECT_EQ(domain.NodeSets()[0].instance_name, std::optional{"First"}); - EXPECT_EQ(domain.NodeSets()[0].node_indices, - (std::vector{0U, 1U})); - EXPECT_EQ(domain.NodeSets()[1].instance_name, std::optional{"Second"}); - EXPECT_EQ(domain.NodeSets()[1].node_indices, - (std::vector{2U, 3U})); - EXPECT_EQ(domain.NodeSets()[2].name, "OnlySecond"); - EXPECT_EQ(domain.NodeSets()[2].node_indices, - (std::vector{3U})); + ASSERT_EQ(domain.NodeSets().size(), 3U); + EXPECT_EQ(domain.NodeSets()[0].name, "Ends"); + EXPECT_EQ(domain.NodeSets()[0].instance_name, + std::optional{"First"}); + EXPECT_EQ(domain.NodeSets()[0].node_indices, + (std::vector{0U, 1U})); + EXPECT_EQ(domain.NodeSets()[1].instance_name, + std::optional{"Second"}); + EXPECT_EQ(domain.NodeSets()[1].node_indices, + (std::vector{2U, 3U})); + EXPECT_EQ(domain.NodeSets()[2].name, "OnlySecond"); + EXPECT_EQ(domain.NodeSets()[2].node_indices, + (std::vector{3U})); - ASSERT_EQ(domain.ElementSets().size(), 3U); - EXPECT_EQ(domain.ElementSets()[0].instance_name, - std::optional{"First"}); - EXPECT_EQ(domain.ElementSets()[0].element_indices, - (std::vector{0U})); - EXPECT_EQ(domain.ElementSets()[1].instance_name, - std::optional{"Second"}); - EXPECT_EQ(domain.ElementSets()[1].element_indices, - (std::vector{1U})); - EXPECT_EQ(domain.ElementSets()[2].name, "OnlySecondBeam"); - EXPECT_EQ(domain.ElementSets()[2].element_indices, - (std::vector{1U})); + ASSERT_EQ(domain.ElementSets().size(), 3U); + EXPECT_EQ(domain.ElementSets()[0].instance_name, + std::optional{"First"}); + EXPECT_EQ(domain.ElementSets()[0].element_indices, + (std::vector{0U})); + EXPECT_EQ(domain.ElementSets()[1].instance_name, + std::optional{"Second"}); + EXPECT_EQ(domain.ElementSets()[1].element_indices, + (std::vector{1U})); + EXPECT_EQ(domain.ElementSets()[2].name, "OnlySecondBeam"); + EXPECT_EQ(domain.ElementSets()[2].element_indices, + (std::vector{1U})); - ASSERT_EQ(domain.Steps().size(), 1U); - EXPECT_EQ(domain.Steps()[0].boundaries[0].target, "OnlySecond"); - EXPECT_EQ(domain.Steps()[0].loads[0].target, "OnlySecond"); + ASSERT_EQ(domain.Steps().size(), 1U); + EXPECT_EQ(domain.Steps()[0].boundaries[0].target, "OnlySecond"); + EXPECT_EQ(domain.Steps()[0].loads[0].target, "OnlySecond"); - auto direct = mapText( - "direct-node-labels", - replaceOnce( - replaceOnce(minimalDeck(), "Root, 1, 6", "1, 1, 6"), - "Tip, 2, -1.", - "2, 2, -1.")); - ASSERT_TRUE(direct.HasValue()); - EXPECT_EQ(direct.Value().Steps()[0].boundaries[0].target, "1"); - EXPECT_EQ(direct.Value().Steps()[0].loads[0].target, "2"); + auto direct = + MapText("direct-node-labels", + ReplaceOnce(ReplaceOnce(MinimalDeck(), "Root, 1, 6", "1, 1, 6"), + "Tip, 2, -1.", "2, 2, -1.")); + ASSERT_TRUE(direct.HasValue()); + EXPECT_EQ(direct.Value().Steps()[0].boundaries[0].target, "1"); + EXPECT_EQ(direct.Value().Steps()[0].loads[0].target, "2"); - auto aboveThresholds = mapText( - "above-geometry-thresholds", - replaceOnce( - replaceOnce(minimalDeck(), "2, 1., 0., 0.", "2, 2e-12, 0., 0."), - "0., 1., 0.", - "1., 2e-12, 0.")); - ASSERT_TRUE(aboveThresholds.HasValue()); + auto above_thresholds = MapText( + "above-geometry-thresholds", + ReplaceOnce( + ReplaceOnce(MinimalDeck(), "2, 1., 0., 0.", "2, 2e-12, 0., 0."), + "0., 1., 0.", "1., 2e-12, 0.")); + ASSERT_TRUE(above_thresholds.HasValue()); - auto largeFinite = mapText( - "large-finite-geometry", - replaceOnce( - replaceOnce( - replaceOnce(minimalDeck(), "1, 0., 0., 0.", "1, 1e308, 0., 0."), - "2, 1., 0., 0.", - "2, 1e308, 1e297, 0."), - "0., 1., 0.", - "1e308, 0., 0.")); - ASSERT_TRUE(largeFinite.HasValue()); + auto large_finite = MapText( + "large-finite-geometry", + ReplaceOnce(ReplaceOnce(ReplaceOnce(MinimalDeck(), "1, 0., 0., 0.", + "1, 1e308, 0., 0."), + "2, 1., 0., 0.", "2, 1e308, 1e297, 0."), + "0., 1., 0.", "1e308, 0., 0.")); + ASSERT_TRUE(large_finite.HasValue()); } TEST(InpDomainMapping, KeepsNodeAndElementSetNamesInSeparateNamespaces) { - auto sharedName = mapText( - "separate-part-set-namespaces", - replaceOnce( - shellDeck(), - "*Elset, elset=ShellS4\n10", - "*Nset, nset=ShellS4\n1, 2, 3, 4\n*Elset, elset=ShellS4\n10")); + auto shared_name = MapText( + "separate-part-set-namespaces", + ReplaceOnce( + ShellDeck(), "*Elset, elset=ShellS4\n10", + "*Nset, nset=ShellS4\n1, 2, 3, 4\n*Elset, elset=ShellS4\n10")); - ASSERT_TRUE(sharedName.HasValue()); - const auto& domain = sharedName.Value(); - EXPECT_TRUE(std::any_of( - domain.NodeSets().begin(), domain.NodeSets().end(), - [](const fesa::NodeSet& set) { return set.name == "ShellS4"; })); - EXPECT_TRUE(std::any_of( - domain.ElementSets().begin(), domain.ElementSets().end(), - [](const fesa::ElementSet& set) { return set.name == "ShellS4"; })); + ASSERT_TRUE(shared_name.HasValue()); + const auto& domain = shared_name.Value(); + EXPECT_TRUE(std::any_of( + domain.NodeSets().begin(), domain.NodeSets().end(), + [](const fesa::NodeSet& set) { return set.name == "ShellS4"; })); + EXPECT_TRUE(std::any_of( + domain.ElementSets().begin(), domain.ElementSets().end(), + [](const fesa::ElementSet& set) { return set.name == "ShellS4"; })); - auto duplicateNodeSet = mapText( - "duplicate-part-node-set", - replaceOnce( - shellDeck(), - "*Elset, elset=ShellS4\n10", - "*Nset, nset=Shared\n1\n*Nset, nset=Shared\n2\n" - "*Elset, elset=ShellS4\n10")); - ASSERT_FALSE(duplicateNodeSet.HasValue()); - EXPECT_NE(findDiagnostic(duplicateNodeSet.GetStatus(), "duplicate-entity"), nullptr); + auto duplicate_node_set = + MapText("duplicate-part-node-set", + ReplaceOnce(ShellDeck(), "*Elset, elset=ShellS4\n10", + "*Nset, nset=Shared\n1\n*Nset, nset=Shared\n2\n" + "*Elset, elset=ShellS4\n10")); + ASSERT_FALSE(duplicate_node_set.HasValue()); + EXPECT_NE(FindDiagnostic(duplicate_node_set.GetStatus(), "duplicate-entity"), + nullptr); - auto duplicateElementSet = mapText( - "duplicate-part-element-set", - replaceOnce( - shellDeck(), - "*Elset, elset=ShellS4\n10", - "*Elset, elset=Repeated\n10\n*Elset, elset=Repeated\n20\n" - "*Elset, elset=ShellS4\n10")); - ASSERT_FALSE(duplicateElementSet.HasValue()); - EXPECT_NE(findDiagnostic(duplicateElementSet.GetStatus(), "duplicate-entity"), nullptr); + auto duplicate_element_set = MapText( + "duplicate-part-element-set", + ReplaceOnce(ShellDeck(), "*Elset, elset=ShellS4\n10", + "*Elset, elset=Repeated\n10\n*Elset, elset=Repeated\n20\n" + "*Elset, elset=ShellS4\n10")); + ASSERT_FALSE(duplicate_element_set.HasValue()); + EXPECT_NE( + FindDiagnostic(duplicate_element_set.GetStatus(), "duplicate-entity"), + nullptr); - auto assemblySharedName = mapText( - "separate-assembly-set-namespaces", - replaceOnce( - replaceOnce( - minimalDeck(), - "*Elset, elset=BeamSet\n1", - "*Nset, nset=Root\n1, 2\n*Elset, elset=BeamSet\n1"), - "*Nset, nset=Root, instance=Beam-1\n1", - "*Nset, nset=Root, instance=Beam-1\n1\n" - "*Elset, elset=Root, instance=Beam-1\n1")); - ASSERT_TRUE(assemblySharedName.HasValue()); - const auto& assemblyDomain = assemblySharedName.Value(); - const auto rootNodeSetCount = std::count_if( - assemblyDomain.NodeSets().begin(), assemblyDomain.NodeSets().end(), - [](const fesa::NodeSet& set) { return set.name == "Root"; }); - EXPECT_EQ(rootNodeSetCount, 1); - const auto rootNodeSet = std::find_if( - assemblyDomain.NodeSets().begin(), assemblyDomain.NodeSets().end(), - [](const fesa::NodeSet& set) { return set.name == "Root"; }); - ASSERT_NE(rootNodeSet, assemblyDomain.NodeSets().end()); - EXPECT_EQ(rootNodeSet->node_indices, (std::vector{0U})); - EXPECT_EQ(std::count_if( - assemblyDomain.ElementSets().begin(), assemblyDomain.ElementSets().end(), - [](const fesa::ElementSet& set) { return set.name == "Root"; }), 1); + auto assembly_shared_name = MapText( + "separate-assembly-set-namespaces", + ReplaceOnce( + ReplaceOnce(MinimalDeck(), "*Elset, elset=BeamSet\n1", + "*Nset, nset=Root\n1, 2\n*Elset, elset=BeamSet\n1"), + "*Nset, nset=Root, instance=Beam-1\n1", + "*Nset, nset=Root, instance=Beam-1\n1\n" + "*Elset, elset=Root, instance=Beam-1\n1")); + ASSERT_TRUE(assembly_shared_name.HasValue()); + const auto& assembly_domain = assembly_shared_name.Value(); + const auto root_node_set_count = std::count_if( + assembly_domain.NodeSets().begin(), assembly_domain.NodeSets().end(), + [](const fesa::NodeSet& set) { return set.name == "Root"; }); + EXPECT_EQ(root_node_set_count, 1); + const auto root_node_set = std::find_if( + assembly_domain.NodeSets().begin(), assembly_domain.NodeSets().end(), + [](const fesa::NodeSet& set) { return set.name == "Root"; }); + ASSERT_NE(root_node_set, assembly_domain.NodeSets().end()); + EXPECT_EQ(root_node_set->node_indices, (std::vector{0U})); + EXPECT_EQ(std::count_if( + assembly_domain.ElementSets().begin(), + assembly_domain.ElementSets().end(), + [](const fesa::ElementSet& set) { return set.name == "Root"; }), + 1); } TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) { - auto plain = mapText("without-no-ops", supportedInventoryDeck(false)); - auto withNoOps = mapText("with-no-ops", supportedInventoryDeck(true)); - ASSERT_TRUE(plain.HasValue()); - ASSERT_TRUE(withNoOps.HasValue()); + auto plain = MapText("without-no-ops", SupportedInventoryDeck(false)); + auto with_no_ops = MapText("with-no-ops", SupportedInventoryDeck(true)); + ASSERT_TRUE(plain.HasValue()); + ASSERT_TRUE(with_no_ops.HasValue()); - EXPECT_TRUE(plain.Value().Warnings().empty()); - ASSERT_EQ(withNoOps.Value().Warnings().size(), 8U); - EXPECT_EQ(withNoOps.Value().Warnings()[0].code, "ignored-input-keyword"); - EXPECT_EQ(withNoOps.Value().Warnings()[0].keyword, "PREPRINT"); - EXPECT_EQ(withNoOps.Value().Warnings()[1].keyword, - "TRANSVERSE SHEAR STIFFNESS"); - EXPECT_EQ(withNoOps.Value().Warnings()[2].keyword, "RESTART"); - EXPECT_EQ(withNoOps.Value().Warnings()[3].keyword, "OUTPUT"); - EXPECT_EQ(withNoOps.Value().Warnings()[4].keyword, "NODE OUTPUT"); - EXPECT_EQ(withNoOps.Value().Warnings()[5].keyword, "ELEMENT OUTPUT"); - EXPECT_EQ(withNoOps.Value().Warnings()[6].keyword, "CONTACT OUTPUT"); - EXPECT_EQ(withNoOps.Value().Warnings()[7].keyword, "OUTPUT"); - for (const auto& warning : withNoOps.Value().Warnings()) { - EXPECT_EQ(warning.severity, fesa::Severity::kWarning); - } + EXPECT_TRUE(plain.Value().Warnings().empty()); + ASSERT_EQ(with_no_ops.Value().Warnings().size(), 8U); + EXPECT_EQ(with_no_ops.Value().Warnings()[0].code, "ignored-input-keyword"); + EXPECT_EQ(with_no_ops.Value().Warnings()[0].keyword, "PREPRINT"); + EXPECT_EQ(with_no_ops.Value().Warnings()[1].keyword, + "TRANSVERSE SHEAR STIFFNESS"); + EXPECT_EQ(with_no_ops.Value().Warnings()[2].keyword, "RESTART"); + EXPECT_EQ(with_no_ops.Value().Warnings()[3].keyword, "OUTPUT"); + EXPECT_EQ(with_no_ops.Value().Warnings()[4].keyword, "NODE OUTPUT"); + EXPECT_EQ(with_no_ops.Value().Warnings()[5].keyword, "ELEMENT OUTPUT"); + EXPECT_EQ(with_no_ops.Value().Warnings()[6].keyword, "CONTACT OUTPUT"); + EXPECT_EQ(with_no_ops.Value().Warnings()[7].keyword, "OUTPUT"); + for (const auto& warning : with_no_ops.Value().Warnings()) { + EXPECT_EQ(warning.severity, fesa::Severity::kWarning); + } - EXPECT_EQ(withNoOps.Value().Nodes().size(), plain.Value().Nodes().size()); - EXPECT_EQ(withNoOps.Value().Elements().size(), plain.Value().Elements().size()); - EXPECT_EQ(withNoOps.Value().Materials().size(), plain.Value().Materials().size()); - EXPECT_EQ(withNoOps.Value().Sections().size(), plain.Value().Sections().size()); - EXPECT_EQ(withNoOps.Value().NodeSets().size(), plain.Value().NodeSets().size()); - EXPECT_EQ(withNoOps.Value().ElementSets().size(), plain.Value().ElementSets().size()); - EXPECT_EQ(withNoOps.Value().Steps().size(), plain.Value().Steps().size()); - EXPECT_EQ(withNoOps.Value().Steps()[0].boundaries.size(), - plain.Value().Steps()[0].boundaries.size()); - EXPECT_EQ(withNoOps.Value().Steps()[0].loads.size(), - plain.Value().Steps()[0].loads.size()); + EXPECT_EQ(with_no_ops.Value().Nodes().size(), plain.Value().Nodes().size()); + EXPECT_EQ(with_no_ops.Value().Elements().size(), + plain.Value().Elements().size()); + EXPECT_EQ(with_no_ops.Value().Materials().size(), + plain.Value().Materials().size()); + EXPECT_EQ(with_no_ops.Value().Sections().size(), + plain.Value().Sections().size()); + EXPECT_EQ(with_no_ops.Value().NodeSets().size(), + plain.Value().NodeSets().size()); + EXPECT_EQ(with_no_ops.Value().ElementSets().size(), + plain.Value().ElementSets().size()); + EXPECT_EQ(with_no_ops.Value().Steps().size(), plain.Value().Steps().size()); + EXPECT_EQ(with_no_ops.Value().Steps()[0].boundaries.size(), + plain.Value().Steps()[0].boundaries.size()); + EXPECT_EQ(with_no_ops.Value().Steps()[0].loads.size(), + plain.Value().Steps()[0].loads.size()); } // MITC4-MAP-001 TEST(InpDomainMapping, MapsS4AndS4rThroughOneMitc4Identity) { - auto result = mapText("mitc4-map-001", shellDeck()); + auto result = MapText("mitc4-map-001", ShellDeck()); - ASSERT_TRUE(result.HasValue()); - const fesa::Domain& domain = result.Value(); - EXPECT_TRUE(domain.Elements().empty()); - ASSERT_EQ(domain.ShellElements().size(), 4U); - EXPECT_EQ(domain.ShellElements()[0].source_id.instance_name, "First"); - EXPECT_EQ(domain.ShellElements()[0].source_id.source_label_text, "0010"); - EXPECT_EQ( - domain.ShellElements()[0].source_type, - fesa::ShellSourceElementType::kS4); - EXPECT_EQ( - domain.ShellElements()[0].node_indices, - (std::array{0U, 1U, 2U, 3U})); - EXPECT_EQ(domain.ShellElements()[0].section_index, 0U); - EXPECT_EQ(domain.ShellElements()[0].material_index, 0U); + ASSERT_TRUE(result.HasValue()); + const fesa::Domain& domain = result.Value(); + EXPECT_TRUE(domain.Elements().empty()); + ASSERT_EQ(domain.ShellElements().size(), 4U); + EXPECT_EQ(domain.ShellElements()[0].source_id.instance_name, "First"); + EXPECT_EQ(domain.ShellElements()[0].source_id.source_label_text, "0010"); + EXPECT_EQ(domain.ShellElements()[0].source_type, + fesa::ShellSourceElementType::kS4); + EXPECT_EQ(domain.ShellElements()[0].node_indices, + (std::array{0U, 1U, 2U, 3U})); + EXPECT_EQ(domain.ShellElements()[0].section_index, 0U); + EXPECT_EQ(domain.ShellElements()[0].material_index, 0U); - EXPECT_EQ(domain.ShellElements()[1].source_id.instance_name, "First"); - EXPECT_EQ( - domain.ShellElements()[1].source_type, - fesa::ShellSourceElementType::kS4r); - EXPECT_EQ( - domain.ShellElements()[1].node_indices, - (std::array{1U, 4U, 5U, 2U})); - EXPECT_EQ(domain.ShellElements()[1].section_index, 1U); - EXPECT_EQ(domain.ShellElements()[1].material_index, 1U); + EXPECT_EQ(domain.ShellElements()[1].source_id.instance_name, "First"); + EXPECT_EQ(domain.ShellElements()[1].source_type, + fesa::ShellSourceElementType::kS4r); + EXPECT_EQ(domain.ShellElements()[1].node_indices, + (std::array{1U, 4U, 5U, 2U})); + EXPECT_EQ(domain.ShellElements()[1].section_index, 1U); + EXPECT_EQ(domain.ShellElements()[1].material_index, 1U); - EXPECT_EQ(domain.ShellElements()[2].source_id.instance_name, "Second"); - EXPECT_EQ( - domain.ShellElements()[2].node_indices, - (std::array{6U, 7U, 8U, 9U})); - EXPECT_EQ(domain.ShellElements()[3].source_id.instance_name, "Second"); - EXPECT_EQ( - fesa::kMitc4InternalFormulation, - std::string_view{"FESA-MITC4"}); + EXPECT_EQ(domain.ShellElements()[2].source_id.instance_name, "Second"); + EXPECT_EQ(domain.ShellElements()[2].node_indices, + (std::array{6U, 7U, 8U, 9U})); + EXPECT_EQ(domain.ShellElements()[3].source_id.instance_name, "Second"); + EXPECT_EQ(fesa::kMitc4InternalFormulation, std::string_view{"FESA-MITC4"}); - ASSERT_EQ(domain.ShellSections().size(), 2U); - EXPECT_EQ(domain.ShellSections()[0].name, "ShellS4"); - EXPECT_DOUBLE_EQ(domain.ShellSections()[0].thickness, 0.1); - EXPECT_EQ(domain.ShellSections()[0].material_index, 0U); - EXPECT_EQ(domain.ShellSections()[1].name, "ShellS4R"); - EXPECT_DOUBLE_EQ(domain.ShellSections()[1].thickness, 0.2); - EXPECT_EQ(domain.ShellSections()[1].material_index, 1U); + ASSERT_EQ(domain.ShellSections().size(), 2U); + EXPECT_EQ(domain.ShellSections()[0].name, "ShellS4"); + EXPECT_DOUBLE_EQ(domain.ShellSections()[0].thickness, 0.1); + EXPECT_EQ(domain.ShellSections()[0].material_index, 0U); + EXPECT_EQ(domain.ShellSections()[1].name, "ShellS4R"); + EXPECT_DOUBLE_EQ(domain.ShellSections()[1].thickness, 0.2); + EXPECT_EQ(domain.ShellSections()[1].material_index, 1U); - ASSERT_EQ(domain.ShellNodeInitialFrames().size(), domain.Nodes().size()); - EXPECT_EQ(domain.ShellNodeInitialFrames()[0].node_index, 0U); - EXPECT_EQ( - domain.ShellNodeInitialFrames()[0].director, - (std::array{0.0, 0.0, 1.0})); - EXPECT_EQ( - domain.ShellNodeInitialFrames()[0].tangent_a, - (std::array{1.0, 0.0, 0.0})); - EXPECT_EQ( - domain.ShellNodeInitialFrames()[0].tangent_b, - (std::array{0.0, 1.0, 0.0})); + ASSERT_EQ(domain.ShellNodeInitialFrames().size(), domain.Nodes().size()); + EXPECT_EQ(domain.ShellNodeInitialFrames()[0].node_index, 0U); + EXPECT_EQ(domain.ShellNodeInitialFrames()[0].director, + (std::array{0.0, 0.0, 1.0})); + EXPECT_EQ(domain.ShellNodeInitialFrames()[0].tangent_a, + (std::array{1.0, 0.0, 0.0})); + EXPECT_EQ(domain.ShellNodeInitialFrames()[0].tangent_b, + (std::array{0.0, 1.0, 0.0})); - ASSERT_EQ(domain.Steps().size(), 1U); - ASSERT_EQ(domain.Steps()[0].boundaries.size(), 1U); - EXPECT_EQ(domain.Steps()[0].boundaries[0].last_dof, 6); - ASSERT_EQ(domain.Steps()[0].loads.size(), 1U); - EXPECT_EQ(domain.Steps()[0].loads[0].dof, 6); + ASSERT_EQ(domain.Steps().size(), 1U); + ASSERT_EQ(domain.Steps()[0].boundaries.size(), 1U); + EXPECT_EQ(domain.Steps()[0].boundaries[0].last_dof, 6); + ASSERT_EQ(domain.Steps()[0].loads.size(), 1U); + EXPECT_EQ(domain.Steps()[0].loads[0].dof, 6); } // MITC4-MAP-002 TEST(InpDomainMapping, RejectsInvalidShellAssignmentsAndProperties) { - struct InvalidCase { - std::string name; - std::string deck; - std::string expectedCode; - fesa::FailureCategory category; - }; + struct InvalidCase { + std::string name; + std::string deck; + std::string expected_code; + fesa::FailureCategory category; + }; - const std::string base = shellDeck(); - const std::vector cases{ - {"unresolved-material", - replaceOnce(base, "material=Steel", "material=Missing"), - "unresolved-shell-section", fesa::FailureCategory::kInput}, - {"unresolved-elset", - replaceOnce(base, "elset=ShellS4, material=Steel", - "elset=Missing, material=Steel"), - "unresolved-shell-section", fesa::FailureCategory::kInput}, - {"missing-assignment", - replaceOnce( - base, - "*Shell Section, elset=ShellS4R, material=Aluminum\n0.2\n", - ""), - "invalid-shell-section-assignment", fesa::FailureCategory::kInput}, - {"conflicting-assignment", - replaceOnce(base, "elset=ShellS4R, material=Aluminum", - "elset=ShellS4, material=Aluminum"), - "invalid-shell-section-assignment", fesa::FailureCategory::kInput}, - {"invalid-thickness", - replaceOnce(base, "0.2\n*End Part", "0.\n*End Part"), - "invalid-shell-thickness", fesa::FailureCategory::kModel}, - {"invalid-material", - replaceOnce(base, "70000., 0.25", "70000., 0.5"), - "invalid-shell-material", fesa::FailureCategory::kModel}}; + const std::string base = ShellDeck(); + const std::vector cases{ + {"unresolved-material", + ReplaceOnce(base, "material=Steel", "material=Missing"), + "unresolved-shell-section", fesa::FailureCategory::kInput}, + {"unresolved-elset", + ReplaceOnce(base, "elset=ShellS4, material=Steel", + "elset=Missing, material=Steel"), + "unresolved-shell-section", fesa::FailureCategory::kInput}, + {"missing-assignment", + ReplaceOnce(base, + "*Shell Section, elset=ShellS4R, material=Aluminum\n0.2\n", + ""), + "invalid-shell-section-assignment", fesa::FailureCategory::kInput}, + {"conflicting-assignment", + ReplaceOnce(base, "elset=ShellS4R, material=Aluminum", + "elset=ShellS4, material=Aluminum"), + "invalid-shell-section-assignment", fesa::FailureCategory::kInput}, + {"invalid-thickness", + ReplaceOnce(base, "0.2\n*End Part", "0.\n*End Part"), + "invalid-shell-thickness", fesa::FailureCategory::kModel}, + {"invalid-material", ReplaceOnce(base, "70000., 0.25", "70000., 0.5"), + "invalid-shell-material", fesa::FailureCategory::kModel}}; - for (const auto& testCase : cases) { - SCOPED_TRACE(testCase.name); - auto result = mapText("mitc4-map-002-" + testCase.name, testCase.deck); - ASSERT_FALSE(result.HasValue()); - EXPECT_EQ(result.GetStatus().Category(), testCase.category); - ASSERT_NE(findDiagnostic(result.GetStatus(), testCase.expectedCode), nullptr); - } + for (const auto& test_case : cases) { + SCOPED_TRACE(test_case.name); + auto result = MapText("mitc4-map-002-" + test_case.name, test_case.deck); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), test_case.category); + ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code), + nullptr); + } } // MITC4-MAP-003 TEST(InpDomainMapping, RejectsInvalidShellConnectivityOptionsAndMixedModels) { - struct InvalidCase { - std::string name; - std::string deck; - std::string expectedCode; - }; + struct InvalidCase { + std::string name; + std::string deck; + std::string expected_code; + }; - const std::string base = shellDeck(); - const std::vector cases{ - {"wrong-arity", replaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3"), - "invalid-shell-connectivity"}, - {"repeated-node", replaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 2, 4"), - "invalid-shell-connectivity"}, - {"dangling-node", replaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3, 99"), - "invalid-shell-connectivity"}, - {"unsupported-section-option", - replaceOnce(base, "material=Steel\n0.1", "material=Steel, offset=0.1\n0.1"), - "unsupported-shell-section-option"}, - {"unsupported-element", - replaceOnce(base, "type=S4R", "type=S8R"), - "unsupported-element-formulation"}, - {"mixed-beam-shell", - replaceOnce(base, - "0020, 2, 5, 6, 3\n*Elset", - "0020, 2, 5, 6, 3\n*Element, type=B33\n30, 1, 2\n*Elset"), - "unsupported-mixed-element-model"}}; + const std::string base = ShellDeck(); + const std::vector cases{ + {"wrong-arity", ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3"), + "invalid-shell-connectivity"}, + {"repeated-node", + ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 2, 4"), + "invalid-shell-connectivity"}, + {"dangling-node", + ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3, 99"), + "invalid-shell-connectivity"}, + {"unsupported-section-option", + ReplaceOnce(base, "material=Steel\n0.1", + "material=Steel, offset=0.1\n0.1"), + "unsupported-shell-section-option"}, + {"unsupported-element", ReplaceOnce(base, "type=S4R", "type=S8R"), + "unsupported-element-formulation"}, + {"mixed-beam-shell", + ReplaceOnce(base, "0020, 2, 5, 6, 3\n*Elset", + "0020, 2, 5, 6, 3\n*Element, type=B33\n30, 1, 2\n*Elset"), + "unsupported-mixed-element-model"}}; - for (const auto& testCase : cases) { - SCOPED_TRACE(testCase.name); - auto result = mapText("mitc4-map-003-" + testCase.name, testCase.deck); - ASSERT_FALSE(result.HasValue()); - EXPECT_EQ( - result.GetStatus().Category(), - fesa::FailureCategory::kInput); - ASSERT_NE(findDiagnostic(result.GetStatus(), testCase.expectedCode), nullptr); - } + for (const auto& test_case : cases) { + SCOPED_TRACE(test_case.name); + auto result = MapText("mitc4-map-003-" + test_case.name, test_case.deck); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); + ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code), + nullptr); + } } // MITC4-MAP-004 -TEST(InpDomainMapping, PreservesProcedureLoadAndOutputRequestBoundariesForShells) { - const std::string withNoOps = replaceOnce( - shellDeck(), - "*End Step\n", - "*Output, field\n*Node Output\nU, RF\n*Element Output\nS\n*End Step\n"); - auto valid = mapText("mitc4-map-004-no-ops", withNoOps); - ASSERT_TRUE(valid.HasValue()); - ASSERT_EQ(valid.Value().Warnings().size(), 3U); - EXPECT_EQ(valid.Value().Warnings()[0].keyword, "OUTPUT"); - EXPECT_EQ(valid.Value().Warnings()[1].keyword, "NODE OUTPUT"); - EXPECT_EQ(valid.Value().Warnings()[2].keyword, "ELEMENT OUTPUT"); +TEST(InpDomainMapping, + PreservesProcedureLoadAndOutputRequestBoundariesForShells) { + const std::string with_no_ops = ReplaceOnce( + ShellDeck(), "*End Step\n", + "*Output, field\n*Node Output\nU, RF\n*Element Output\nS\n*End Step\n"); + auto valid = MapText("mitc4-map-004-no-ops", with_no_ops); + ASSERT_TRUE(valid.HasValue()); + ASSERT_EQ(valid.Value().Warnings().size(), 3U); + EXPECT_EQ(valid.Value().Warnings()[0].keyword, "OUTPUT"); + EXPECT_EQ(valid.Value().Warnings()[1].keyword, "NODE OUTPUT"); + EXPECT_EQ(valid.Value().Warnings()[2].keyword, "ELEMENT OUTPUT"); - struct InvalidCase { - std::string name; - std::string deck; - std::string expectedCode; - }; - const std::string base = shellDeck(); - const std::vector cases{ - {"second-step", - base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n", - "unsupported-multiple-step"}, - {"nonlinear-step", - replaceOnce(base, "nlgeom=NO", "nlgeom=YES"), - "unsupported-nonlinear-geometry"}, - {"other-procedure", - replaceOnce(base, "*Static\n0.1, 1., 0.01, 1.", - "*Dynamic\n0.1, 1., 0.01, 1."), - "unsupported-keyword"}, - {"distributed-load", - replaceOnce(base, "*Cload\nTipSecond, 6, 1.", - "*Dload\nShellS4, P, 1."), - "unsupported-distributed-load"}}; + struct InvalidCase { + std::string name; + std::string deck; + std::string expected_code; + }; + const std::string base = ShellDeck(); + const std::vector cases{ + {"second-step", base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n", + "unsupported-multiple-step"}, + {"nonlinear-step", ReplaceOnce(base, "nlgeom=NO", "nlgeom=YES"), + "unsupported-nonlinear-geometry"}, + {"other-procedure", + ReplaceOnce(base, "*Static\n0.1, 1., 0.01, 1.", + "*Dynamic\n0.1, 1., 0.01, 1."), + "unsupported-keyword"}, + {"distributed-load", + ReplaceOnce(base, "*Cload\nTipSecond, 6, 1.", "*Dload\nShellS4, P, 1."), + "unsupported-distributed-load"}}; - for (const auto& testCase : cases) { - SCOPED_TRACE(testCase.name); - auto result = mapText("mitc4-map-004-" + testCase.name, testCase.deck); - ASSERT_FALSE(result.HasValue()); - EXPECT_EQ( - result.GetStatus().Category(), - fesa::FailureCategory::kInput); - ASSERT_NE(findDiagnostic(result.GetStatus(), testCase.expectedCode), nullptr); - } + for (const auto& test_case : cases) { + SCOPED_TRACE(test_case.name); + auto result = MapText("mitc4-map-004-" + test_case.name, test_case.deck); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); + ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code), + nullptr); + } } TEST(InpDomainMapping, RejectsUnsupportedAndInvalidPortfolio) { - struct InvalidCase { - std::string name; - std::string deck; - std::string expectedCode; - fesa::FailureCategory category; - }; + struct InvalidCase { + std::string name; + std::string deck; + std::string expected_code; + fesa::FailureCategory category; + }; - const std::string base = minimalDeck(); - const std::vector cases{ - {"b31", replaceOnce(base, "type=B33", "type=B31"), - "unsupported-element-formulation", fesa::FailureCategory::kInput}, - {"transform", replaceOnce(base, "*End Instance\n", "1., 0., 0.\n*End Instance\n"), - "unsupported-instance-transform", fesa::FailureCategory::kInput}, - {"nested-assembly", replaceOnce(base, "*End Assembly\n", "*Assembly, name=Nested\n*End Assembly\n*End Assembly\n"), - "unsupported-nested-assembly", fesa::FailureCategory::kInput}, - {"multiple-step", base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n", - "unsupported-multiple-step", fesa::FailureCategory::kInput}, - {"late-part", replaceOnce(base, "*End Assembly\n*Material", "*End Assembly\n*Part, name=Late\n*End Part\n*Material"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"material-before-assembly", replaceOnce(base, "*Assembly, name=Assembly", "*Material, name=Early\n*Elastic\n50., 0.2\n*Assembly, name=Assembly"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"material-after-model-boundary", replaceOnce(base, - "*Material, name=Steel\n*Elastic\n100., 0.25\n*Boundary\nRoot, 1, 6", - "*Boundary\nRoot, 1, 6\n*Material, name=Steel\n*Elastic\n100., 0.25"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"keyword-after-step", base + "*Preprint, echo=NO\n", - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"assembly-instance-after-set", replaceOnce(base, - "*Instance, name=Beam-1, part=BeamPart\n*End Instance\n*Nset, nset=Root, instance=Beam-1\n1", - "*Nset, nset=Root, instance=Beam-1\n1\n*Instance, name=Beam-1, part=BeamPart\n*End Instance"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"element-before-node", replaceOnce(base, - "*Node\n1, 0., 0., 0.\n2, 1., 0., 0.\n*Element, type=B33\n1, 1, 2", - "*Element, type=B33\n1, 1, 2\n*Node\n1, 0., 0., 0.\n2, 1., 0., 0."), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"shear-before-section-context", replaceOnce(base, - "*Beam General Section", - "*Transverse Shear Stiffness\n1., 2., 3.\n*Beam General Section"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"incomplete-part", replaceOnce(base, - base.substr(base.find("*Part"), base.find("*End Part") + std::string{"*End Part\n"}.size() - base.find("*Part")), - "*Part, name=BeamPart\n*End Part\n"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"empty-assembly", replaceOnce(base, - base.substr(base.find("*Assembly"), base.find("*End Assembly") + std::string{"*End Assembly\n"}.size() - base.find("*Assembly")), - "*Assembly, name=Assembly\n*End Assembly\n"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"cload-before-static", replaceOnce(base, - "*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.", - "*Cload\nTip, 2, -1.\n*Static\n0.1, 1., 0.01, 1."), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"static-after-boundary", replaceOnce( - replaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"), - "*Static\n0.1, 1., 0.01, 1.", - "*Boundary\nRoot, 1, 6\n*Static\n0.1, 1., 0.01, 1."), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"boundary-after-cload", replaceOnce( - replaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"), - "Tip, 2, -1.\n*End Step", - "Tip, 2, -1.\n*Boundary\nRoot, 1, 6\n*End Step"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"cload-after-no-op", replaceOnce(base, "*Cload", "*Restart, write, frequency=0\n*Cload"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"step-without-static", replaceOnce(base, - "*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.\n*End Step", - "*End Step"), - "invalid-keyword-location", fesa::FailureCategory::kInput}, - {"dependent-instance", replaceOnce(base, "part=BeamPart", "part=BeamPart, dependent=YES"), - "unsupported-instance-mesh-semantics", fesa::FailureCategory::kInput}, - {"coupled-section", replaceOnce(base, "1., 1., 0., 1., 1.", "1., 1., 0.5, 1., 1."), - "unsupported-coupled-section", fesa::FailureCategory::kModel}, - {"zero-length", replaceOnce(base, "2, 1., 0., 0.", "2, 0., 0., 0."), - "invalid-beam-length", fesa::FailureCategory::kModel}, - {"parallel-guide", replaceOnce(base, "0., 1., 0.", "1., 0., 0."), - "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, - {"nonpositive-area", replaceOnce(base, "1., 1., 0., 1., 1.", "0., 1., 0., 1., 1."), - "invalid-beam-property", fesa::FailureCategory::kModel}, - {"nonpositive-derived-shear", replaceOnce(base, "100., 0.25", "100., -1.25"), - "invalid-beam-property", fesa::FailureCategory::kModel}, - {"nonfinite-elastic", replaceOnce(base, "100., 0.25", "inf, 0.25"), - "invalid-beam-property", fesa::FailureCategory::kModel}, - {"nonfinite-section-property", replaceOnce(base, "1., 1., 0., 1., 1.", "nan, 1., 0., 1., 1."), - "invalid-beam-property", fesa::FailureCategory::kModel}, - {"nonfinite-guide", replaceOnce(base, "0., 1., 0.", "0., inf, 0."), - "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, - {"length-at-threshold", replaceOnce(base, "2, 1., 0., 0.", "2, 1e-12, 0., 0."), - "invalid-beam-length", fesa::FailureCategory::kModel}, - {"guide-at-threshold", replaceOnce(base, "0., 1., 0.", "1., 1e-12, 0."), - "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, - {"duplicate-elastic", replaceOnce(base, "100., 0.25\n*Boundary", "100., 0.25\n*Elastic\n100., 0.25\n*Boundary"), - "duplicate-entity", fesa::FailureCategory::kInput}, - {"duplicate-node-label", replaceOnce(base, "2, 1., 0., 0.", "1, 1., 0., 0."), - "duplicate-entity", fesa::FailureCategory::kInput}, - {"dangling-connectivity", replaceOnce(base, "1, 1, 2", "1, 1, 9"), - "unresolved-reference", fesa::FailureCategory::kInput}, - {"invalid-dof", replaceOnce(base, "Root, 1, 6", "Root, 1, 7"), - "invalid-dof", fesa::FailureCategory::kInput}, - {"nonfinite-coordinate", replaceOnce(base, "1., 0., 0.", "nan, 0., 0."), - "invalid-numeric-value", fesa::FailureCategory::kInput}, - {"malformed-node-arity", replaceOnce(base, "1, 0., 0., 0.", "1, 0., 0."), - "invalid-data-arity", fesa::FailureCategory::kInput}, - {"nlgeom", replaceOnce(base, "nlgeom=NO", "nlgeom=YES"), - "unsupported-nonlinear-geometry", fesa::FailureCategory::kModel}, - {"unknown-keyword", replaceOnce(base, "*Assembly, name=Assembly", "*Density\n1.\n*Assembly, name=Assembly"), - "unsupported-keyword", fesa::FailureCategory::kInput}, - {"invalid-static-arity", replaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 0.01"), - "invalid-static-data", fesa::FailureCategory::kInput}, - {"invalid-static-range", replaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 2., 1."), - "invalid-static-data", fesa::FailureCategory::kInput}, - {"invalid-generate", replaceOnce(base, "*Elset, elset=BeamSet\n1", "*Elset, elset=BeamSet, generate\n1, 1, 0"), - "invalid-set-range", fesa::FailureCategory::kInput}, - {"nonlanding-generate", replaceOnce(base, "*Elset, elset=BeamSet\n1", "*Elset, elset=BeamSet, generate\n1, 2, 2"), - "invalid-set-range", fesa::FailureCategory::kInput}, - {"ambiguous-direct-label", replaceOnce( - replaceOnce(base, - "*End Instance\n*Nset, nset=Root", - "*End Instance\n*Instance, name=Beam-2, part=BeamPart\n*End Instance\n*Nset, nset=Root"), - "Root, 1, 6", - "1, 1, 6"), - "unresolved-reference", fesa::FailureCategory::kInput}, - {"ambiguous-part-set", replaceOnce( - replaceOnce( - replaceOnce(base, - "*Elset, elset=BeamSet", - "*Nset, nset=Local\n1\n*Elset, elset=BeamSet"), - "*End Instance\n*Nset, nset=Root", - "*End Instance\n*Instance, name=Beam-2, part=BeamPart\n*End Instance\n*Nset, nset=Root"), - "Root, 1, 6", - "Local, 1, 6"), - "unresolved-reference", fesa::FailureCategory::kInput}, - {"direct-set-conflict", replaceOnce(base, - "*Step, name=Load", - "*Boundary\n1, 1, 1, 2.\n*Step, name=Load"), - "conflicting-boundary-condition", fesa::FailureCategory::kInput}, - {"dangling-boundary-target", replaceOnce(base, "Root, 1, 6", "Missing, 1, 6"), - "unresolved-reference", fesa::FailureCategory::kInput}, - {"conflicting-boundary", replaceOnce(base, "*Step, name=Load", "*Boundary\nRoot, 1, 1, 2.\n*Step, name=Load"), - "conflicting-boundary-condition", fesa::FailureCategory::kInput}}; + const std::string base = MinimalDeck(); + const std::vector cases{ + {"b31", ReplaceOnce(base, "type=B33", "type=B31"), + "unsupported-element-formulation", fesa::FailureCategory::kInput}, + {"transform", + ReplaceOnce(base, "*End Instance\n", "1., 0., 0.\n*End Instance\n"), + "unsupported-instance-transform", fesa::FailureCategory::kInput}, + {"nested-assembly", + ReplaceOnce(base, "*End Assembly\n", + "*Assembly, name=Nested\n*End Assembly\n*End Assembly\n"), + "unsupported-nested-assembly", fesa::FailureCategory::kInput}, + {"multiple-step", base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n", + "unsupported-multiple-step", fesa::FailureCategory::kInput}, + {"late-part", + ReplaceOnce(base, "*End Assembly\n*Material", + "*End Assembly\n*Part, name=Late\n*End Part\n*Material"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"material-before-assembly", + ReplaceOnce(base, "*Assembly, name=Assembly", + "*Material, name=Early\n*Elastic\n50., 0.2\n*Assembly, " + "name=Assembly"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"material-after-model-boundary", + ReplaceOnce(base, + "*Material, name=Steel\n*Elastic\n100., " + "0.25\n*Boundary\nRoot, 1, 6", + "*Boundary\nRoot, 1, 6\n*Material, " + "name=Steel\n*Elastic\n100., 0.25"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"keyword-after-step", base + "*Preprint, echo=NO\n", + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"assembly-instance-after-set", + ReplaceOnce(base, + "*Instance, name=Beam-1, part=BeamPart\n*End " + "Instance\n*Nset, nset=Root, instance=Beam-1\n1", + "*Nset, nset=Root, instance=Beam-1\n1\n*Instance, " + "name=Beam-1, part=BeamPart\n*End Instance"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"element-before-node", + ReplaceOnce( + base, + "*Node\n1, 0., 0., 0.\n2, 1., 0., 0.\n*Element, type=B33\n1, 1, 2", + "*Element, type=B33\n1, 1, 2\n*Node\n1, 0., 0., 0.\n2, 1., 0., " + "0."), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"shear-before-section-context", + ReplaceOnce( + base, "*Beam General Section", + "*Transverse Shear Stiffness\n1., 2., 3.\n*Beam General Section"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"incomplete-part", + ReplaceOnce(base, + base.substr(base.find("*Part"), + base.find("*End Part") + + std::string{"*End Part\n"}.size() - + base.find("*Part")), + "*Part, name=BeamPart\n*End Part\n"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"empty-assembly", + ReplaceOnce(base, + base.substr(base.find("*Assembly"), + base.find("*End Assembly") + + std::string{"*End Assembly\n"}.size() - + base.find("*Assembly")), + "*Assembly, name=Assembly\n*End Assembly\n"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"cload-before-static", + ReplaceOnce(base, "*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.", + "*Cload\nTip, 2, -1.\n*Static\n0.1, 1., 0.01, 1."), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"static-after-boundary", + ReplaceOnce(ReplaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"), + "*Static\n0.1, 1., 0.01, 1.", + "*Boundary\nRoot, 1, 6\n*Static\n0.1, 1., 0.01, 1."), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"boundary-after-cload", + ReplaceOnce(ReplaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"), + "Tip, 2, -1.\n*End Step", + "Tip, 2, -1.\n*Boundary\nRoot, 1, 6\n*End Step"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"cload-after-no-op", + ReplaceOnce(base, "*Cload", "*Restart, write, frequency=0\n*Cload"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"step-without-static", + ReplaceOnce(base, + "*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.\n*End Step", + "*End Step"), + "invalid-keyword-location", fesa::FailureCategory::kInput}, + {"dependent-instance", + ReplaceOnce(base, "part=BeamPart", "part=BeamPart, dependent=YES"), + "unsupported-instance-mesh-semantics", fesa::FailureCategory::kInput}, + {"coupled-section", + ReplaceOnce(base, "1., 1., 0., 1., 1.", "1., 1., 0.5, 1., 1."), + "unsupported-coupled-section", fesa::FailureCategory::kModel}, + {"zero-length", ReplaceOnce(base, "2, 1., 0., 0.", "2, 0., 0., 0."), + "invalid-beam-length", fesa::FailureCategory::kModel}, + {"parallel-guide", ReplaceOnce(base, "0., 1., 0.", "1., 0., 0."), + "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, + {"nonpositive-area", + ReplaceOnce(base, "1., 1., 0., 1., 1.", "0., 1., 0., 1., 1."), + "invalid-beam-property", fesa::FailureCategory::kModel}, + {"nonpositive-derived-shear", + ReplaceOnce(base, "100., 0.25", "100., -1.25"), "invalid-beam-property", + fesa::FailureCategory::kModel}, + {"nonfinite-elastic", ReplaceOnce(base, "100., 0.25", "inf, 0.25"), + "invalid-beam-property", fesa::FailureCategory::kModel}, + {"nonfinite-section-property", + ReplaceOnce(base, "1., 1., 0., 1., 1.", "nan, 1., 0., 1., 1."), + "invalid-beam-property", fesa::FailureCategory::kModel}, + {"nonfinite-guide", ReplaceOnce(base, "0., 1., 0.", "0., inf, 0."), + "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, + {"length-at-threshold", + ReplaceOnce(base, "2, 1., 0., 0.", "2, 1e-12, 0., 0."), + "invalid-beam-length", fesa::FailureCategory::kModel}, + {"guide-at-threshold", ReplaceOnce(base, "0., 1., 0.", "1., 1e-12, 0."), + "invalid-beam-guide-vector", fesa::FailureCategory::kModel}, + {"duplicate-elastic", + ReplaceOnce(base, "100., 0.25\n*Boundary", + "100., 0.25\n*Elastic\n100., 0.25\n*Boundary"), + "duplicate-entity", fesa::FailureCategory::kInput}, + {"duplicate-node-label", + ReplaceOnce(base, "2, 1., 0., 0.", "1, 1., 0., 0."), "duplicate-entity", + fesa::FailureCategory::kInput}, + {"dangling-connectivity", ReplaceOnce(base, "1, 1, 2", "1, 1, 9"), + "unresolved-reference", fesa::FailureCategory::kInput}, + {"invalid-dof", ReplaceOnce(base, "Root, 1, 6", "Root, 1, 7"), + "invalid-dof", fesa::FailureCategory::kInput}, + {"nonfinite-coordinate", ReplaceOnce(base, "1., 0., 0.", "nan, 0., 0."), + "invalid-numeric-value", fesa::FailureCategory::kInput}, + {"malformed-node-arity", ReplaceOnce(base, "1, 0., 0., 0.", "1, 0., 0."), + "invalid-data-arity", fesa::FailureCategory::kInput}, + {"nlgeom", ReplaceOnce(base, "nlgeom=NO", "nlgeom=YES"), + "unsupported-nonlinear-geometry", fesa::FailureCategory::kModel}, + {"unknown-keyword", + ReplaceOnce(base, "*Assembly, name=Assembly", + "*Density\n1.\n*Assembly, name=Assembly"), + "unsupported-keyword", fesa::FailureCategory::kInput}, + {"invalid-static-arity", + ReplaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 0.01"), + "invalid-static-data", fesa::FailureCategory::kInput}, + {"invalid-static-range", + ReplaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 2., 1."), + "invalid-static-data", fesa::FailureCategory::kInput}, + {"invalid-generate", + ReplaceOnce(base, "*Elset, elset=BeamSet\n1", + "*Elset, elset=BeamSet, generate\n1, 1, 0"), + "invalid-set-range", fesa::FailureCategory::kInput}, + {"nonlanding-generate", + ReplaceOnce(base, "*Elset, elset=BeamSet\n1", + "*Elset, elset=BeamSet, generate\n1, 2, 2"), + "invalid-set-range", fesa::FailureCategory::kInput}, + {"ambiguous-direct-label", + ReplaceOnce( + ReplaceOnce(base, "*End Instance\n*Nset, nset=Root", + "*End Instance\n*Instance, name=Beam-2, " + "part=BeamPart\n*End Instance\n*Nset, nset=Root"), + "Root, 1, 6", "1, 1, 6"), + "unresolved-reference", fesa::FailureCategory::kInput}, + {"ambiguous-part-set", + ReplaceOnce( + ReplaceOnce( + ReplaceOnce(base, "*Elset, elset=BeamSet", + "*Nset, nset=Local\n1\n*Elset, elset=BeamSet"), + "*End Instance\n*Nset, nset=Root", + "*End Instance\n*Instance, name=Beam-2, part=BeamPart\n*End " + "Instance\n*Nset, nset=Root"), + "Root, 1, 6", "Local, 1, 6"), + "unresolved-reference", fesa::FailureCategory::kInput}, + {"direct-set-conflict", + ReplaceOnce(base, "*Step, name=Load", + "*Boundary\n1, 1, 1, 2.\n*Step, name=Load"), + "conflicting-boundary-condition", fesa::FailureCategory::kInput}, + {"dangling-boundary-target", + ReplaceOnce(base, "Root, 1, 6", "Missing, 1, 6"), "unresolved-reference", + fesa::FailureCategory::kInput}, + {"conflicting-boundary", + ReplaceOnce(base, "*Step, name=Load", + "*Boundary\nRoot, 1, 1, 2.\n*Step, name=Load"), + "conflicting-boundary-condition", fesa::FailureCategory::kInput}}; - for (const auto& testCase : cases) { - SCOPED_TRACE(testCase.name); - auto result = mapText("invalid-" + testCase.name, testCase.deck); - ASSERT_FALSE(result.HasValue()); - EXPECT_EQ(result.GetStatus().Category(), testCase.category); - const auto* diagnostic = findDiagnostic(result.GetStatus(), testCase.expectedCode); - ASSERT_NE(diagnostic, nullptr); - EXPECT_EQ(diagnostic->severity, fesa::Severity::kError); - EXPECT_FALSE(diagnostic->location.file.empty()); - EXPECT_GT(diagnostic->location.line, 0U); - } + for (const auto& test_case : cases) { + SCOPED_TRACE(test_case.name); + auto result = MapText("invalid-" + test_case.name, test_case.deck); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), test_case.category); + const auto* diagnostic = + FindDiagnostic(result.GetStatus(), test_case.expected_code); + ASSERT_NE(diagnostic, nullptr); + EXPECT_EQ(diagnostic->severity, fesa::Severity::kError); + EXPECT_FALSE(diagnostic->location.file.empty()); + EXPECT_GT(diagnostic->location.line, 0U); + } - struct SameTokenAmbiguityCase { - std::string name; - std::string deck; - std::string expectedKeyword; - std::size_t expectedLine; - }; - const std::string sameTokenBase = replaceOnce( - base, - "*Elset, elset=BeamSet", - "*Nset, nset=1\n2\n*Elset, elset=BeamSet"); - const std::vector sameTokenCases{ - {"boundary", replaceOnce(sameTokenBase, "Root, 1, 6", "1, 1, 6"), - "BOUNDARY", 27U}, - {"cload", replaceOnce(sameTokenBase, "Tip, 2, -1.", "1, 2, -1."), - "CLOAD", 32U}}; + struct SameTokenAmbiguityCase { + std::string name; + std::string deck; + std::string expected_keyword; + std::size_t expected_line; + }; + const std::string same_token_base = ReplaceOnce( + base, "*Elset, elset=BeamSet", "*Nset, nset=1\n2\n*Elset, elset=BeamSet"); + const std::vector same_token_cases{ + {"boundary", ReplaceOnce(same_token_base, "Root, 1, 6", "1, 1, 6"), + "BOUNDARY", 27U}, + {"cload", ReplaceOnce(same_token_base, "Tip, 2, -1.", "1, 2, -1."), + "CLOAD", 32U}}; - for (const auto& testCase : sameTokenCases) { - SCOPED_TRACE("same-token-" + testCase.name); - auto result = mapText("same-token-" + testCase.name, testCase.deck); - ASSERT_FALSE(result.HasValue()); - EXPECT_EQ( - result.GetStatus().Category(), - fesa::FailureCategory::kInput); - const auto* diagnostic = - findDiagnostic(result.GetStatus(), "unresolved-reference"); - ASSERT_NE(diagnostic, nullptr); - EXPECT_EQ(diagnostic->severity, fesa::Severity::kError); - EXPECT_EQ(diagnostic->keyword, testCase.expectedKeyword); - EXPECT_EQ(diagnostic->entity_identity, "1"); - EXPECT_EQ(diagnostic->location.line, testCase.expectedLine); - EXPECT_EQ( - diagnostic->location.file.filename().string(), - "fesa-domain-mapper-same-token-" + testCase.name + ".inp"); - } + for (const auto& test_case : same_token_cases) { + SCOPED_TRACE("same-token-" + test_case.name); + auto result = MapText("same-token-" + test_case.name, test_case.deck); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); + const auto* diagnostic = + FindDiagnostic(result.GetStatus(), "unresolved-reference"); + ASSERT_NE(diagnostic, nullptr); + EXPECT_EQ(diagnostic->severity, fesa::Severity::kError); + EXPECT_EQ(diagnostic->keyword, test_case.expected_keyword); + EXPECT_EQ(diagnostic->entity_identity, "1"); + EXPECT_EQ(diagnostic->location.line, test_case.expected_line); + EXPECT_EQ(diagnostic->location.file.filename().string(), + "fesa-domain-mapper-same-token-" + test_case.name + ".inp"); + } } TEST(InpDomainMapping, RejectsDloadWithoutDistributedLoadObject) { - const auto deck = replaceOnce( - minimalDeck(), - "*Cload\nTip, 2, -1.\n", - "*Dload\nBeamSet, PY, -1.\n"); - auto result = mapText("dload", deck); + const auto deck = ReplaceOnce(MinimalDeck(), "*Cload\nTip, 2, -1.\n", + "*Dload\nBeamSet, PY, -1.\n"); + auto result = MapText("dload", deck); - ASSERT_FALSE(result.HasValue()); - EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); - const auto* diagnostic = findDiagnostic(result.GetStatus(), "unsupported-keyword"); - ASSERT_NE(diagnostic, nullptr); - EXPECT_EQ(diagnostic->keyword, "DLOAD"); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput); + const auto* diagnostic = + FindDiagnostic(result.GetStatus(), "unsupported-keyword"); + ASSERT_NE(diagnostic, nullptr); + EXPECT_EQ(diagnostic->keyword, "DLOAD"); } diff --git a/tests/unit/io/abaqus/input_reader_test.cpp b/tests/unit/io/abaqus/input_reader_test.cpp index bf66c25..8b05fa1 100644 --- a/tests/unit/io/abaqus/input_reader_test.cpp +++ b/tests/unit/io/abaqus/input_reader_test.cpp @@ -1,4 +1,4 @@ -#include "fesa/io/abaqus/input_reader.hpp" +#include "fesa/io/abaqus/input_reader.h" #include @@ -12,122 +12,112 @@ namespace { class TemporaryInputFile { -public: - TemporaryInputFile(const std::string& stem, const std::string& content) - : path_{std::filesystem::temp_directory_path() / - ("fesa-" + stem + ".inp")} { - std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; - stream.write(content.data(), static_cast(content.size())); - if (!stream) { - throw std::runtime_error{"Unable to create INP reader test fixture."}; - } + public: + TemporaryInputFile(const std::string& stem, const std::string& content) + : path_{std::filesystem::temp_directory_path() / + ("fesa-" + stem + ".inp")} { + std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; + stream.write(content.data(), static_cast(content.size())); + if (!stream) { + throw std::runtime_error{"Unable to create INP reader test fixture."}; } + } - ~TemporaryInputFile() { - std::error_code error; - std::filesystem::remove(path_, error); - } + ~TemporaryInputFile() { + std::error_code error; + std::filesystem::remove(path_, error); + } - const std::filesystem::path& path() const noexcept { - return path_; - } + const std::filesystem::path& Path() const noexcept { return path_; } -private: - std::filesystem::path path_; + private: + std::filesystem::path path_; }; -std::string readExactBytes(const std::filesystem::path& path) { - std::ifstream stream{path, std::ios::binary}; - if (!stream) { - throw std::runtime_error{"Unable to read legacy INP fixture."}; - } - return std::string{ - std::istreambuf_iterator{stream}, - std::istreambuf_iterator{}}; +std::string ReadExactBytes(const std::filesystem::path& path) { + std::ifstream stream{path, std::ios::binary}; + if (!stream) { + throw std::runtime_error{"Unable to read legacy INP fixture."}; + } + return std::string{std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; } -std::filesystem::path repositoryRoot() { - auto path = std::filesystem::path{__FILE__}.parent_path(); - for (int parent = 0; parent < 4; ++parent) { - path = path.parent_path(); - } - return path; +std::filesystem::path RepositoryRoot() { + auto path = std::filesystem::path{__FILE__}.parent_path(); + for (int parent = 0; parent < 4; ++parent) { + path = path.parent_path(); + } + return path; } -} // namespace +} // namespace TEST(InpSyntax, RejectsMalformedOrOrphanData) { - const auto missingPath = - std::filesystem::temp_directory_path() / "fesa-missing-input.inp"; - std::error_code removeError; - std::filesystem::remove(missingPath, removeError); + const auto missing_path = + std::filesystem::temp_directory_path() / "fesa-missing-input.inp"; + std::error_code remove_error; + std::filesystem::remove(missing_path, remove_error); - const auto unreadable = fesa::AbaqusInputReader{}.read(missingPath); - ASSERT_FALSE(unreadable.HasValue()); - EXPECT_EQ( - unreadable.GetStatus().Category(), - fesa::FailureCategory::kInput); - ASSERT_EQ(unreadable.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ(unreadable.GetStatus().Diagnostics()[0].code, - "input-file-unreadable"); + const auto unreadable = fesa::AbaqusInputReader{}.Read(missing_path); + ASSERT_FALSE(unreadable.HasValue()); + EXPECT_EQ(unreadable.GetStatus().Category(), fesa::FailureCategory::kInput); + ASSERT_EQ(unreadable.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(unreadable.GetStatus().Diagnostics()[0].code, + "input-file-unreadable"); - const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"}; - const auto malformedResult = - fesa::AbaqusInputReader{}.read(malformed.path()); - ASSERT_FALSE(malformedResult.HasValue()); - EXPECT_EQ( - malformedResult.GetStatus().Category(), - fesa::FailureCategory::kInput); - ASSERT_EQ(malformedResult.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ(malformedResult.GetStatus().Diagnostics()[0].code, - "malformed-keyword"); - EXPECT_EQ(malformedResult.GetStatus().Diagnostics()[0].location.line, 1U); + const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"}; + const auto malformed_result = + fesa::AbaqusInputReader{}.Read(malformed.Path()); + ASSERT_FALSE(malformed_result.HasValue()); + EXPECT_EQ(malformed_result.GetStatus().Category(), + fesa::FailureCategory::kInput); + ASSERT_EQ(malformed_result.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(malformed_result.GetStatus().Diagnostics()[0].code, + "malformed-keyword"); + EXPECT_EQ(malformed_result.GetStatus().Diagnostics()[0].location.line, 1U); - const TemporaryInputFile orphan{ - "orphan-data", "** comment\n\norphan, data\n"}; - const auto orphanResult = fesa::AbaqusInputReader{}.read(orphan.path()); - ASSERT_FALSE(orphanResult.HasValue()); - EXPECT_EQ( - orphanResult.GetStatus().Category(), - fesa::FailureCategory::kInput); - ASSERT_EQ(orphanResult.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ(orphanResult.GetStatus().Diagnostics()[0].code, - "orphan-data-line"); - EXPECT_EQ(orphanResult.GetStatus().Diagnostics()[0].location.line, 3U); + const TemporaryInputFile orphan{"orphan-data", + "** comment\n\norphan, data\n"}; + const auto orphan_result = fesa::AbaqusInputReader{}.Read(orphan.Path()); + ASSERT_FALSE(orphan_result.HasValue()); + EXPECT_EQ(orphan_result.GetStatus().Category(), + fesa::FailureCategory::kInput); + ASSERT_EQ(orphan_result.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(orphan_result.GetStatus().Diagnostics()[0].code, + "orphan-data-line"); + EXPECT_EQ(orphan_result.GetStatus().Diagnostics()[0].location.line, 3U); } TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) { - const auto inputPath = - repositoryRoot() / "reference" / "cantilever beam" / - "cantilever beam.inp"; - const auto bytesBefore = readExactBytes(inputPath); - const auto timestampBefore = std::filesystem::last_write_time(inputPath); + const auto input_path = RepositoryRoot() / "reference" / "cantilever beam" / + "cantilever beam.inp"; + const auto bytes_before = ReadExactBytes(input_path); + const auto timestamp_before = std::filesystem::last_write_time(input_path); - const auto result = fesa::AbaqusInputReader{}.read(inputPath); + const auto result = fesa::AbaqusInputReader{}.Read(input_path); - ASSERT_TRUE(result.HasValue()); - EXPECT_EQ(result.Value().sourceContentIdentity, - "fnv1a64:04543464cc970405"); - EXPECT_EQ(result.Value().sourcePath, - std::filesystem::absolute(inputPath).lexically_normal()); - ASSERT_EQ(result.Value().blocks.size(), 30U); - EXPECT_EQ(result.Value().blocks.front().canonicalName, "HEADING"); - EXPECT_EQ(result.Value().blocks.front().location.line, 1U); - EXPECT_EQ(result.Value().blocks.back().canonicalName, "END STEP"); + ASSERT_TRUE(result.HasValue()); + EXPECT_EQ(result.Value().source_content_identity, "fnv1a64:04543464cc970405"); + EXPECT_EQ(result.Value().source_path, + std::filesystem::absolute(input_path).lexically_normal()); + ASSERT_EQ(result.Value().blocks.size(), 30U); + EXPECT_EQ(result.Value().blocks.front().canonical_name, "HEADING"); + EXPECT_EQ(result.Value().blocks.front().location.line, 1U); + EXPECT_EQ(result.Value().blocks.back().canonical_name, "END STEP"); - const auto element = std::find_if( - result.Value().blocks.begin(), - result.Value().blocks.end(), - [](const fesa::KeywordBlock& block) { - return block.canonicalName == "ELEMENT"; - }); - ASSERT_NE(element, result.Value().blocks.end()); - ASSERT_EQ(element->parameters.size(), 1U); - EXPECT_EQ(element->parameters[0].name, "TYPE"); - ASSERT_TRUE(element->parameters[0].value.has_value()); - EXPECT_EQ(*element->parameters[0].value, "B33"); - EXPECT_EQ(element->data.size(), 10U); + const auto element = + std::find_if(result.Value().blocks.begin(), result.Value().blocks.end(), + [](const fesa::KeywordBlock& block) { + return block.canonical_name == "ELEMENT"; + }); + ASSERT_NE(element, result.Value().blocks.end()); + ASSERT_EQ(element->parameters.size(), 1U); + EXPECT_EQ(element->parameters[0].name, "TYPE"); + ASSERT_TRUE(element->parameters[0].value.has_value()); + EXPECT_EQ(*element->parameters[0].value, "B33"); + EXPECT_EQ(element->data.size(), 10U); - EXPECT_EQ(readExactBytes(inputPath), bytesBefore); - EXPECT_EQ(std::filesystem::last_write_time(inputPath), timestampBefore); + EXPECT_EQ(ReadExactBytes(input_path), bytes_before); + EXPECT_EQ(std::filesystem::last_write_time(input_path), timestamp_before); } diff --git a/tests/unit/io/abaqus/input_syntax_test.cpp b/tests/unit/io/abaqus/input_syntax_test.cpp index b01e233..717e7ea 100644 --- a/tests/unit/io/abaqus/input_syntax_test.cpp +++ b/tests/unit/io/abaqus/input_syntax_test.cpp @@ -1,5 +1,3 @@ -#include "fesa/io/abaqus/input_reader.hpp" - #include #include @@ -8,88 +6,84 @@ #include #include +#include "fesa/io/abaqus/input_reader.h" + namespace { class TemporaryInputFile { -public: - TemporaryInputFile(const std::string& stem, const std::string& content) - : path_{std::filesystem::temp_directory_path() / - ("fesa-" + stem + ".inp")} { - std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; - stream.write(content.data(), static_cast(content.size())); - if (!stream) { - throw std::runtime_error{"Unable to create INP syntax test fixture."}; - } + public: + TemporaryInputFile(const std::string& stem, const std::string& content) + : path_{std::filesystem::temp_directory_path() / + ("fesa-" + stem + ".inp")} { + std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; + stream.write(content.data(), static_cast(content.size())); + if (!stream) { + throw std::runtime_error{"Unable to create INP syntax test fixture."}; } + } - ~TemporaryInputFile() { - std::error_code error; - std::filesystem::remove(path_, error); - } + ~TemporaryInputFile() { + std::error_code error; + std::filesystem::remove(path_, error); + } - const std::filesystem::path& path() const noexcept { - return path_; - } + const std::filesystem::path& Path() const noexcept { return path_; } -private: - std::filesystem::path path_; + private: + std::filesystem::path path_; }; -} // namespace +} // namespace TEST(InpSyntax, CanonicalizesKeywordAndParameterNamesOnly) { - const std::string originalLine = - " *eLeMeNt, TyPe= b33 , generate, ELSET=Beam_Set "; - const TemporaryInputFile input{ - "canonical-names", originalLine + "\n"}; + const std::string original_line = + " *eLeMeNt, TyPe= b33 , generate, ELSET=Beam_Set "; + const TemporaryInputFile input{"canonical-names", original_line + "\n"}; - const auto result = fesa::AbaqusInputReader{}.read(input.path()); + const auto result = fesa::AbaqusInputReader{}.Read(input.Path()); - ASSERT_TRUE(result.HasValue()); - ASSERT_EQ(result.Value().blocks.size(), 1U); - const auto& block = result.Value().blocks[0]; - EXPECT_EQ(block.canonicalName, "ELEMENT"); - EXPECT_EQ(block.originalLine, originalLine); - EXPECT_EQ(block.location.line, 1U); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value().blocks.size(), 1U); + const auto& block = result.Value().blocks[0]; + EXPECT_EQ(block.canonical_name, "ELEMENT"); + EXPECT_EQ(block.original_line, original_line); + EXPECT_EQ(block.location.line, 1U); - ASSERT_EQ(block.parameters.size(), 3U); - EXPECT_EQ(block.parameters[0].name, "TYPE"); - ASSERT_TRUE(block.parameters[0].value.has_value()); - EXPECT_EQ(*block.parameters[0].value, "b33"); - EXPECT_EQ(block.parameters[1].name, "GENERATE"); - EXPECT_FALSE(block.parameters[1].value.has_value()); - EXPECT_EQ(block.parameters[2].name, "ELSET"); - ASSERT_TRUE(block.parameters[2].value.has_value()); - EXPECT_EQ(*block.parameters[2].value, "Beam_Set"); + ASSERT_EQ(block.parameters.size(), 3U); + EXPECT_EQ(block.parameters[0].name, "TYPE"); + ASSERT_TRUE(block.parameters[0].value.has_value()); + EXPECT_EQ(*block.parameters[0].value, "b33"); + EXPECT_EQ(block.parameters[1].name, "GENERATE"); + EXPECT_FALSE(block.parameters[1].value.has_value()); + EXPECT_EQ(block.parameters[2].name, "ELSET"); + ASSERT_TRUE(block.parameters[2].value.has_value()); + EXPECT_EQ(*block.parameters[2].value, "Beam_Set"); } TEST(InpSyntax, PreservesDataAndSourceLocations) { - const std::string exactBytes = - "** retained only in line accounting\r\n" - "\r\n" - "*NoDe\r\n" - " 0007, Label_A, ,\r\n"; - const TemporaryInputFile input{"data-and-locations", exactBytes}; + const std::string exact_bytes = + "** retained only in line accounting\r\n" + "\r\n" + "*NoDe\r\n" + " 0007, Label_A, ,\r\n"; + const TemporaryInputFile input{"data-and-locations", exact_bytes}; - const auto result = fesa::AbaqusInputReader{}.read(input.path()); + const auto result = fesa::AbaqusInputReader{}.Read(input.Path()); - ASSERT_TRUE(result.HasValue()); - EXPECT_EQ( - result.Value().sourcePath, - std::filesystem::absolute(input.path()).lexically_normal()); - EXPECT_EQ(result.Value().sourceContentIdentity, - "fnv1a64:c120b6ed2445be46"); - ASSERT_EQ(result.Value().blocks.size(), 1U); - const auto& block = result.Value().blocks[0]; - EXPECT_EQ(block.canonicalName, "NODE"); - EXPECT_EQ(block.originalLine, "*NoDe"); - EXPECT_EQ(block.location.file, result.Value().sourcePath); - EXPECT_EQ(block.location.line, 3U); + ASSERT_TRUE(result.HasValue()); + EXPECT_EQ(result.Value().source_path, + std::filesystem::absolute(input.Path()).lexically_normal()); + EXPECT_EQ(result.Value().source_content_identity, "fnv1a64:c120b6ed2445be46"); + ASSERT_EQ(result.Value().blocks.size(), 1U); + const auto& block = result.Value().blocks[0]; + EXPECT_EQ(block.canonical_name, "NODE"); + EXPECT_EQ(block.original_line, "*NoDe"); + EXPECT_EQ(block.location.file, result.Value().source_path); + EXPECT_EQ(block.location.line, 3U); - ASSERT_EQ(block.data.size(), 1U); - EXPECT_EQ( - block.data[0].fields, - (std::vector{"0007", "Label_A", "", ""})); - EXPECT_EQ(block.data[0].location.file, result.Value().sourcePath); - EXPECT_EQ(block.data[0].location.line, 4U); + ASSERT_EQ(block.data.size(), 1U); + EXPECT_EQ(block.data[0].fields, + (std::vector{"0007", "Label_A", "", ""})); + EXPECT_EQ(block.data[0].location.file, result.Value().source_path); + EXPECT_EQ(block.data[0].location.line, 4U); } diff --git a/tests/unit/io/hdf5/hdf5_results_writer_test.cpp b/tests/unit/io/hdf5/hdf5_results_writer_test.cpp index f3ed388..ab1e5d5 100644 --- a/tests/unit/io/hdf5/hdf5_results_writer_test.cpp +++ b/tests/unit/io/hdf5/hdf5_results_writer_test.cpp @@ -1,17 +1,9 @@ #define NOMINMAX +#include "fesa/io/hdf5/hdf5_results_writer.h" + #include - -#include "fesa/io/hdf5/hdf5_results_writer.hpp" - -#include "fesa/analysis/analysis_model.h" -#include "fesa/analysis/analysis_state.h" -#include "fesa/build_info.h" -#include "fesa/fem/dof_manager.h" -#include "fesa/model/domain.h" - -#include - #include +#include #include #include @@ -28,1347 +20,1288 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/analysis/analysis_state.h" +#include "fesa/build_info.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/model/domain.h" + namespace { constexpr const char* kStepRoot = "/steps/Step-1/frames/0"; class Hdf5Handle { -public: - using Closer = herr_t (*)(hid_t); + public: + using Closer = herr_t (*)(hid_t); - Hdf5Handle() = default; - Hdf5Handle(const hid_t value, Closer closer) : value_{value}, closer_{closer} {} - Hdf5Handle(const Hdf5Handle&) = delete; - Hdf5Handle& operator=(const Hdf5Handle&) = delete; - Hdf5Handle(Hdf5Handle&& other) noexcept - : value_{other.value_}, closer_{other.closer_} { - other.value_ = -1; - other.closer_ = nullptr; + Hdf5Handle() = default; + Hdf5Handle(const hid_t value, Closer closer) + : value_{value}, closer_{closer} {} + Hdf5Handle(const Hdf5Handle&) = delete; + Hdf5Handle& operator=(const Hdf5Handle&) = delete; + Hdf5Handle(Hdf5Handle&& other) noexcept + : value_{other.value_}, closer_{other.closer_} { + other.value_ = -1; + other.closer_ = nullptr; + } + Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { + if (this != &other) { + Reset(); + value_ = other.value_; + closer_ = other.closer_; + other.value_ = -1; + other.closer_ = nullptr; } - Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { - if (this != &other) { - reset(); - value_ = other.value_; - closer_ = other.closer_; - other.value_ = -1; - other.closer_ = nullptr; - } - return *this; + return *this; + } + ~Hdf5Handle() { Reset(); } + + hid_t Get() const noexcept { return value_; } + + private: + void Reset() noexcept { + if (value_ >= 0 && closer_ != nullptr) { + (void)closer_(value_); } - ~Hdf5Handle() { reset(); } + value_ = -1; + closer_ = nullptr; + } - hid_t get() const noexcept { return value_; } - -private: - void reset() noexcept { - if (value_ >= 0 && closer_ != nullptr) { - (void)closer_(value_); - } - value_ = -1; - closer_ = nullptr; - } - - hid_t value_{-1}; - Closer closer_{nullptr}; + hid_t value_{-1}; + Closer closer_{nullptr}; }; class WinHandle { -public: - explicit WinHandle(HANDLE value) : value_{value} {} - WinHandle(const WinHandle&) = delete; - WinHandle& operator=(const WinHandle&) = delete; - ~WinHandle() { - if (value_ != INVALID_HANDLE_VALUE) { - (void)CloseHandle(value_); - } + public: + explicit WinHandle(HANDLE value) : value_{value} {} + WinHandle(const WinHandle&) = delete; + WinHandle& operator=(const WinHandle&) = delete; + ~WinHandle() { + if (value_ != INVALID_HANDLE_VALUE) { + (void)CloseHandle(value_); } - HANDLE get() const noexcept { return value_; } + } + HANDLE Get() const noexcept { return value_; } -private: - HANDLE value_{INVALID_HANDLE_VALUE}; + private: + HANDLE value_{INVALID_HANDLE_VALUE}; }; class TempDirectory { -public: - explicit TempDirectory(const std::string& label) { - static std::atomic sequence{0U}; - path_ = std::filesystem::temp_directory_path() / - ("fesa-step23-" + label + "-" + - std::to_string(GetCurrentProcessId()) + "-" + - std::to_string(sequence.fetch_add(1U))); - std::error_code error; - if (!std::filesystem::create_directory(path_, error) || error) { - throw std::runtime_error{"Unable to create the Step 23 test directory."}; - } + public: + explicit TempDirectory(const std::string& label) { + static std::atomic sequence{0U}; + path_ = + std::filesystem::temp_directory_path() / + ("fesa-step23-" + label + "-" + std::to_string(GetCurrentProcessId()) + + "-" + std::to_string(sequence.fetch_add(1U))); + std::error_code error; + if (!std::filesystem::create_directory(path_, error) || error) { + throw std::runtime_error{"Unable to create the Step 23 test directory."}; } - TempDirectory(const TempDirectory&) = delete; - TempDirectory& operator=(const TempDirectory&) = delete; - ~TempDirectory() { - std::error_code ignored; - std::filesystem::remove_all(path_, ignored); - } - const std::filesystem::path& path() const noexcept { return path_; } + } + TempDirectory(const TempDirectory&) = delete; + TempDirectory& operator=(const TempDirectory&) = delete; + ~TempDirectory() { + std::error_code ignored; + std::filesystem::remove_all(path_, ignored); + } + const std::filesystem::path& Path() const noexcept { return path_; } -private: - std::filesystem::path path_; + private: + std::filesystem::path path_; }; struct WriterFixture { - std::unique_ptr domain; - std::unique_ptr dofs; - std::unique_ptr state; + std::unique_ptr domain; + std::unique_ptr dofs; + std::unique_ptr state; }; -fesa::ModelDefinition makeDefinition( - const std::filesystem::path& source, - const bool useDefaultCentroid) { - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:0123456789abcdef"; - definition.nodes = { - {{u8"Beam-\u03b1", 101, "101"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{u8"Beam-\u03b1", 202, "202"}, {3.0, 4.0, 0.0}, {source, 11U}}}; - definition.materials = { - {"Steel", 210.0e9, 0.3, {source, 20U}}}; - definition.sections = {{ - "General", - 0.02, - 3.0e-5, - 0.0, - 4.0e-5, - 5.0e-5, - {0.0, 0.0, 1.0}, - useDefaultCentroid - ? std::vector>{} - : std::vector>{{{-0.1, 0.2}, {0.3, -0.4}}}, - {source, 30U}}}; - definition.elements = {{ - {u8"Beam-\u03b1", 303, "303"}, - {0U, 1U}, - 0U, - 0U, - {source, 40U}}}; - definition.steps = {{ - "Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; - return definition; +fesa::ModelDefinition MakeDefinition(const std::filesystem::path& source, + const bool use_default_centroid) { + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:0123456789abcdef"; + definition.nodes = { + {{u8"Beam-\u03b1", 101, "101"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{u8"Beam-\u03b1", 202, "202"}, {3.0, 4.0, 0.0}, {source, 11U}}}; + definition.materials = {{"Steel", 210.0e9, 0.3, {source, 20U}}}; + definition.sections = { + {"General", + 0.02, + 3.0e-5, + 0.0, + 4.0e-5, + 5.0e-5, + {0.0, 0.0, 1.0}, + use_default_centroid + ? std::vector>{} + : std::vector>{{{-0.1, 0.2}, {0.3, -0.4}}}, + {source, 30U}}}; + definition.elements = { + {{u8"Beam-\u03b1", 303, "303"}, {0U, 1U}, 0U, 0U, {source, 40U}}}; + definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; + return definition; } -WriterFixture makeFixture( - const std::filesystem::path& source, - const bool useDefaultCentroid = false) { - auto domainResult = fesa::Domain::Create( - makeDefinition(source, useDefaultCentroid)); - if (!domainResult.HasValue()) { - throw std::runtime_error{"Writer fixture Domain construction failed."}; - } - auto domain = std::make_unique( - std::move(domainResult.Value())); +WriterFixture MakeFixture(const std::filesystem::path& source, + const bool use_default_centroid = false) { + auto domain_result = + fesa::Domain::Create(MakeDefinition(source, use_default_centroid)); + if (!domain_result.HasValue()) { + throw std::runtime_error{"Writer fixture Domain construction failed."}; + } + auto domain = + std::make_unique(std::move(domain_result.Value())); - auto modelResult = fesa::AnalysisModel::Create(*domain); - if (!modelResult.HasValue()) { - throw std::runtime_error{"Writer fixture AnalysisModel construction failed."}; - } - const fesa::AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = fesa::DofManager::Create(model); - if (!dofsResult.HasValue()) { - throw std::runtime_error{"Writer fixture DofManager construction failed."}; - } - auto dofs = std::make_unique( - std::move(dofsResult.Value())); - auto state = std::make_unique( - fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); + auto model_result = fesa::AnalysisModel::Create(*domain); + if (!model_result.HasValue()) { + throw std::runtime_error{ + "Writer fixture AnalysisModel construction failed."}; + } + const fesa::AnalysisModel model = std::move(model_result.Value()); + auto dofs_result = fesa::DofManager::Create(model); + if (!dofs_result.HasValue()) { + throw std::runtime_error{"Writer fixture DofManager construction failed."}; + } + auto dofs = + std::make_unique(std::move(dofs_result.Value())); + auto state = std::make_unique( + fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); - for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { - state->Displacement()[index] = 0.25 + static_cast(index); - state->ExternalForce()[index] = 100.0 + static_cast(index); - state->InternalForce()[index] = 200.0 + 2.0 * static_cast(index); - state->Residual()[index] = 100.0 + static_cast(index); - state->Reaction()[index] = 100.0 + static_cast(index); - } + for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { + state->Displacement()[index] = 0.25 + static_cast(index); + state->ExternalForce()[index] = 100.0 + static_cast(index); + state->InternalForce()[index] = 200.0 + 2.0 * static_cast(index); + state->Residual()[index] = 100.0 + static_cast(index); + state->Reaction()[index] = 100.0 + static_cast(index); + } - const auto& nodes = domain->Nodes(); - state->EndpointResults() = { + const auto& nodes = domain->Nodes(); + state->EndpointResults() = {{0U, + 0, + nodes[0U].source_id, + {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, + {11.0, 12.0, 13.0, 14.0}}, + {0U, + 1, + nodes[1U].source_id, + {7.0, 8.0, 9.0, 10.0, 11.0, 12.0}, + {15.0, 16.0, 17.0, 18.0}}}; + state->GaussResults() = { + {0U, 1, {0.01, 0.02, 0.03, 0.04}, {21.0, 22.0, 23.0, 24.0}}, + {0U, 2, {0.05, 0.06, 0.07, 0.08}, {25.0, 26.0, 27.0, 28.0}}}; + if (use_default_centroid) { + state->StressResults() = {{0U, 1, 0U, 0.0, 0.0, 31.0, "fesa-default"}, + {0U, 2, 0U, 0.0, 0.0, 32.0, "fesa-default"}}; + } else { + state->StressResults() = {{0U, 1, 1U, -0.1, 0.2, 31.0, "input"}, + {0U, 1, 2U, 0.3, -0.4, 32.0, "input"}, + {0U, 2, 1U, -0.1, 0.2, 33.0, "input"}, + {0U, 2, 2U, 0.3, -0.4, 34.0, "input"}}; + } + return {std::move(domain), std::move(dofs), std::move(state)}; +} + +fesa::ModelDefinition MakeShellDefinition(const std::filesystem::path& source) { + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:fedcba9876543210"; + definition.nodes = {{{"Shell-1", 11, "11"}, {-1.0, -1.0, 0.0}, {source, 10U}}, + {{"Shell-1", 12, "12"}, {1.0, -1.0, 0.0}, {source, 11U}}, + {{"Shell-1", 13, "13"}, {1.0, 1.0, 0.0}, {source, 12U}}, + {{"Shell-1", 14, "14"}, {-1.0, 1.0, 0.0}, {source, 13U}}}; + definition.materials = {{"ShellSteel", 210.0e9, 0.3, {source, 20U}}}; + definition.shell_sections = {{"PlateSet", 0.02, 0U, {source, 30U}}}; + definition.shell_elements = {{{"Shell-1", 401, "401"}, + fesa::ShellSourceElementType::kS4r, + {0U, 1U, 2U, 3U}, + 0U, + 0U, + {source, 40U}}}; + for (std::size_t node = 0U; node < definition.nodes.size(); ++node) { + definition.shell_node_initial_frames.push_back( + {static_cast(node), + {0.0, 0.0, 1.0}, + {1.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}}); + } + definition.node_sets = {{"Fixed", {}, {0U}, {source, 50U}}}; + definition.steps = {{"Step-1", + {{"Fixed", 1, 3, 0.0, {source, 60U}}, + {"Fixed", 4, 4, 0.125, {source, 61U}}}, + {}, + 0.1, + 1.0, + 0.01, + 1.0, + {source, 59U}}}; + return definition; +} + +WriterFixture MakeShellFixture(const std::filesystem::path& source) { + auto domain_result = fesa::Domain::Create(MakeShellDefinition(source)); + if (!domain_result.HasValue()) { + throw std::runtime_error{ + "Shell writer fixture Domain construction failed."}; + } + auto domain = + std::make_unique(std::move(domain_result.Value())); + auto model_result = fesa::AnalysisModel::Create(*domain); + if (!model_result.HasValue()) { + throw std::runtime_error{ + "Shell writer fixture AnalysisModel construction failed."}; + } + const fesa::AnalysisModel model = std::move(model_result.Value()); + auto dofs_result = fesa::DofManager::Create(model); + if (!dofs_result.HasValue()) { + throw std::runtime_error{ + "Shell writer fixture DofManager construction failed."}; + } + auto dofs = + std::make_unique(std::move(dofs_result.Value())); + auto state = std::make_unique( + fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); + for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { + state->Displacement()[index] = 0.01 * static_cast(index + 1U); + state->ExternalForce()[index] = 10.0 + static_cast(index); + state->InternalForce()[index] = 20.0 + static_cast(index); + state->Residual()[index] = 30.0 + static_cast(index); + state->Reaction()[index] = 40.0 + static_cast(index); + } + + const double gauss = 1.0 / std::sqrt(3.0); + const std::array, 4> coordinates{ + {{-gauss, -gauss}, {gauss, -gauss}, {gauss, gauss}, {-gauss, gauss}}}; + const std::array locations{ + fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2, + fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4}; + fesa::ShellStateCandidate candidate{}; + for (std::size_t point = 0U; point < locations.size(); ++point) { + const double base = 100.0 * static_cast(point + 1U); + candidate.rows.push_back( {0U, - 0, - nodes[0U].source_id, - {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, - {11.0, 12.0, 13.0, 14.0}}, - {0U, - 1, - nodes[1U].source_id, - {7.0, 8.0, 9.0, 10.0, 11.0, 12.0}, - {15.0, 16.0, 17.0, 18.0}}}; - state->GaussResults() = { - {0U, 1, {0.01, 0.02, 0.03, 0.04}, {21.0, 22.0, 23.0, 24.0}}, - {0U, 2, {0.05, 0.06, 0.07, 0.08}, {25.0, 26.0, 27.0, 28.0}}}; - if (useDefaultCentroid) { - state->StressResults() = { - {0U, 1, 0U, 0.0, 0.0, 31.0, "fesa-default"}, - {0U, 2, 0U, 0.0, 0.0, 32.0, "fesa-default"}}; - } else { - state->StressResults() = { - {0U, 1, 1U, -0.1, 0.2, 31.0, "input"}, - {0U, 1, 2U, 0.3, -0.4, 32.0, "input"}, - {0U, 2, 1U, -0.1, 0.2, 33.0, "input"}, - {0U, 2, 2U, 0.3, -0.4, 34.0, "input"}}; - } - return {std::move(domain), std::move(dofs), std::move(state)}; + locations[point], + coordinates[point], + {{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}, + {base + 1.0, base + 2.0, base + 3.0, base + 4.0, base + 5.0, + base + 6.0, base + 7.0, base + 8.0}, + {base + 11.0, base + 12.0, base + 13.0, base + 14.0, base + 15.0, + base + 16.0, base + 17.0, base + 18.0}, + {{{fesa::ShellSectionPosition::kBottom, + -1.0, + {base + 21.0, base + 22.0, base + 23.0}}, + {fesa::ShellSectionPosition::kMiddle, + 0.0, + {base + 24.0, base + 25.0, base + 26.0}}, + {fesa::ShellSectionPosition::kTop, + 1.0, + {base + 27.0, base + 28.0, base + 29.0}}}}}); + } + candidate.physical_strain_energy = 123.5; + candidate.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + candidate.verification_metrics = {1.0e-13, 2.0e-13, 3.0e-13}; + const fesa::Status commit = + state->CommitShellResults({0U}, std::move(candidate)); + if (!commit.IsOk()) { + throw std::runtime_error{"Shell writer fixture state commit failed."}; + } + return {std::move(domain), std::move(dofs), std::move(state)}; } -fesa::ModelDefinition makeShellDefinition(const std::filesystem::path& source) { - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:fedcba9876543210"; - definition.nodes = { - {{"Shell-1", 11, "11"}, {-1.0, -1.0, 0.0}, {source, 10U}}, - {{"Shell-1", 12, "12"}, {1.0, -1.0, 0.0}, {source, 11U}}, - {{"Shell-1", 13, "13"}, {1.0, 1.0, 0.0}, {source, 12U}}, - {{"Shell-1", 14, "14"}, {-1.0, 1.0, 0.0}, {source, 13U}}}; - definition.materials = { - {"ShellSteel", 210.0e9, 0.3, {source, 20U}}}; - definition.shell_sections = { - {"PlateSet", 0.02, 0U, {source, 30U}}}; - definition.shell_elements = {{ - {"Shell-1", 401, "401"}, - fesa::ShellSourceElementType::kS4r, - {0U, 1U, 2U, 3U}, - 0U, - 0U, - {source, 40U}}}; - for (std::size_t node = 0U; node < definition.nodes.size(); ++node) { - definition.shell_node_initial_frames.push_back({ - static_cast(node), - {0.0, 0.0, 1.0}, - {1.0, 0.0, 0.0}, - {0.0, 1.0, 0.0}}); - } - definition.node_sets = { - {"Fixed", {}, {0U}, {source, 50U}}}; - definition.steps = {{ - "Step-1", - {{"Fixed", 1, 3, 0.0, {source, 60U}}, - {"Fixed", 4, 4, 0.125, {source, 61U}}}, - {}, - 0.1, - 1.0, - 0.01, - 1.0, - {source, 59U}}}; - return definition; +Hdf5Handle OpenFile(const std::filesystem::path& path) { + const hid_t file = + H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); + if (file < 0) { + throw std::runtime_error{"Unable to open test HDF5 output."}; + } + return Hdf5Handle{file, H5Fclose}; } -WriterFixture makeShellFixture(const std::filesystem::path& source) { - auto domainResult = fesa::Domain::Create(makeShellDefinition(source)); - if (!domainResult.HasValue()) { - throw std::runtime_error{"Shell writer fixture Domain construction failed."}; - } - auto domain = std::make_unique( - std::move(domainResult.Value())); - auto modelResult = fesa::AnalysisModel::Create(*domain); - if (!modelResult.HasValue()) { - throw std::runtime_error{"Shell writer fixture AnalysisModel construction failed."}; - } - const fesa::AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = fesa::DofManager::Create(model); - if (!dofsResult.HasValue()) { - throw std::runtime_error{"Shell writer fixture DofManager construction failed."}; - } - auto dofs = std::make_unique( - std::move(dofsResult.Value())); - auto state = std::make_unique( - fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); - for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { - state->Displacement()[index] = 0.01 * static_cast(index + 1U); - state->ExternalForce()[index] = 10.0 + static_cast(index); - state->InternalForce()[index] = 20.0 + static_cast(index); - state->Residual()[index] = 30.0 + static_cast(index); - state->Reaction()[index] = 40.0 + static_cast(index); - } - - const double gauss = 1.0 / std::sqrt(3.0); - const std::array, 4> coordinates{{ - {-gauss, -gauss}, - {gauss, -gauss}, - {gauss, gauss}, - {-gauss, gauss}}}; - const std::array locations{ - fesa::ShellMidsurfaceLocation::kGp1, - fesa::ShellMidsurfaceLocation::kGp2, - fesa::ShellMidsurfaceLocation::kGp3, - fesa::ShellMidsurfaceLocation::kGp4}; - fesa::ShellStateCandidate candidate{}; - for (std::size_t point = 0U; point < locations.size(); ++point) { - const double base = 100.0 * static_cast(point + 1U); - candidate.rows.push_back({ - 0U, - locations[point], - coordinates[point], - {{{1.0, 0.0, 0.0}, - {0.0, 1.0, 0.0}, - {0.0, 0.0, 1.0}}}, - {base + 1.0, base + 2.0, base + 3.0, base + 4.0, - base + 5.0, base + 6.0, base + 7.0, base + 8.0}, - {base + 11.0, base + 12.0, base + 13.0, base + 14.0, - base + 15.0, base + 16.0, base + 17.0, base + 18.0}, - {{{fesa::ShellSectionPosition::kBottom, - -1.0, - {base + 21.0, base + 22.0, base + 23.0}}, - {fesa::ShellSectionPosition::kMiddle, - 0.0, - {base + 24.0, base + 25.0, base + 26.0}}, - {fesa::ShellSectionPosition::kTop, - 1.0, - {base + 27.0, base + 28.0, base + 29.0}}}}}); - } - candidate.physical_strain_energy = 123.5; - candidate.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; - candidate.verification_metrics = {1.0e-13, 2.0e-13, 3.0e-13}; - const fesa::Status commit = state->CommitShellResults( - {0U}, std::move(candidate)); - if (!commit.IsOk()) { - throw std::runtime_error{"Shell writer fixture state commit failed."}; - } - return {std::move(domain), std::move(dofs), std::move(state)}; +Hdf5Handle OpenDataset(const hid_t file, const std::string& path) { + const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT); + if (dataset < 0) { + throw std::runtime_error{"Unable to open expected HDF5 dataset: " + path}; + } + return Hdf5Handle{dataset, H5Dclose}; } -Hdf5Handle openFile(const std::filesystem::path& path) { - const hid_t file = H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - if (file < 0) { - throw std::runtime_error{"Unable to open test HDF5 output."}; - } - return Hdf5Handle{file, H5Fclose}; +std::vector DatasetDimensions(const hid_t file, + const std::string& path) { + const auto dataset = OpenDataset(file, path); + Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; + if (space.Get() < 0) { + throw std::runtime_error{"Unable to inspect HDF5 dataspace."}; + } + const int rank = H5Sget_simple_extent_ndims(space.Get()); + if (rank < 0) { + throw std::runtime_error{"Unable to inspect HDF5 rank."}; + } + std::vector dimensions(static_cast(rank)); + if (rank > 0 && + H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0) { + throw std::runtime_error{"Unable to inspect HDF5 dimensions."}; + } + return dimensions; } -Hdf5Handle openDataset(const hid_t file, const std::string& path) { - const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT); - if (dataset < 0) { - throw std::runtime_error{"Unable to open expected HDF5 dataset: " + path}; - } - return Hdf5Handle{dataset, H5Dclose}; +std::vector ReadDoubleDataset(const hid_t file, + const std::string& path) { + const auto dimensions = DatasetDimensions(file, path); + std::size_t value_count = 1U; + for (const hsize_t dimension : dimensions) { + value_count *= static_cast(dimension); + } + const auto dataset = OpenDataset(file, path); + std::vector values(value_count); + if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, + H5S_ALL, H5P_DEFAULT, values.data()) < 0) { + throw std::runtime_error{"Unable to read numeric HDF5 dataset."}; + } + return values; } -std::vector datasetDimensions( - const hid_t file, const std::string& path) { - const auto dataset = openDataset(file, path); - Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; - if (space.get() < 0) { - throw std::runtime_error{"Unable to inspect HDF5 dataspace."}; - } - const int rank = H5Sget_simple_extent_ndims(space.get()); - if (rank < 0) { - throw std::runtime_error{"Unable to inspect HDF5 rank."}; - } - std::vector dimensions(static_cast(rank)); - if (rank > 0 && - H5Sget_simple_extent_dims(space.get(), dimensions.data(), nullptr) < 0) { - throw std::runtime_error{"Unable to inspect HDF5 dimensions."}; - } - return dimensions; +std::vector ReadUint8Dataset(const hid_t file, + const std::string& path) { + const auto dimensions = DatasetDimensions(file, path); + std::size_t value_count = 1U; + for (const hsize_t dimension : dimensions) { + value_count *= static_cast(dimension); + } + const auto dataset = OpenDataset(file, path); + Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; + if (type.Get() < 0 || H5Tget_class(type.Get()) != H5T_INTEGER || + H5Tget_size(type.Get()) != sizeof(std::uint8_t) || + H5Tget_sign(type.Get()) != H5T_SGN_NONE || + H5Tequal(type.Get(), H5T_STD_U8LE) <= 0) { + throw std::runtime_error{"Expected a portable uint8 HDF5 dataset."}; + } + std::vector values(value_count); + if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_UINT8, H5S_ALL, + H5S_ALL, H5P_DEFAULT, values.data()) < 0) { + throw std::runtime_error{"Unable to read uint8 HDF5 dataset."}; + } + return values; } -std::vector readDoubleDataset( - const hid_t file, const std::string& path) { - const auto dimensions = datasetDimensions(file, path); - std::size_t valueCount = 1U; - for (const hsize_t dimension : dimensions) { - valueCount *= static_cast(dimension); - } - const auto dataset = openDataset(file, path); - std::vector values(valueCount); - if (!values.empty() && - H5Dread(dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()) < 0) { - throw std::runtime_error{"Unable to read numeric HDF5 dataset."}; - } - return values; +std::string ReadStringAttribute(const hid_t object, const char* name) { + Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; + Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; + if (attribute.Get() < 0 || type.Get() < 0 || + H5Tget_class(type.Get()) != H5T_STRING || + H5Tis_variable_str(type.Get()) <= 0 || + H5Tget_cset(type.Get()) != H5T_CSET_UTF8) { + throw std::runtime_error{"Expected a variable-length UTF-8 attribute."}; + } + char* raw = nullptr; + if (H5Aread(attribute.Get(), type.Get(), &raw) < 0 || raw == nullptr) { + throw std::runtime_error{"Unable to read UTF-8 HDF5 attribute."}; + } + const std::string value{raw}; + (void)H5free_memory(raw); + return value; } -std::vector readUint8Dataset( - const hid_t file, const std::string& path) { - const auto dimensions = datasetDimensions(file, path); - std::size_t valueCount = 1U; - for (const hsize_t dimension : dimensions) { - valueCount *= static_cast(dimension); - } - const auto dataset = openDataset(file, path); - Hdf5Handle type{H5Dget_type(dataset.get()), H5Tclose}; - if (type.get() < 0 || H5Tget_class(type.get()) != H5T_INTEGER || - H5Tget_size(type.get()) != sizeof(std::uint8_t) || - H5Tget_sign(type.get()) != H5T_SGN_NONE || - H5Tequal(type.get(), H5T_STD_U8LE) <= 0) { - throw std::runtime_error{"Expected a portable uint8 HDF5 dataset."}; - } - std::vector values(valueCount); - if (!values.empty() && - H5Dread(dataset.get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, - H5P_DEFAULT, values.data()) < 0) { - throw std::runtime_error{"Unable to read uint8 HDF5 dataset."}; - } - return values; +std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) { + Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; + Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; + if (attribute.Get() < 0 || type.Get() < 0 || + H5Tget_class(type.Get()) != H5T_INTEGER || + H5Tget_size(type.Get()) != sizeof(std::uint64_t) || + H5Tget_sign(type.Get()) != H5T_SGN_NONE || + H5Tequal(type.Get(), H5T_STD_U64LE) <= 0) { + throw std::runtime_error{"Expected a portable uint64 HDF5 attribute."}; + } + std::uint64_t value = 0U; + if (H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value) < 0) { + throw std::runtime_error{"Unable to read uint64 HDF5 attribute."}; + } + return value; } -std::string readStringAttribute(const hid_t object, const char* name) { - Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; - Hdf5Handle type{H5Aget_type(attribute.get()), H5Tclose}; - if (attribute.get() < 0 || type.get() < 0 || - H5Tget_class(type.get()) != H5T_STRING || - H5Tis_variable_str(type.get()) <= 0 || - H5Tget_cset(type.get()) != H5T_CSET_UTF8) { - throw std::runtime_error{"Expected a variable-length UTF-8 attribute."}; - } - char* raw = nullptr; - if (H5Aread(attribute.get(), type.get(), &raw) < 0 || raw == nullptr) { - throw std::runtime_error{"Unable to read UTF-8 HDF5 attribute."}; - } - const std::string value{raw}; - (void)H5free_memory(raw); - return value; +void ExpectPortableCompoundMember(const hid_t compound_type, + const unsigned index, + const std::string& name) { + Hdf5Handle member_type{H5Tget_member_type(compound_type, index), H5Tclose}; + ASSERT_GE(member_type.Get(), 0); + const bool is_uint64 = name == "internal_node_id" || + name == "internal_element_id" || + name == "gauss_point_index" || + name == "section_point_index" || name == "line"; + const bool is_float64 = name == "x1" || name == "x2" || name == "S11"; + const bool is_string = + name == "instance_name" || name == "source_label" || name == "source" || + name == "severity" || name == "code" || name == "file" || + name == "keyword" || name == "entity_identity" || name == "message"; + if (is_uint64) { + EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_INTEGER); + EXPECT_EQ(H5Tget_size(member_type.Get()), sizeof(std::uint64_t)); + EXPECT_EQ(H5Tget_sign(member_type.Get()), H5T_SGN_NONE); + EXPECT_GT(H5Tequal(member_type.Get(), H5T_STD_U64LE), 0); + return; + } + if (is_float64) { + EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_FLOAT); + EXPECT_EQ(H5Tget_size(member_type.Get()), sizeof(double)); + EXPECT_GT(H5Tequal(member_type.Get(), H5T_IEEE_F64LE), 0); + return; + } + if (is_string) { + EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_STRING); + EXPECT_GT(H5Tis_variable_str(member_type.Get()), 0); + EXPECT_EQ(H5Tget_cset(member_type.Get()), H5T_CSET_UTF8); + return; + } + + ASSERT_EQ(H5Tget_class(member_type.Get()), H5T_ARRAY); + const int rank = H5Tget_array_ndims(member_type.Get()); + ASSERT_GT(rank, 0); + std::vector dimensions(static_cast(rank)); + ASSERT_GE(H5Tget_array_dims2(member_type.Get(), dimensions.data()), 0); + Hdf5Handle base_type{H5Tget_super(member_type.Get()), H5Tclose}; + ASSERT_GE(base_type.Get(), 0); + if (name == "node_internal_ids") { + EXPECT_EQ(dimensions, std::vector({2U})); + EXPECT_GT(H5Tequal(base_type.Get(), H5T_STD_U64LE), 0); + } else if (name == "coordinates") { + EXPECT_EQ(dimensions, std::vector({3U})); + EXPECT_GT(H5Tequal(base_type.Get(), H5T_IEEE_F64LE), 0); + } else { + EXPECT_EQ(name, "local_axes"); + EXPECT_EQ(dimensions, std::vector({3U, 3U})); + EXPECT_GT(H5Tequal(base_type.Get(), H5T_IEEE_F64LE), 0); + } } -std::uint64_t readUint64Attribute(const hid_t object, const char* name) { - Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; - Hdf5Handle type{H5Aget_type(attribute.get()), H5Tclose}; - if (attribute.get() < 0 || type.get() < 0 || - H5Tget_class(type.get()) != H5T_INTEGER || - H5Tget_size(type.get()) != sizeof(std::uint64_t) || - H5Tget_sign(type.get()) != H5T_SGN_NONE || - H5Tequal(type.get(), H5T_STD_U64LE) <= 0) { - throw std::runtime_error{"Expected a portable uint64 HDF5 attribute."}; - } - std::uint64_t value = 0U; - if (H5Aread(attribute.get(), H5T_NATIVE_UINT64, &value) < 0) { - throw std::runtime_error{"Unable to read uint64 HDF5 attribute."}; - } - return value; +void ExpectCompoundMembers(const hid_t file, const std::string& path, + const std::vector& expected_names) { + const auto dataset = OpenDataset(file, path); + Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; + ASSERT_EQ(H5Tget_class(type.Get()), H5T_COMPOUND); + ASSERT_EQ(H5Tget_nmembers(type.Get()), + static_cast(expected_names.size())); + for (std::size_t index = 0U; index < expected_names.size(); ++index) { + char* raw_name = + H5Tget_member_name(type.Get(), static_cast(index)); + ASSERT_NE(raw_name, nullptr); + const std::string actual_name{raw_name}; + (void)H5free_memory(raw_name); + EXPECT_EQ(actual_name, expected_names[index]); + ExpectPortableCompoundMember(type.Get(), static_cast(index), + expected_names[index]); + } } -void expectPortableCompoundMember( - const hid_t compoundType, - const unsigned index, - const std::string& name) { - Hdf5Handle memberType{ - H5Tget_member_type(compoundType, index), H5Tclose}; - ASSERT_GE(memberType.get(), 0); - const bool isUint64 = - name == "internal_node_id" || name == "internal_element_id" || - name == "gauss_point_index" || name == "section_point_index" || - name == "line"; - const bool isFloat64 = name == "x1" || name == "x2" || name == "S11"; - const bool isString = - name == "instance_name" || name == "source_label" || - name == "source" || name == "severity" || name == "code" || - name == "file" || name == "keyword" || - name == "entity_identity" || name == "message"; - if (isUint64) { - EXPECT_EQ(H5Tget_class(memberType.get()), H5T_INTEGER); - EXPECT_EQ(H5Tget_size(memberType.get()), sizeof(std::uint64_t)); - EXPECT_EQ(H5Tget_sign(memberType.get()), H5T_SGN_NONE); - EXPECT_GT(H5Tequal(memberType.get(), H5T_STD_U64LE), 0); - return; - } - if (isFloat64) { - EXPECT_EQ(H5Tget_class(memberType.get()), H5T_FLOAT); - EXPECT_EQ(H5Tget_size(memberType.get()), sizeof(double)); - EXPECT_GT(H5Tequal(memberType.get(), H5T_IEEE_F64LE), 0); - return; - } - if (isString) { - EXPECT_EQ(H5Tget_class(memberType.get()), H5T_STRING); - EXPECT_GT(H5Tis_variable_str(memberType.get()), 0); - EXPECT_EQ(H5Tget_cset(memberType.get()), H5T_CSET_UTF8); - return; - } - - ASSERT_EQ(H5Tget_class(memberType.get()), H5T_ARRAY); - const int rank = H5Tget_array_ndims(memberType.get()); - ASSERT_GT(rank, 0); - std::vector dimensions(static_cast(rank)); - ASSERT_GE(H5Tget_array_dims2(memberType.get(), dimensions.data()), 0); - Hdf5Handle baseType{H5Tget_super(memberType.get()), H5Tclose}; - ASSERT_GE(baseType.get(), 0); - if (name == "node_internal_ids") { - EXPECT_EQ(dimensions, std::vector({2U})); - EXPECT_GT(H5Tequal(baseType.get(), H5T_STD_U64LE), 0); - } else if (name == "coordinates") { - EXPECT_EQ(dimensions, std::vector({3U})); - EXPECT_GT(H5Tequal(baseType.get(), H5T_IEEE_F64LE), 0); - } else { - EXPECT_EQ(name, "local_axes"); - EXPECT_EQ(dimensions, std::vector({3U, 3U})); - EXPECT_GT(H5Tequal(baseType.get(), H5T_IEEE_F64LE), 0); - } +void ExpectCompoundMemberNames(const hid_t file, const std::string& path, + const std::vector& expected_names) { + const auto dataset = OpenDataset(file, path); + Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; + ASSERT_EQ(H5Tget_class(type.Get()), H5T_COMPOUND); + ASSERT_EQ(H5Tget_nmembers(type.Get()), + static_cast(expected_names.size())); + for (std::size_t index = 0U; index < expected_names.size(); ++index) { + char* raw_name = + H5Tget_member_name(type.Get(), static_cast(index)); + ASSERT_NE(raw_name, nullptr); + const std::string actual_name{raw_name}; + (void)H5free_memory(raw_name); + EXPECT_EQ(actual_name, expected_names[index]); + } } -void expectCompoundMembers( - const hid_t file, - const std::string& path, - const std::vector& expectedNames) { - const auto dataset = openDataset(file, path); - Hdf5Handle type{H5Dget_type(dataset.get()), H5Tclose}; - ASSERT_EQ(H5Tget_class(type.get()), H5T_COMPOUND); - ASSERT_EQ( - H5Tget_nmembers(type.get()), static_cast(expectedNames.size())); - for (std::size_t index = 0U; index < expectedNames.size(); ++index) { - char* rawName = H5Tget_member_name(type.get(), - static_cast(index)); - ASSERT_NE(rawName, nullptr); - const std::string actualName{rawName}; - (void)H5free_memory(rawName); - EXPECT_EQ(actualName, expectedNames[index]); - expectPortableCompoundMember( - type.get(), static_cast(index), expectedNames[index]); - } +void ExpectNumericDataset(const hid_t file, const std::string& path, + const std::vector& dimensions, + const std::string& components, + const std::string& units, + const std::string& coordinate_system, + const std::string& location) { + EXPECT_EQ(DatasetDimensions(file, path), dimensions); + const auto dataset = OpenDataset(file, path); + Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; + ASSERT_EQ(H5Tget_class(type.Get()), H5T_FLOAT); + EXPECT_EQ(H5Tget_size(type.Get()), 8U); + EXPECT_GT(H5Tequal(type.Get(), H5T_IEEE_F64LE), 0); + EXPECT_EQ(ReadStringAttribute(dataset.Get(), "component_names"), components); + EXPECT_EQ(ReadStringAttribute(dataset.Get(), "component_unit_dimensions"), + units); + EXPECT_EQ(ReadStringAttribute(dataset.Get(), "coordinate_system"), + coordinate_system); + EXPECT_EQ(ReadStringAttribute(dataset.Get(), "location"), location); + EXPECT_EQ(ReadStringAttribute(dataset.Get(), "step_name"), "Step-1"); + EXPECT_EQ(ReadUint64Attribute(dataset.Get(), "frame_index"), 0U); } -void expectCompoundMemberNames( - const hid_t file, - const std::string& path, - const std::vector& expectedNames) { - const auto dataset = openDataset(file, path); - Hdf5Handle type{H5Dget_type(dataset.get()), H5Tclose}; - ASSERT_EQ(H5Tget_class(type.get()), H5T_COMPOUND); - ASSERT_EQ( - H5Tget_nmembers(type.get()), static_cast(expectedNames.size())); - for (std::size_t index = 0U; index < expectedNames.size(); ++index) { - char* rawName = H5Tget_member_name( - type.get(), static_cast(index)); - ASSERT_NE(rawName, nullptr); - const std::string actualName{rawName}; - (void)H5free_memory(rawName); - EXPECT_EQ(actualName, expectedNames[index]); - } -} - -void expectNumericDataset( - const hid_t file, - const std::string& path, - const std::vector& dimensions, - const std::string& components, - const std::string& units, - const std::string& coordinateSystem, - const std::string& location) { - EXPECT_EQ(datasetDimensions(file, path), dimensions); - const auto dataset = openDataset(file, path); - Hdf5Handle type{H5Dget_type(dataset.get()), H5Tclose}; - ASSERT_EQ(H5Tget_class(type.get()), H5T_FLOAT); - EXPECT_EQ(H5Tget_size(type.get()), 8U); - EXPECT_GT(H5Tequal(type.get(), H5T_IEEE_F64LE), 0); - EXPECT_EQ(readStringAttribute(dataset.get(), "component_names"), components); - EXPECT_EQ( - readStringAttribute(dataset.get(), "component_unit_dimensions"), units); - EXPECT_EQ( - readStringAttribute(dataset.get(), "coordinate_system"), coordinateSystem); - EXPECT_EQ(readStringAttribute(dataset.get(), "location"), location); - EXPECT_EQ(readStringAttribute(dataset.get(), "step_name"), "Step-1"); - EXPECT_EQ(readUint64Attribute(dataset.get(), "frame_index"), 0U); -} - -Hdf5Handle makeUtf8StringType() { - Hdf5Handle type{H5Tcopy(H5T_C_S1), H5Tclose}; - if (type.get() < 0 || H5Tset_size(type.get(), H5T_VARIABLE) < 0 || - H5Tset_cset(type.get(), H5T_CSET_UTF8) < 0) { - throw std::runtime_error{"Unable to create a test UTF-8 memory type."}; - } - return type; +Hdf5Handle MakeUtf8StringType() { + Hdf5Handle type{H5Tcopy(H5T_C_S1), H5Tclose}; + if (type.Get() < 0 || H5Tset_size(type.Get(), H5T_VARIABLE) < 0 || + H5Tset_cset(type.Get(), H5T_CSET_UTF8) < 0) { + throw std::runtime_error{"Unable to create a test UTF-8 memory type."}; + } + return type; } struct NodeReadRow { - std::uint64_t internalNodeId; - char* instanceName; - char* sourceLabel; - double coordinates[3]; + std::uint64_t internal_node_id; + char* instance_name; + char* source_label; + double coordinates[3]; }; -std::vector readNodeRows(const hid_t file) { - const auto dataset = openDataset(file, "/model/nodes"); - Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; - auto stringType = makeUtf8StringType(); - const hsize_t coordinateDimensions[] = {3U}; - Hdf5Handle coordinatesType{ - H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), H5Tclose}; - Hdf5Handle memoryType{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), H5Tclose}; - if (H5Tinsert(memoryType.get(), "internal_node_id", - HOFFSET(NodeReadRow, internalNodeId), H5T_NATIVE_UINT64) < 0 || - H5Tinsert(memoryType.get(), "instance_name", - HOFFSET(NodeReadRow, instanceName), stringType.get()) < 0 || - H5Tinsert(memoryType.get(), "source_label", - HOFFSET(NodeReadRow, sourceLabel), stringType.get()) < 0 || - H5Tinsert(memoryType.get(), "coordinates", - HOFFSET(NodeReadRow, coordinates), coordinatesType.get()) < 0) { - throw std::runtime_error{"Unable to create the node memory type."}; - } - std::vector rows(2U); - if (H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, - H5P_DEFAULT, rows.data()) < 0) { - throw std::runtime_error{"Unable to read node rows."}; - } - return rows; +std::vector ReadNodeRows(const hid_t file) { + const auto dataset = OpenDataset(file, "/model/nodes"); + Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; + auto string_type = MakeUtf8StringType(); + const hsize_t coordinate_dimensions[] = {3U}; + Hdf5Handle coordinates_type{ + H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), H5Tclose}; + Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), + H5Tclose}; + if (H5Tinsert(memory_type.Get(), "internal_node_id", + HOFFSET(NodeReadRow, internal_node_id), + H5T_NATIVE_UINT64) < 0 || + H5Tinsert(memory_type.Get(), "instance_name", + HOFFSET(NodeReadRow, instance_name), string_type.Get()) < 0 || + H5Tinsert(memory_type.Get(), "source_label", + HOFFSET(NodeReadRow, source_label), string_type.Get()) < 0 || + H5Tinsert(memory_type.Get(), "coordinates", + HOFFSET(NodeReadRow, coordinates), + coordinates_type.Get()) < 0) { + throw std::runtime_error{"Unable to create the node memory type."}; + } + std::vector rows(2U); + if (H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, + rows.data()) < 0) { + throw std::runtime_error{"Unable to read node rows."}; + } + return rows; } -void reclaimNodeRows(const hid_t file, std::vector& rows) { - const auto dataset = openDataset(file, "/model/nodes"); - Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; - auto stringType = makeUtf8StringType(); - const hsize_t coordinateDimensions[] = {3U}; - Hdf5Handle coordinatesType{ - H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), H5Tclose}; - Hdf5Handle memoryType{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), H5Tclose}; - (void)H5Tinsert(memoryType.get(), "internal_node_id", - HOFFSET(NodeReadRow, internalNodeId), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "instance_name", - HOFFSET(NodeReadRow, instanceName), stringType.get()); - (void)H5Tinsert(memoryType.get(), "source_label", - HOFFSET(NodeReadRow, sourceLabel), stringType.get()); - (void)H5Tinsert(memoryType.get(), "coordinates", - HOFFSET(NodeReadRow, coordinates), coordinatesType.get()); - (void)H5Dvlen_reclaim( - memoryType.get(), space.get(), H5P_DEFAULT, rows.data()); +void ReclaimNodeRows(const hid_t file, std::vector& rows) { + const auto dataset = OpenDataset(file, "/model/nodes"); + Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; + auto string_type = MakeUtf8StringType(); + const hsize_t coordinate_dimensions[] = {3U}; + Hdf5Handle coordinates_type{ + H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), H5Tclose}; + Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), + H5Tclose}; + (void)H5Tinsert(memory_type.Get(), "internal_node_id", + HOFFSET(NodeReadRow, internal_node_id), H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "instance_name", + HOFFSET(NodeReadRow, instance_name), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "source_label", + HOFFSET(NodeReadRow, source_label), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "coordinates", + HOFFSET(NodeReadRow, coordinates), coordinates_type.Get()); + (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, + rows.data()); } struct ElementReadRow { - std::uint64_t internalElementId; - char* instanceName; - char* sourceLabel; - std::uint64_t nodeInternalIds[2]; - double localAxes[9]; + std::uint64_t internal_element_id; + char* instance_name; + char* source_label; + std::uint64_t node_internal_ids[2]; + double local_axes[9]; }; -std::vector readElementRows(const hid_t file) { - const auto dataset = openDataset(file, "/model/elements"); - auto stringType = makeUtf8StringType(); - const hsize_t nodeDimensions[] = {2U}; - const hsize_t axesDimensions[] = {3U, 3U}; - Hdf5Handle nodeType{ - H5Tarray_create2(H5T_NATIVE_UINT64, 1, nodeDimensions), H5Tclose}; - Hdf5Handle axesType{ - H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axesDimensions), H5Tclose}; - Hdf5Handle memoryType{ - H5Tcreate(H5T_COMPOUND, sizeof(ElementReadRow)), H5Tclose}; - (void)H5Tinsert(memoryType.get(), "internal_element_id", - HOFFSET(ElementReadRow, internalElementId), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "instance_name", - HOFFSET(ElementReadRow, instanceName), stringType.get()); - (void)H5Tinsert(memoryType.get(), "source_label", - HOFFSET(ElementReadRow, sourceLabel), stringType.get()); - (void)H5Tinsert(memoryType.get(), "node_internal_ids", - HOFFSET(ElementReadRow, nodeInternalIds), nodeType.get()); - (void)H5Tinsert(memoryType.get(), "local_axes", - HOFFSET(ElementReadRow, localAxes), axesType.get()); - std::vector rows(1U); - if (H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, - H5P_DEFAULT, rows.data()) < 0) { - throw std::runtime_error{"Unable to read element rows."}; - } - Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; - EXPECT_EQ(rows[0U].internalElementId, 0U); - EXPECT_STREQ(rows[0U].instanceName, u8"Beam-\u03b1"); - EXPECT_STREQ(rows[0U].sourceLabel, "303"); - EXPECT_EQ(rows[0U].nodeInternalIds[0U], 0U); - EXPECT_EQ(rows[0U].nodeInternalIds[1U], 1U); - const std::array expectedAxes = { - 0.6, 0.8, 0.0, - 0.0, 0.0, 1.0, - 0.8, -0.6, 0.0}; - for (std::size_t index = 0U; index < expectedAxes.size(); ++index) { - EXPECT_NEAR(rows[0U].localAxes[index], expectedAxes[index], 1.0e-15); - } - (void)H5Dvlen_reclaim( - memoryType.get(), space.get(), H5P_DEFAULT, rows.data()); - return rows; +std::vector ReadElementRows(const hid_t file) { + const auto dataset = OpenDataset(file, "/model/elements"); + auto string_type = MakeUtf8StringType(); + const hsize_t node_dimensions[] = {2U}; + const hsize_t axes_dimensions[] = {3U, 3U}; + Hdf5Handle node_type{H5Tarray_create2(H5T_NATIVE_UINT64, 1, node_dimensions), + H5Tclose}; + Hdf5Handle axes_type{H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axes_dimensions), + H5Tclose}; + Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(ElementReadRow)), + H5Tclose}; + (void)H5Tinsert(memory_type.Get(), "internal_element_id", + HOFFSET(ElementReadRow, internal_element_id), + H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "instance_name", + HOFFSET(ElementReadRow, instance_name), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "source_label", + HOFFSET(ElementReadRow, source_label), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "node_internal_ids", + HOFFSET(ElementReadRow, node_internal_ids), node_type.Get()); + (void)H5Tinsert(memory_type.Get(), "local_axes", + HOFFSET(ElementReadRow, local_axes), axes_type.Get()); + std::vector rows(1U); + if (H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, + rows.data()) < 0) { + throw std::runtime_error{"Unable to read element rows."}; + } + Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; + EXPECT_EQ(rows[0U].internal_element_id, 0U); + EXPECT_STREQ(rows[0U].instance_name, u8"Beam-\u03b1"); + EXPECT_STREQ(rows[0U].source_label, "303"); + EXPECT_EQ(rows[0U].node_internal_ids[0U], 0U); + EXPECT_EQ(rows[0U].node_internal_ids[1U], 1U); + const std::array expected_axes = {0.6, 0.8, 0.0, 0.0, 0.0, + 1.0, 0.8, -0.6, 0.0}; + for (std::size_t index = 0U; index < expected_axes.size(); ++index) { + EXPECT_NEAR(rows[0U].local_axes[index], expected_axes[index], 1.0e-15); + } + (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, + rows.data()); + return rows; } struct StressReadRow { - std::uint64_t internalElementId; - std::uint64_t gaussPointIndex; - std::uint64_t sectionPointIndex; - double x1; - double x2; - char* source; - double s11; + std::uint64_t internal_element_id; + std::uint64_t gauss_point_index; + std::uint64_t section_point_index; + double x1; + double x2; + char* source; + double s11; }; -std::vector readStressRows(const hid_t file) { - const std::string path = std::string{kStepRoot} + "/element/stress_s11"; - const auto dataset = openDataset(file, path); - const auto dimensions = datasetDimensions(file, path); - auto stringType = makeUtf8StringType(); - Hdf5Handle memoryType{ - H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), H5Tclose}; - (void)H5Tinsert(memoryType.get(), "internal_element_id", - HOFFSET(StressReadRow, internalElementId), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "gauss_point_index", - HOFFSET(StressReadRow, gaussPointIndex), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "section_point_index", - HOFFSET(StressReadRow, sectionPointIndex), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "x1", - HOFFSET(StressReadRow, x1), H5T_NATIVE_DOUBLE); - (void)H5Tinsert(memoryType.get(), "x2", - HOFFSET(StressReadRow, x2), H5T_NATIVE_DOUBLE); - (void)H5Tinsert(memoryType.get(), "source", - HOFFSET(StressReadRow, source), stringType.get()); - (void)H5Tinsert(memoryType.get(), "S11", - HOFFSET(StressReadRow, s11), H5T_NATIVE_DOUBLE); - std::vector rows( - dimensions.empty() ? 0U : static_cast(dimensions[0U])); - if (!rows.empty() && - H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, - H5P_DEFAULT, rows.data()) < 0) { - throw std::runtime_error{"Unable to read stress rows."}; - } - return rows; +std::vector ReadStressRows(const hid_t file) { + const std::string path = std::string{kStepRoot} + "/element/stress_s11"; + const auto dataset = OpenDataset(file, path); + const auto dimensions = DatasetDimensions(file, path); + auto string_type = MakeUtf8StringType(); + Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), + H5Tclose}; + (void)H5Tinsert(memory_type.Get(), "internal_element_id", + HOFFSET(StressReadRow, internal_element_id), + H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "gauss_point_index", + HOFFSET(StressReadRow, gauss_point_index), H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "section_point_index", + HOFFSET(StressReadRow, section_point_index), + H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "x1", HOFFSET(StressReadRow, x1), + H5T_NATIVE_DOUBLE); + (void)H5Tinsert(memory_type.Get(), "x2", HOFFSET(StressReadRow, x2), + H5T_NATIVE_DOUBLE); + (void)H5Tinsert(memory_type.Get(), "source", HOFFSET(StressReadRow, source), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressReadRow, s11), + H5T_NATIVE_DOUBLE); + std::vector rows( + dimensions.empty() ? 0U : static_cast(dimensions[0U])); + if (!rows.empty() && H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, + H5S_ALL, H5P_DEFAULT, rows.data()) < 0) { + throw std::runtime_error{"Unable to read stress rows."}; + } + return rows; } -void reclaimStressRows(const hid_t file, std::vector& rows) { - const std::string path = std::string{kStepRoot} + "/element/stress_s11"; - const auto dataset = openDataset(file, path); - Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; - auto stringType = makeUtf8StringType(); - Hdf5Handle memoryType{ - H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), H5Tclose}; - (void)H5Tinsert(memoryType.get(), "internal_element_id", - HOFFSET(StressReadRow, internalElementId), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "gauss_point_index", - HOFFSET(StressReadRow, gaussPointIndex), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "section_point_index", - HOFFSET(StressReadRow, sectionPointIndex), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "x1", HOFFSET(StressReadRow, x1), H5T_NATIVE_DOUBLE); - (void)H5Tinsert(memoryType.get(), "x2", HOFFSET(StressReadRow, x2), H5T_NATIVE_DOUBLE); - (void)H5Tinsert(memoryType.get(), "source", - HOFFSET(StressReadRow, source), stringType.get()); - (void)H5Tinsert(memoryType.get(), "S11", HOFFSET(StressReadRow, s11), H5T_NATIVE_DOUBLE); - if (!rows.empty()) { - (void)H5Dvlen_reclaim( - memoryType.get(), space.get(), H5P_DEFAULT, rows.data()); - } +void ReclaimStressRows(const hid_t file, std::vector& rows) { + const std::string path = std::string{kStepRoot} + "/element/stress_s11"; + const auto dataset = OpenDataset(file, path); + Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; + auto string_type = MakeUtf8StringType(); + Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), + H5Tclose}; + (void)H5Tinsert(memory_type.Get(), "internal_element_id", + HOFFSET(StressReadRow, internal_element_id), + H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "gauss_point_index", + HOFFSET(StressReadRow, gauss_point_index), H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "section_point_index", + HOFFSET(StressReadRow, section_point_index), + H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "x1", HOFFSET(StressReadRow, x1), + H5T_NATIVE_DOUBLE); + (void)H5Tinsert(memory_type.Get(), "x2", HOFFSET(StressReadRow, x2), + H5T_NATIVE_DOUBLE); + (void)H5Tinsert(memory_type.Get(), "source", HOFFSET(StressReadRow, source), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressReadRow, s11), + H5T_NATIVE_DOUBLE); + if (!rows.empty()) { + (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, + rows.data()); + } } struct DiagnosticReadRow { - char* severity; - char* code; - char* file; - std::uint64_t line; - char* keyword; - char* entityIdentity; - char* message; + char* severity; + char* code; + char* file; + std::uint64_t line; + char* keyword; + char* entity_identity; + char* message; }; -std::vector readDiagnosticRows(const hid_t file) { - const auto dataset = openDataset(file, "/diagnostics"); - const auto dimensions = datasetDimensions(file, "/diagnostics"); - auto stringType = makeUtf8StringType(); - Hdf5Handle memoryType{ - H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), H5Tclose}; - (void)H5Tinsert(memoryType.get(), "severity", - HOFFSET(DiagnosticReadRow, severity), stringType.get()); - (void)H5Tinsert(memoryType.get(), "code", - HOFFSET(DiagnosticReadRow, code), stringType.get()); - (void)H5Tinsert(memoryType.get(), "file", - HOFFSET(DiagnosticReadRow, file), stringType.get()); - (void)H5Tinsert(memoryType.get(), "line", - HOFFSET(DiagnosticReadRow, line), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "keyword", - HOFFSET(DiagnosticReadRow, keyword), stringType.get()); - (void)H5Tinsert(memoryType.get(), "entity_identity", - HOFFSET(DiagnosticReadRow, entityIdentity), stringType.get()); - (void)H5Tinsert(memoryType.get(), "message", - HOFFSET(DiagnosticReadRow, message), stringType.get()); - std::vector rows( - dimensions.empty() ? 0U : static_cast(dimensions[0U])); - if (!rows.empty() && - H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, - H5P_DEFAULT, rows.data()) < 0) { - throw std::runtime_error{"Unable to read diagnostic rows."}; - } - return rows; +std::vector ReadDiagnosticRows(const hid_t file) { + const auto dataset = OpenDataset(file, "/diagnostics"); + const auto dimensions = DatasetDimensions(file, "/diagnostics"); + auto string_type = MakeUtf8StringType(); + Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), + H5Tclose}; + (void)H5Tinsert(memory_type.Get(), "severity", + HOFFSET(DiagnosticReadRow, severity), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "code", HOFFSET(DiagnosticReadRow, code), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "file", HOFFSET(DiagnosticReadRow, file), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "line", HOFFSET(DiagnosticReadRow, line), + H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "keyword", + HOFFSET(DiagnosticReadRow, keyword), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "entity_identity", + HOFFSET(DiagnosticReadRow, entity_identity), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "message", + HOFFSET(DiagnosticReadRow, message), string_type.Get()); + std::vector rows( + dimensions.empty() ? 0U : static_cast(dimensions[0U])); + if (!rows.empty() && H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, + H5S_ALL, H5P_DEFAULT, rows.data()) < 0) { + throw std::runtime_error{"Unable to read diagnostic rows."}; + } + return rows; } -void reclaimDiagnosticRows( - const hid_t file, std::vector& rows) { - const auto dataset = openDataset(file, "/diagnostics"); - Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; - auto stringType = makeUtf8StringType(); - Hdf5Handle memoryType{ - H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), H5Tclose}; - (void)H5Tinsert(memoryType.get(), "severity", - HOFFSET(DiagnosticReadRow, severity), stringType.get()); - (void)H5Tinsert(memoryType.get(), "code", - HOFFSET(DiagnosticReadRow, code), stringType.get()); - (void)H5Tinsert(memoryType.get(), "file", - HOFFSET(DiagnosticReadRow, file), stringType.get()); - (void)H5Tinsert(memoryType.get(), "line", - HOFFSET(DiagnosticReadRow, line), H5T_NATIVE_UINT64); - (void)H5Tinsert(memoryType.get(), "keyword", - HOFFSET(DiagnosticReadRow, keyword), stringType.get()); - (void)H5Tinsert(memoryType.get(), "entity_identity", - HOFFSET(DiagnosticReadRow, entityIdentity), stringType.get()); - (void)H5Tinsert(memoryType.get(), "message", - HOFFSET(DiagnosticReadRow, message), stringType.get()); - if (!rows.empty()) { - (void)H5Dvlen_reclaim( - memoryType.get(), space.get(), H5P_DEFAULT, rows.data()); - } +void ReclaimDiagnosticRows(const hid_t file, + std::vector& rows) { + const auto dataset = OpenDataset(file, "/diagnostics"); + Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; + auto string_type = MakeUtf8StringType(); + Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), + H5Tclose}; + (void)H5Tinsert(memory_type.Get(), "severity", + HOFFSET(DiagnosticReadRow, severity), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "code", HOFFSET(DiagnosticReadRow, code), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "file", HOFFSET(DiagnosticReadRow, file), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "line", HOFFSET(DiagnosticReadRow, line), + H5T_NATIVE_UINT64); + (void)H5Tinsert(memory_type.Get(), "keyword", + HOFFSET(DiagnosticReadRow, keyword), string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "entity_identity", + HOFFSET(DiagnosticReadRow, entity_identity), + string_type.Get()); + (void)H5Tinsert(memory_type.Get(), "message", + HOFFSET(DiagnosticReadRow, message), string_type.Get()); + if (!rows.empty()) { + (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, + rows.data()); + } } -std::vector readBytes(const std::filesystem::path& path) { - std::ifstream input{path, std::ios::binary}; - return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; +std::vector ReadBytes(const std::filesystem::path& path) { + std::ifstream input{path, std::ios::binary}; + return {std::istreambuf_iterator{input}, + std::istreambuf_iterator{}}; } -void writeBytes(const std::filesystem::path& path, const std::vector& bytes) { - std::ofstream output{path, std::ios::binary | std::ios::trunc}; - output.write(bytes.data(), static_cast(bytes.size())); - if (!output) { - throw std::runtime_error{"Unable to write atomicity sentinel bytes."}; - } +void WriteBytes(const std::filesystem::path& path, + const std::vector& bytes) { + std::ofstream output{path, std::ios::binary | std::ios::trunc}; + output.write(bytes.data(), static_cast(bytes.size())); + if (!output) { + throw std::runtime_error{"Unable to write atomicity sentinel bytes."}; + } } -std::size_t entryCount(const std::filesystem::path& directory) { - return static_cast( - std::distance(std::filesystem::directory_iterator{directory}, - std::filesystem::directory_iterator{})); +std::size_t EntryCount(const std::filesystem::path& directory) { + return static_cast( + std::distance(std::filesystem::directory_iterator{directory}, + std::filesystem::directory_iterator{})); } -void expectOutputFailure( - const fesa::Status& status, const std::string& expectedCode) { - ASSERT_FALSE(status.IsOk()); - EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); - ASSERT_EQ(status.Diagnostics().size(), 1U); - EXPECT_EQ(status.Diagnostics()[0U].severity, fesa::Severity::kError); - EXPECT_EQ(status.Diagnostics()[0U].code, expectedCode); +void ExpectOutputFailure(const fesa::Status& status, + const std::string& expected_code) { + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0U].severity, fesa::Severity::kError); + EXPECT_EQ(status.Diagnostics()[0U].code, expected_code); } -} // namespace +} // namespace TEST(Hdf5ResultsWriter, WritesExactSchemaShapesAttributesAndIdentity) { - TempDirectory directory{"schema"}; - const auto source = directory.path() / "model.inp"; - auto fixture = makeFixture(source); - const auto output = directory.path() / "results.h5"; + TempDirectory directory{"schema"}; + const auto source = directory.Path() / "model.inp"; + auto fixture = MakeFixture(source); + const auto output = directory.Path() / "results.h5"; - fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); - ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); + fesa::Hdf5ResultsWriter writer; + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); + ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); - const auto file = openFile(output); - for (const char* path : { - "/metadata", - "/model/nodes", - "/model/elements", - "/steps/Step-1/frames/0/nodal/displacement", - "/steps/Step-1/frames/0/nodal/reaction", - "/steps/Step-1/frames/0/element/end_force_local", - "/steps/Step-1/frames/0/element/section_resultant", - "/steps/Step-1/frames/0/element/generalized_strain", - "/steps/Step-1/frames/0/element/generalized_resultant", - "/steps/Step-1/frames/0/element/stress_s11", - "/diagnostics"}) { - EXPECT_GT(H5Lexists(file.get(), path, H5P_DEFAULT), 0) << path; - } + const auto file = OpenFile(output); + for (const char* path : + {"/metadata", "/model/nodes", "/model/elements", + "/steps/Step-1/frames/0/nodal/displacement", + "/steps/Step-1/frames/0/nodal/reaction", + "/steps/Step-1/frames/0/element/end_force_local", + "/steps/Step-1/frames/0/element/section_resultant", + "/steps/Step-1/frames/0/element/generalized_strain", + "/steps/Step-1/frames/0/element/generalized_resultant", + "/steps/Step-1/frames/0/element/stress_s11", "/diagnostics"}) { + EXPECT_GT(H5Lexists(file.Get(), path, H5P_DEFAULT), 0) << path; + } - Hdf5Handle metadata{ - H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose}; - ASSERT_GE(metadata.get(), 0); - EXPECT_EQ(readUint64Attribute(metadata.get(), "schema_version"), 0U); - EXPECT_EQ( - readStringAttribute(metadata.get(), "feature_id"), - "linear-static-3d-euler-beam"); - EXPECT_EQ( - readStringAttribute(metadata.get(), "solver_version"), - std::string{fesa::SolverVersion()}); - const std::string normalizedSource = - std::filesystem::absolute(source).lexically_normal().generic_u8string(); - EXPECT_EQ( - readStringAttribute(metadata.get(), "source_input_identity"), - "path=" + normalizedSource + - ";content_identity=fnv1a64:0123456789abcdef"); - EXPECT_EQ( - readStringAttribute(metadata.get(), "unit_system_label"), - "user-consistent-unspecified"); - EXPECT_EQ( - readStringAttribute(metadata.get(), "coordinate_convention"), - "global-cartesian; beam-local=(t,n1,t-cross-n1)"); - EXPECT_EQ( - readStringAttribute(metadata.get(), "element_formulation"), - "B33-3D-Euler-Bernoulli"); - EXPECT_EQ(readStringAttribute(metadata.get(), "step_name"), "Step-1"); - EXPECT_EQ(readUint64Attribute(metadata.get(), "frame_index"), 0U); + Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; + ASSERT_GE(metadata.Get(), 0); + EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), + "linear-static-3d-euler-beam"); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "solver_version"), + std::string{fesa::SolverVersion()}); + const std::string normalized_source = + std::filesystem::absolute(source).lexically_normal().generic_u8string(); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity"), + "path=" + normalized_source + + ";content_identity=fnv1a64:0123456789abcdef"); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "unit_system_label"), + "user-consistent-unspecified"); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "coordinate_convention"), + "global-cartesian; beam-local=(t,n1,t-cross-n1)"); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "element_formulation"), + "B33-3D-Euler-Bernoulli"); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "step_name"), "Step-1"); + EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "frame_index"), 0U); - EXPECT_EQ(datasetDimensions(file.get(), "/model/nodes"), - std::vector({2U})); - expectCompoundMembers( - file.get(), "/model/nodes", - {"internal_node_id", "instance_name", "source_label", "coordinates"}); - const auto nodesDataset = openDataset(file.get(), "/model/nodes"); - EXPECT_EQ( - readStringAttribute(nodesDataset.get(), "coordinate_system"), - "global-cartesian"); - EXPECT_EQ(readStringAttribute(nodesDataset.get(), "units_label"), "length"); - auto nodes = readNodeRows(file.get()); - ASSERT_EQ(nodes.size(), 2U); - EXPECT_EQ(nodes[0U].internalNodeId, 0U); - EXPECT_STREQ(nodes[0U].instanceName, u8"Beam-\u03b1"); - EXPECT_STREQ(nodes[0U].sourceLabel, "101"); - EXPECT_DOUBLE_EQ(nodes[1U].coordinates[0U], 3.0); - EXPECT_DOUBLE_EQ(nodes[1U].coordinates[1U], 4.0); - reclaimNodeRows(file.get(), nodes); + EXPECT_EQ(DatasetDimensions(file.Get(), "/model/nodes"), + std::vector({2U})); + ExpectCompoundMembers( + file.Get(), "/model/nodes", + {"internal_node_id", "instance_name", "source_label", "coordinates"}); + const auto nodes_dataset = OpenDataset(file.Get(), "/model/nodes"); + EXPECT_EQ(ReadStringAttribute(nodes_dataset.Get(), "coordinate_system"), + "global-cartesian"); + EXPECT_EQ(ReadStringAttribute(nodes_dataset.Get(), "units_label"), "length"); + auto nodes = ReadNodeRows(file.Get()); + ASSERT_EQ(nodes.size(), 2U); + EXPECT_EQ(nodes[0U].internal_node_id, 0U); + EXPECT_STREQ(nodes[0U].instance_name, u8"Beam-\u03b1"); + EXPECT_STREQ(nodes[0U].source_label, "101"); + EXPECT_DOUBLE_EQ(nodes[1U].coordinates[0U], 3.0); + EXPECT_DOUBLE_EQ(nodes[1U].coordinates[1U], 4.0); + ReclaimNodeRows(file.Get(), nodes); - EXPECT_EQ(datasetDimensions(file.get(), "/model/elements"), - std::vector({1U})); - expectCompoundMembers( - file.get(), "/model/elements", - {"internal_element_id", "instance_name", "source_label", - "node_internal_ids", "local_axes"}); - const auto elementsDataset = openDataset(file.get(), "/model/elements"); - EXPECT_EQ( - readStringAttribute(elementsDataset.get(), "formulation"), - "B33-3D-Euler-Bernoulli"); - (void)readElementRows(file.get()); + EXPECT_EQ(DatasetDimensions(file.Get(), "/model/elements"), + std::vector({1U})); + ExpectCompoundMembers(file.Get(), "/model/elements", + {"internal_element_id", "instance_name", "source_label", + "node_internal_ids", "local_axes"}); + const auto elements_dataset = OpenDataset(file.Get(), "/model/elements"); + EXPECT_EQ(ReadStringAttribute(elements_dataset.Get(), "formulation"), + "B33-3D-Euler-Bernoulli"); + (void)ReadElementRows(file.Get()); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/nodal/displacement", {2U, 6U}, - "UX,UY,UZ,URX,URY,URZ", - "length,length,length,radian,radian,radian", - "global-cartesian", "nodal"); - const auto displacement = readDoubleDataset( - file.get(), std::string{kStepRoot} + "/nodal/displacement"); - ASSERT_EQ(displacement.size(), 12U); - EXPECT_DOUBLE_EQ(displacement.front(), 0.25); - EXPECT_DOUBLE_EQ(displacement.back(), 11.25); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/nodal/reaction", {2U, 6U}, - "RF1,RF2,RF3,RM1,RM2,RM3", - "force,force,force,force*length,force*length,force*length", - "global-cartesian", "nodal"); - const auto reaction = readDoubleDataset( - file.get(), std::string{kStepRoot} + "/nodal/reaction"); - ASSERT_EQ(reaction.size(), 12U); - EXPECT_DOUBLE_EQ(reaction.front(), 100.0); - EXPECT_DOUBLE_EQ(reaction.back(), 111.0); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/element/end_force_local", - {1U, 2U, 6U}, "FX,FY,FZ,MX,MY,MZ", - "force,force,force,force*length,force*length,force*length", - "beam-local", "endpoint-outward-action"); - const auto endForce = readDoubleDataset( - file.get(), std::string{kStepRoot} + "/element/end_force_local"); - ASSERT_EQ(endForce.size(), 12U); - EXPECT_DOUBLE_EQ(endForce.front(), 1.0); - EXPECT_DOUBLE_EQ(endForce.back(), 12.0); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/element/section_resultant", - {1U, 2U, 4U}, "N,T,My,Mz", - "force,force*length,force*length,force*length", - "beam-local", "endpoint-positive-local-x-section-cut"); - const auto sectionResultant = readDoubleDataset( - file.get(), std::string{kStepRoot} + "/element/section_resultant"); - ASSERT_EQ(sectionResultant.size(), 8U); - EXPECT_DOUBLE_EQ(sectionResultant.front(), 11.0); - EXPECT_DOUBLE_EQ(sectionResultant.back(), 18.0); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/element/generalized_strain", - {1U, 2U, 4U}, "epsilon0,kappa_x,kappa_y,kappa_z", - "1,1/length,1/length,1/length", - "beam-local", "integration-point"); - const auto generalizedStrain = readDoubleDataset( - file.get(), std::string{kStepRoot} + "/element/generalized_strain"); - ASSERT_EQ(generalizedStrain.size(), 8U); - EXPECT_DOUBLE_EQ(generalizedStrain.front(), 0.01); - EXPECT_DOUBLE_EQ(generalizedStrain.back(), 0.08); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/element/generalized_resultant", - {1U, 2U, 4U}, "N,T,My,Mz", - "force,force*length,force*length,force*length", - "beam-local", "integration-point"); - const auto generalizedResultant = readDoubleDataset( - file.get(), std::string{kStepRoot} + "/element/generalized_resultant"); - ASSERT_EQ(generalizedResultant.size(), 8U); - EXPECT_DOUBLE_EQ(generalizedResultant.front(), 21.0); - EXPECT_DOUBLE_EQ(generalizedResultant.back(), 28.0); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/nodal/displacement", {2U, 6U}, + "UX,UY,UZ,URX,URY,URZ", "length,length,length,radian,radian,radian", + "global-cartesian", "nodal"); + const auto displacement = ReadDoubleDataset( + file.Get(), std::string{kStepRoot} + "/nodal/displacement"); + ASSERT_EQ(displacement.size(), 12U); + EXPECT_DOUBLE_EQ(displacement.front(), 0.25); + EXPECT_DOUBLE_EQ(displacement.back(), 11.25); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/nodal/reaction", {2U, 6U}, + "RF1,RF2,RF3,RM1,RM2,RM3", + "force,force,force,force*length,force*length,force*length", + "global-cartesian", "nodal"); + const auto reaction = + ReadDoubleDataset(file.Get(), std::string{kStepRoot} + "/nodal/reaction"); + ASSERT_EQ(reaction.size(), 12U); + EXPECT_DOUBLE_EQ(reaction.front(), 100.0); + EXPECT_DOUBLE_EQ(reaction.back(), 111.0); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/element/end_force_local", + {1U, 2U, 6U}, "FX,FY,FZ,MX,MY,MZ", + "force,force,force,force*length,force*length,force*length", "beam-local", + "endpoint-outward-action"); + const auto end_force = ReadDoubleDataset( + file.Get(), std::string{kStepRoot} + "/element/end_force_local"); + ASSERT_EQ(end_force.size(), 12U); + EXPECT_DOUBLE_EQ(end_force.front(), 1.0); + EXPECT_DOUBLE_EQ(end_force.back(), 12.0); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/element/section_resultant", + {1U, 2U, 4U}, "N,T,My,Mz", "force,force*length,force*length,force*length", + "beam-local", "endpoint-positive-local-x-section-cut"); + const auto section_resultant = ReadDoubleDataset( + file.Get(), std::string{kStepRoot} + "/element/section_resultant"); + ASSERT_EQ(section_resultant.size(), 8U); + EXPECT_DOUBLE_EQ(section_resultant.front(), 11.0); + EXPECT_DOUBLE_EQ(section_resultant.back(), 18.0); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/element/generalized_strain", + {1U, 2U, 4U}, "epsilon0,kappa_x,kappa_y,kappa_z", + "1,1/length,1/length,1/length", "beam-local", "integration-point"); + const auto generalized_strain = ReadDoubleDataset( + file.Get(), std::string{kStepRoot} + "/element/generalized_strain"); + ASSERT_EQ(generalized_strain.size(), 8U); + EXPECT_DOUBLE_EQ(generalized_strain.front(), 0.01); + EXPECT_DOUBLE_EQ(generalized_strain.back(), 0.08); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/element/generalized_resultant", + {1U, 2U, 4U}, "N,T,My,Mz", "force,force*length,force*length,force*length", + "beam-local", "integration-point"); + const auto generalized_resultant = ReadDoubleDataset( + file.Get(), std::string{kStepRoot} + "/element/generalized_resultant"); + ASSERT_EQ(generalized_resultant.size(), 8U); + EXPECT_DOUBLE_EQ(generalized_resultant.front(), 21.0); + EXPECT_DOUBLE_EQ(generalized_resultant.back(), 28.0); - const std::string stressPath = std::string{kStepRoot} + "/element/stress_s11"; - EXPECT_EQ(datasetDimensions(file.get(), stressPath), - std::vector({4U})); - expectCompoundMembers( - file.get(), stressPath, - {"internal_element_id", "gauss_point_index", "section_point_index", - "x1", "x2", "source", "S11"}); - const auto stressDataset = openDataset(file.get(), stressPath); - EXPECT_EQ(readStringAttribute(stressDataset.get(), "component_names"), "S11"); - EXPECT_EQ( - readStringAttribute(stressDataset.get(), "component_unit_dimensions"), - "force/length^2"); - EXPECT_EQ( - readStringAttribute(stressDataset.get(), "coordinate_system"), - "beam-local"); - EXPECT_EQ(readStringAttribute(stressDataset.get(), "location"), "section-point"); - EXPECT_EQ(readStringAttribute(stressDataset.get(), "step_name"), "Step-1"); - EXPECT_EQ(readUint64Attribute(stressDataset.get(), "frame_index"), 0U); - auto stressRows = readStressRows(file.get()); - ASSERT_EQ(stressRows.size(), 4U); - EXPECT_EQ(stressRows[0U].internalElementId, 0U); - EXPECT_EQ(stressRows[0U].gaussPointIndex, 1U); - EXPECT_EQ(stressRows[0U].sectionPointIndex, 1U); - EXPECT_DOUBLE_EQ(stressRows[0U].x1, -0.1); - EXPECT_DOUBLE_EQ(stressRows[0U].x2, 0.2); - EXPECT_STREQ(stressRows[0U].source, "input"); - EXPECT_DOUBLE_EQ(stressRows[3U].s11, 34.0); - reclaimStressRows(file.get(), stressRows); + const std::string stress_path = + std::string{kStepRoot} + "/element/stress_s11"; + EXPECT_EQ(DatasetDimensions(file.Get(), stress_path), + std::vector({4U})); + ExpectCompoundMembers(file.Get(), stress_path, + {"internal_element_id", "gauss_point_index", + "section_point_index", "x1", "x2", "source", "S11"}); + const auto stress_dataset = OpenDataset(file.Get(), stress_path); + EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "component_names"), + "S11"); + EXPECT_EQ( + ReadStringAttribute(stress_dataset.Get(), "component_unit_dimensions"), + "force/length^2"); + EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "coordinate_system"), + "beam-local"); + EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "location"), + "section-point"); + EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "step_name"), "Step-1"); + EXPECT_EQ(ReadUint64Attribute(stress_dataset.Get(), "frame_index"), 0U); + auto stress_rows = ReadStressRows(file.Get()); + ASSERT_EQ(stress_rows.size(), 4U); + EXPECT_EQ(stress_rows[0U].internal_element_id, 0U); + EXPECT_EQ(stress_rows[0U].gauss_point_index, 1U); + EXPECT_EQ(stress_rows[0U].section_point_index, 1U); + EXPECT_DOUBLE_EQ(stress_rows[0U].x1, -0.1); + EXPECT_DOUBLE_EQ(stress_rows[0U].x2, 0.2); + EXPECT_STREQ(stress_rows[0U].source, "input"); + EXPECT_DOUBLE_EQ(stress_rows[3U].s11, 34.0); + ReclaimStressRows(file.Get(), stress_rows); - EXPECT_EQ(datasetDimensions(file.get(), "/diagnostics"), - std::vector({0U})); - expectCompoundMembers( - file.get(), "/diagnostics", - {"severity", "code", "file", "line", "keyword", - "entity_identity", "message"}); - EXPECT_EQ( - H5Lexists(file.get(), - "/steps/Step-1/frames/0/element/transverse_shear_stress", - H5P_DEFAULT), - 0); + EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"), + std::vector({0U})); + ExpectCompoundMembers(file.Get(), "/diagnostics", + {"severity", "code", "file", "line", "keyword", + "entity_identity", "message"}); + EXPECT_EQ(H5Lexists(file.Get(), + "/steps/Step-1/frames/0/element/transverse_shear_stress", + H5P_DEFAULT), + 0); } TEST(Hdf5ResultsWriter, WritesMandatoryOutputsDespiteOutputRequests) { - TempDirectory directory{"mandatory"}; - auto fixture = makeFixture(directory.path() / "request-model.inp"); - const fesa::Diagnostic ignoredRequest{ - fesa::Severity::kWarning, - "ignored-output-request", - {fixture.domain->SourcePath(), 70U}, - "*OUTPUT", - "FIELD", - "Abaqus output requests do not filter FESA mandatory results."}; - const auto output = directory.path() / "results.h5"; + TempDirectory directory{"mandatory"}; + auto fixture = MakeFixture(directory.Path() / "request-model.inp"); + const fesa::Diagnostic ignored_request{ + fesa::Severity::kWarning, + "ignored-output-request", + {fixture.domain->SourcePath(), 70U}, + "*OUTPUT", + "FIELD", + "Abaqus output requests do not filter FESA mandatory results."}; + const auto output = directory.Path() / "results.h5"; - fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE( - writer.Write(output, *fixture.domain, *fixture.state, {ignoredRequest}) - .IsOk()); - const auto file = openFile(output); - for (const char* suffix : { - "/nodal/displacement", - "/nodal/reaction", - "/element/end_force_local", - "/element/section_resultant", - "/element/generalized_strain", - "/element/generalized_resultant", - "/element/stress_s11"}) { - const std::string path = std::string{kStepRoot} + suffix; - EXPECT_GT(H5Lexists(file.get(), path.c_str(), H5P_DEFAULT), 0) << path; - } - EXPECT_EQ(datasetDimensions(file.get(), "/diagnostics"), - std::vector({1U})); + fesa::Hdf5ResultsWriter writer; + ASSERT_TRUE( + writer.Write(output, *fixture.domain, *fixture.state, {ignored_request}) + .IsOk()); + const auto file = OpenFile(output); + for (const char* suffix : + {"/nodal/displacement", "/nodal/reaction", "/element/end_force_local", + "/element/section_resultant", "/element/generalized_strain", + "/element/generalized_resultant", "/element/stress_s11"}) { + const std::string path = std::string{kStepRoot} + suffix; + EXPECT_GT(H5Lexists(file.Get(), path.c_str(), H5P_DEFAULT), 0) << path; + } + EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"), + std::vector({1U})); } TEST(Hdf5ResultsWriter, WritesWarningsAndDefaultCentroid) { - TempDirectory directory{"warnings"}; - auto fixture = makeFixture(directory.path() / "centroid.inp", true); - std::vector diagnostics = { - {fesa::Severity::kWarning, - "ignored-output-request", - {fixture.domain->SourcePath(), 80U}, - "*OUTPUT", - "FIELD", - "Ignored output request."}, - {fesa::Severity::kWarning, - "ignored-keyword", - {fixture.domain->SourcePath(), 20U}, - "*PREPRINT", - "", - "Ignored generator control."}}; - const auto output = directory.path() / "results.h5"; + TempDirectory directory{"warnings"}; + auto fixture = MakeFixture(directory.Path() / "centroid.inp", true); + std::vector diagnostics = { + {fesa::Severity::kWarning, + "ignored-output-request", + {fixture.domain->SourcePath(), 80U}, + "*OUTPUT", + "FIELD", + "Ignored output request."}, + {fesa::Severity::kWarning, + "ignored-keyword", + {fixture.domain->SourcePath(), 20U}, + "*PREPRINT", + "", + "Ignored generator control."}}; + const auto output = directory.Path() / "results.h5"; - fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, diagnostics).IsOk()); - const auto file = openFile(output); - auto stressRows = readStressRows(file.get()); - ASSERT_EQ(stressRows.size(), 2U); - for (std::size_t index = 0U; index < stressRows.size(); ++index) { - EXPECT_EQ(stressRows[index].internalElementId, 0U); - EXPECT_EQ(stressRows[index].gaussPointIndex, index + 1U); - EXPECT_EQ(stressRows[index].sectionPointIndex, 0U); - EXPECT_DOUBLE_EQ(stressRows[index].x1, 0.0); - EXPECT_DOUBLE_EQ(stressRows[index].x2, 0.0); - EXPECT_STREQ(stressRows[index].source, "fesa-default"); - } - reclaimStressRows(file.get(), stressRows); + fesa::Hdf5ResultsWriter writer; + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, diagnostics) + .IsOk()); + const auto file = OpenFile(output); + auto stress_rows = ReadStressRows(file.Get()); + ASSERT_EQ(stress_rows.size(), 2U); + for (std::size_t index = 0U; index < stress_rows.size(); ++index) { + EXPECT_EQ(stress_rows[index].internal_element_id, 0U); + EXPECT_EQ(stress_rows[index].gauss_point_index, index + 1U); + EXPECT_EQ(stress_rows[index].section_point_index, 0U); + EXPECT_DOUBLE_EQ(stress_rows[index].x1, 0.0); + EXPECT_DOUBLE_EQ(stress_rows[index].x2, 0.0); + EXPECT_STREQ(stress_rows[index].source, "fesa-default"); + } + ReclaimStressRows(file.Get(), stress_rows); - auto rows = readDiagnosticRows(file.get()); - ASSERT_EQ(rows.size(), 2U); - EXPECT_STREQ(rows[0U].severity, "warning"); - EXPECT_STREQ(rows[0U].code, "ignored-keyword"); - EXPECT_STREQ( - rows[0U].file, - std::filesystem::absolute(fixture.domain->SourcePath()) - .lexically_normal() - .generic_u8string() - .c_str()); - EXPECT_EQ(rows[0U].line, 20U); - EXPECT_STREQ(rows[0U].keyword, "*PREPRINT"); - EXPECT_STREQ(rows[0U].entityIdentity, ""); - EXPECT_STREQ(rows[0U].message, "Ignored generator control."); - EXPECT_STREQ(rows[1U].code, "ignored-output-request"); - EXPECT_EQ(rows[1U].line, 80U); - reclaimDiagnosticRows(file.get(), rows); + auto rows = ReadDiagnosticRows(file.Get()); + ASSERT_EQ(rows.size(), 2U); + EXPECT_STREQ(rows[0U].severity, "warning"); + EXPECT_STREQ(rows[0U].code, "ignored-keyword"); + EXPECT_STREQ(rows[0U].file, + std::filesystem::absolute(fixture.domain->SourcePath()) + .lexically_normal() + .generic_u8string() + .c_str()); + EXPECT_EQ(rows[0U].line, 20U); + EXPECT_STREQ(rows[0U].keyword, "*PREPRINT"); + EXPECT_STREQ(rows[0U].entity_identity, ""); + EXPECT_STREQ(rows[0U].message, "Ignored generator control."); + EXPECT_STREQ(rows[1U].code, "ignored-output-request"); + EXPECT_EQ(rows[1U].line, 80U); + ReclaimDiagnosticRows(file.Get(), rows); } TEST(Hdf5ResultsWriter, FailureLeavesNoPartialAndPreservesExistingFinal) { - TempDirectory directory{"failure"}; - auto fixture = makeFixture(directory.path() / "failure.inp"); - fesa::Hdf5ResultsWriter writer; + TempDirectory directory{"failure"}; + auto fixture = MakeFixture(directory.Path() / "failure.inp"); + fesa::Hdf5ResultsWriter writer; - fixture.state->Displacement()[0U] = - std::numeric_limits::quiet_NaN(); - const auto invalidOutput = directory.path() / "invalid-results.h5"; - expectOutputFailure( - writer.Write(invalidOutput, *fixture.domain, *fixture.state, {}), - "invalid-result-state"); - EXPECT_FALSE(std::filesystem::exists(invalidOutput)); - EXPECT_EQ(entryCount(directory.path()), 0U); - fixture.state->Displacement()[0U] = 0.25; + fixture.state->Displacement()[0U] = std::numeric_limits::quiet_NaN(); + const auto invalid_output = directory.Path() / "invalid-results.h5"; + ExpectOutputFailure( + writer.Write(invalid_output, *fixture.domain, *fixture.state, {}), + "invalid-result-state"); + EXPECT_FALSE(std::filesystem::exists(invalid_output)); + EXPECT_EQ(EntryCount(directory.Path()), 0U); + fixture.state->Displacement()[0U] = 0.25; - const auto final = directory.path() / "results.h5"; - const std::vector sentinel = {'p', 'r', 'e', 'v', 'i', 'o', 'u', 's'}; - writeBytes(final, sentinel); - WinHandle lock{CreateFileW( - final.c_str(), - GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - nullptr)}; - ASSERT_NE(lock.get(), INVALID_HANDLE_VALUE); + const auto final = directory.Path() / "results.h5"; + const std::vector sentinel = {'p', 'r', 'e', 'v', 'i', 'o', 'u', 's'}; + WriteBytes(final, sentinel); + WinHandle lock{CreateFileW(final.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; + ASSERT_NE(lock.Get(), INVALID_HANDLE_VALUE); - expectOutputFailure( - writer.Write(final, *fixture.domain, *fixture.state, {}), - "hdf5-finalization-failure"); - EXPECT_EQ(readBytes(final), sentinel); - EXPECT_EQ(entryCount(directory.path()), 1U); + ExpectOutputFailure(writer.Write(final, *fixture.domain, *fixture.state, {}), + "hdf5-finalization-failure"); + EXPECT_EQ(ReadBytes(final), sentinel); + EXPECT_EQ(EntryCount(directory.Path()), 1U); } TEST(Hdf5ResultsWriter, SuccessfullyReplacesExistingFinal) { - TempDirectory directory{"replace"}; - auto fixture = makeFixture(directory.path() / "replace.inp"); - const auto final = directory.path() / "results.h5"; - writeBytes(final, {'o', 'l', 'd'}); + TempDirectory directory{"replace"}; + auto fixture = MakeFixture(directory.Path() / "replace.inp"); + const auto final = directory.Path() / "results.h5"; + WriteBytes(final, {'o', 'l', 'd'}); - fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.Write(final, *fixture.domain, *fixture.state, {}).IsOk()); - EXPECT_GT(H5Fis_hdf5(final.string().c_str()), 0); - EXPECT_EQ(entryCount(directory.path()), 1U); - const auto file = openFile(final); - Hdf5Handle metadata{ - H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose}; - ASSERT_GE(metadata.get(), 0); - EXPECT_EQ(readUint64Attribute(metadata.get(), "schema_version"), 0U); + fesa::Hdf5ResultsWriter writer; + ASSERT_TRUE(writer.Write(final, *fixture.domain, *fixture.state, {}).IsOk()); + EXPECT_GT(H5Fis_hdf5(final.string().c_str()), 0); + EXPECT_EQ(EntryCount(directory.Path()), 1U); + const auto file = OpenFile(final); + Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; + ASSERT_GE(metadata.Get(), 0); + EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U); } // MITC4-H5-001 TEST(Hdf5ResultsWriter, WritesExactShellMetadataAndModelIdentity) { - TempDirectory directory{"shell-model"}; - const auto source = directory.path() / "shell.inp"; - auto fixture = makeShellFixture(source); - const auto output = directory.path() / "results.h5"; + TempDirectory directory{"shell-model"}; + const auto source = directory.Path() / "shell.inp"; + auto fixture = MakeShellFixture(source); + const auto output = directory.Path() / "results.h5"; - fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); - const auto file = openFile(output); + fesa::Hdf5ResultsWriter writer; + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); + const auto file = OpenFile(output); - Hdf5Handle metadata{ - H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose}; - ASSERT_GE(metadata.get(), 0); - EXPECT_EQ(readUint64Attribute(metadata.get(), "schema_version"), 0U); - EXPECT_EQ( - readStringAttribute(metadata.get(), "feature_id"), - "linear-static-mitc4-shell"); - EXPECT_EQ( - readStringAttribute(metadata.get(), "coordinate_convention"), - "global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta"); - EXPECT_EQ( - readStringAttribute(metadata.get(), "internal_formulation"), - "FESA-MITC4"); - EXPECT_EQ( - readStringAttribute(metadata.get(), "integration_rule"), - "2x2x2-gauss; mitc4-edge-midpoint-shear"); + Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; + ASSERT_GE(metadata.Get(), 0); + EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), + "linear-static-mitc4-shell"); + EXPECT_EQ( + ReadStringAttribute(metadata.Get(), "coordinate_convention"), + "global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta"); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "internal_formulation"), + "FESA-MITC4"); + EXPECT_EQ(ReadStringAttribute(metadata.Get(), "integration_rule"), + "2x2x2-gauss; mitc4-edge-midpoint-shear"); - EXPECT_EQ( - datasetDimensions(file.get(), "/model/elements"), - std::vector({1U})); - expectCompoundMemberNames( - file.get(), - "/model/elements", - {"internal_element_id", "instance_name", "source_label", - "source_element_type", "internal_formulation", "node_internal_ids", - "shell_section_internal_id", "material_internal_id"}); - const auto elements = openDataset(file.get(), "/model/elements"); - EXPECT_EQ( - readStringAttribute(elements.get(), "formulation"), "FESA-MITC4"); + EXPECT_EQ(DatasetDimensions(file.Get(), "/model/elements"), + std::vector({1U})); + ExpectCompoundMemberNames( + file.Get(), "/model/elements", + {"internal_element_id", "instance_name", "source_label", + "source_element_type", "internal_formulation", "node_internal_ids", + "shell_section_internal_id", "material_internal_id"}); + const auto elements = OpenDataset(file.Get(), "/model/elements"); + EXPECT_EQ(ReadStringAttribute(elements.Get(), "formulation"), "FESA-MITC4"); - EXPECT_EQ( - datasetDimensions(file.get(), "/model/shell/nodal_director"), - std::vector({4U, 3U})); - EXPECT_EQ( - readDoubleDataset(file.get(), "/model/shell/nodal_director"), - std::vector({0.0, 0.0, 1.0, - 0.0, 0.0, 1.0, - 0.0, 0.0, 1.0, - 0.0, 0.0, 1.0})); - EXPECT_EQ( - datasetDimensions(file.get(), "/model/shell/nodal_frame"), - std::vector({4U, 3U, 3U})); - expectCompoundMemberNames( - file.get(), "/model/shell/materials", - {"internal_material_id", "name", "E", "nu"}); - expectCompoundMemberNames( - file.get(), "/model/shell/sections", - {"internal_section_id", "source_file", "source_line", "source_elset", - "material_internal_id", "thickness"}); + EXPECT_EQ(DatasetDimensions(file.Get(), "/model/shell/nodal_director"), + std::vector({4U, 3U})); + EXPECT_EQ(ReadDoubleDataset(file.Get(), "/model/shell/nodal_director"), + std::vector( + {0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0})); + EXPECT_EQ(DatasetDimensions(file.Get(), "/model/shell/nodal_frame"), + std::vector({4U, 3U, 3U})); + ExpectCompoundMemberNames(file.Get(), "/model/shell/materials", + {"internal_material_id", "name", "E", "nu"}); + ExpectCompoundMemberNames( + file.Get(), "/model/shell/sections", + {"internal_section_id", "source_file", "source_line", "source_elset", + "material_internal_id", "thickness"}); - EXPECT_EQ( - datasetDimensions(file.get(), "/model/nodal_constraint_mask"), - std::vector({4U, 6U})); - const auto mask = readUint8Dataset(file.get(), "/model/nodal_constraint_mask"); - ASSERT_EQ(mask.size(), 24U); - EXPECT_EQ(mask[0U], 1U); - EXPECT_EQ(mask[1U], 1U); - EXPECT_EQ(mask[2U], 1U); - EXPECT_EQ(mask[3U], 1U); - EXPECT_EQ(mask[4U], 0U); - EXPECT_EQ(mask[5U], 0U); - const auto prescribed = readDoubleDataset( - file.get(), "/model/prescribed_displacement"); - ASSERT_EQ(prescribed.size(), 24U); - EXPECT_DOUBLE_EQ(prescribed[0U], 0.0); - EXPECT_DOUBLE_EQ(prescribed[3U], 0.125); - EXPECT_DOUBLE_EQ(prescribed[4U], 0.0); - EXPECT_EQ( - readDoubleDataset(file.get(), "/model/shell/section_positions"), - std::vector({-1.0, 0.0, 1.0})); - const auto locations = readDoubleDataset( - file.get(), "/model/shell/midsurface_locations"); - ASSERT_EQ(locations.size(), 8U); - const double gauss = 1.0 / std::sqrt(3.0); - EXPECT_DOUBLE_EQ(locations[0U], -gauss); - EXPECT_DOUBLE_EQ(locations[1U], -gauss); - EXPECT_DOUBLE_EQ(locations[6U], -gauss); - EXPECT_DOUBLE_EQ(locations[7U], gauss); + EXPECT_EQ(DatasetDimensions(file.Get(), "/model/nodal_constraint_mask"), + std::vector({4U, 6U})); + const auto mask = + ReadUint8Dataset(file.Get(), "/model/nodal_constraint_mask"); + ASSERT_EQ(mask.size(), 24U); + EXPECT_EQ(mask[0U], 1U); + EXPECT_EQ(mask[1U], 1U); + EXPECT_EQ(mask[2U], 1U); + EXPECT_EQ(mask[3U], 1U); + EXPECT_EQ(mask[4U], 0U); + EXPECT_EQ(mask[5U], 0U); + const auto prescribed = + ReadDoubleDataset(file.Get(), "/model/prescribed_displacement"); + ASSERT_EQ(prescribed.size(), 24U); + EXPECT_DOUBLE_EQ(prescribed[0U], 0.0); + EXPECT_DOUBLE_EQ(prescribed[3U], 0.125); + EXPECT_DOUBLE_EQ(prescribed[4U], 0.0); + EXPECT_EQ(ReadDoubleDataset(file.Get(), "/model/shell/section_positions"), + std::vector({-1.0, 0.0, 1.0})); + const auto locations = + ReadDoubleDataset(file.Get(), "/model/shell/midsurface_locations"); + ASSERT_EQ(locations.size(), 8U); + const double gauss = 1.0 / std::sqrt(3.0); + EXPECT_DOUBLE_EQ(locations[0U], -gauss); + EXPECT_DOUBLE_EQ(locations[1U], -gauss); + EXPECT_DOUBLE_EQ(locations[6U], -gauss); + EXPECT_DOUBLE_EQ(locations[7U], gauss); } // MITC4-H5-002 TEST(Hdf5ResultsWriter, WritesExactMandatoryShellResultInventory) { - TempDirectory directory{"shell-results"}; - auto fixture = makeShellFixture(directory.path() / "shell.inp"); - const auto output = directory.path() / "results.h5"; + TempDirectory directory{"shell-results"}; + auto fixture = MakeShellFixture(directory.Path() / "shell.inp"); + const auto output = directory.Path() / "results.h5"; - fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); - const auto file = openFile(output); - const std::string shellRoot = std::string{kStepRoot} + "/element/shell"; - expectNumericDataset( - file.get(), shellRoot + "/local_frame", {1U, 4U, 3U, 3U}, - "X,Y,Z", "1,1,1", "global-cartesian", "shell-local-frame"); - expectNumericDataset( - file.get(), shellRoot + "/generalized_strain", {1U, 4U, 8U}, - "E11,E22,G12,K11,K22,K12,G13,G23", - "1,1,1,1/length,1/length,1/length,1,1", - "shell-local", "midsurface"); - expectNumericDataset( - file.get(), shellRoot + "/section_resultant", {1U, 4U, 8U}, - "N11,N22,N12,M11,M22,M12,Q13,Q23", - "force/length,force/length,force/length,force,force,force,force/length,force/length", - "shell-local", "midsurface"); - expectNumericDataset( - file.get(), shellRoot + "/stress", {1U, 4U, 3U, 3U}, - "S11,S22,S12", - "force/length^2,force/length^2,force/length^2", - "shell-local", "section-position"); + fesa::Hdf5ResultsWriter writer; + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); + const auto file = OpenFile(output); + const std::string shell_root = std::string{kStepRoot} + "/element/shell"; + ExpectNumericDataset(file.Get(), shell_root + "/local_frame", + {1U, 4U, 3U, 3U}, "X,Y,Z", "1,1,1", "global-cartesian", + "shell-local-frame"); + ExpectNumericDataset(file.Get(), shell_root + "/generalized_strain", + {1U, 4U, 8U}, "E11,E22,G12,K11,K22,K12,G13,G23", + "1,1,1,1/length,1/length,1/length,1,1", "shell-local", + "midsurface"); + ExpectNumericDataset(file.Get(), shell_root + "/section_resultant", + {1U, 4U, 8U}, "N11,N22,N12,M11,M22,M12,Q13,Q23", + "force/length,force/length,force/" + "length,force,force,force,force/length,force/length", + "shell-local", "midsurface"); + ExpectNumericDataset(file.Get(), shell_root + "/stress", {1U, 4U, 3U, 3U}, + "S11,S22,S12", + "force/length^2,force/length^2,force/length^2", + "shell-local", "section-position"); - const auto strain = readDoubleDataset(file.get(), shellRoot + "/generalized_strain"); - ASSERT_EQ(strain.size(), 32U); - EXPECT_DOUBLE_EQ(strain.front(), 101.0); - EXPECT_DOUBLE_EQ(strain.back(), 408.0); - const auto stress = readDoubleDataset(file.get(), shellRoot + "/stress"); - ASSERT_EQ(stress.size(), 36U); - EXPECT_DOUBLE_EQ(stress.front(), 121.0); - EXPECT_DOUBLE_EQ(stress.back(), 429.0); + const auto strain = + ReadDoubleDataset(file.Get(), shell_root + "/generalized_strain"); + ASSERT_EQ(strain.size(), 32U); + EXPECT_DOUBLE_EQ(strain.front(), 101.0); + EXPECT_DOUBLE_EQ(strain.back(), 408.0); + const auto stress = ReadDoubleDataset(file.Get(), shell_root + "/stress"); + ASSERT_EQ(stress.size(), 36U); + EXPECT_DOUBLE_EQ(stress.front(), 121.0); + EXPECT_DOUBLE_EQ(stress.back(), 429.0); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/global/energy", {1U}, - "PHYSICAL_STRAIN_ENERGY", "force*length", "global", "global"); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/global/equilibrium", {6U}, - "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", - "force,force,force,force*length,force*length,force*length", - "global-cartesian", "global-origin"); - expectNumericDataset( - file.get(), std::string{kStepRoot} + "/global/verification_metrics", {3U}, - "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_NORMALIZED", - "1,1,1", "global", "verification"); - const auto metrics = openDataset( - file.get(), std::string{kStepRoot} + "/global/verification_metrics"); - EXPECT_EQ( - readStringAttribute(metrics.get(), "metric_definition_ids"), - "free-residual-l2-over-max-free-force-l2,force-balance-l2-over-max-force-sum,moment-balance-l2-over-max-moment-sum"); - EXPECT_EQ( - readStringAttribute(metrics.get(), "acceptance_thresholds"), - "1e-10,1e-10,1e-10"); + ExpectNumericDataset(file.Get(), std::string{kStepRoot} + "/global/energy", + {1U}, "PHYSICAL_STRAIN_ENERGY", "force*length", "global", + "global"); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/global/equilibrium", {6U}, + "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", + "force,force,force,force*length,force*length,force*length", + "global-cartesian", "global-origin"); + ExpectNumericDataset( + file.Get(), std::string{kStepRoot} + "/global/verification_metrics", {3U}, + "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_" + "NORMALIZED", + "1,1,1", "global", "verification"); + const auto metrics = OpenDataset( + file.Get(), std::string{kStepRoot} + "/global/verification_metrics"); + EXPECT_EQ(ReadStringAttribute(metrics.Get(), "metric_definition_ids"), + "free-residual-l2-over-max-free-force-l2,force-balance-l2-over-" + "max-force-sum,moment-balance-l2-over-max-moment-sum"); + EXPECT_EQ(ReadStringAttribute(metrics.Get(), "acceptance_thresholds"), + "1e-10,1e-10,1e-10"); } // MITC4-H5-003 -TEST(Hdf5ResultsWriter, WritesShellInventoryDespiteRequestsAndOmitsForbiddenPaths) { - TempDirectory directory{"shell-mandatory"}; - auto fixture = makeShellFixture(directory.path() / "shell.inp"); - const fesa::Diagnostic ignoredRequest{ - fesa::Severity::kWarning, - "ignored-output-request", - {fixture.domain->SourcePath(), 80U}, - "*ELEMENT OUTPUT", - "S", - "Output requests cannot filter mandatory shell results."}; - const auto output = directory.path() / "results.h5"; +TEST(Hdf5ResultsWriter, + WritesShellInventoryDespiteRequestsAndOmitsForbiddenPaths) { + TempDirectory directory{"shell-mandatory"}; + auto fixture = MakeShellFixture(directory.Path() / "shell.inp"); + const fesa::Diagnostic ignored_request{ + fesa::Severity::kWarning, + "ignored-output-request", + {fixture.domain->SourcePath(), 80U}, + "*ELEMENT OUTPUT", + "S", + "Output requests cannot filter mandatory shell results."}; + const auto output = directory.Path() / "results.h5"; - fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE( - writer.Write(output, *fixture.domain, *fixture.state, {ignoredRequest}) - .IsOk()); - const auto file = openFile(output); - for (const char* suffix : { - "/element/shell/local_frame", - "/element/shell/generalized_strain", - "/element/shell/section_resultant", - "/element/shell/stress", - "/global/energy", - "/global/equilibrium", - "/global/verification_metrics"}) { - const std::string path = std::string{kStepRoot} + suffix; - EXPECT_GT(H5Lexists(file.get(), path.c_str(), H5P_DEFAULT), 0) << path; - } - for (const char* forbidden : { - "/steps/Step-1/frames/0/element/shell/drilling", - "/steps/Step-1/frames/0/element/shell/drilling_energy", - "/steps/Step-1/frames/0/element/shell/S33", - "/steps/Step-1/frames/0/element/shell/S13", - "/steps/Step-1/frames/0/element/shell/S23"}) { - EXPECT_EQ(H5Lexists(file.get(), forbidden, H5P_DEFAULT), 0) << forbidden; - } - EXPECT_EQ(datasetDimensions(file.get(), "/diagnostics"), - std::vector({1U})); + fesa::Hdf5ResultsWriter writer; + ASSERT_TRUE( + writer.Write(output, *fixture.domain, *fixture.state, {ignored_request}) + .IsOk()); + const auto file = OpenFile(output); + for (const char* suffix : + {"/element/shell/local_frame", "/element/shell/generalized_strain", + "/element/shell/section_resultant", "/element/shell/stress", + "/global/energy", "/global/equilibrium", + "/global/verification_metrics"}) { + const std::string path = std::string{kStepRoot} + suffix; + EXPECT_GT(H5Lexists(file.Get(), path.c_str(), H5P_DEFAULT), 0) << path; + } + for (const char* forbidden : + {"/steps/Step-1/frames/0/element/shell/drilling", + "/steps/Step-1/frames/0/element/shell/drilling_energy", + "/steps/Step-1/frames/0/element/shell/S33", + "/steps/Step-1/frames/0/element/shell/S13", + "/steps/Step-1/frames/0/element/shell/S23"}) { + EXPECT_EQ(H5Lexists(file.Get(), forbidden, H5P_DEFAULT), 0) << forbidden; + } + EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"), + std::vector({1U})); } // MITC4-H5-004 TEST(Hdf5ResultsWriter, InvalidShellInventoryPreservesExistingFinal) { - TempDirectory directory{"shell-atomic"}; - auto fixture = makeShellFixture(directory.path() / "shell.inp"); - auto invalidState = fesa::AnalysisState::Create( - *fixture.dofs, {"Step-1", 0U}); - const auto final = directory.path() / "results.h5"; - const std::vector sentinel = {'s', 'h', 'e', 'l', 'l'}; - writeBytes(final, sentinel); + TempDirectory directory{"shell-atomic"}; + auto fixture = MakeShellFixture(directory.Path() / "shell.inp"); + auto invalid_state = + fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U}); + const auto final = directory.Path() / "results.h5"; + const std::vector sentinel = {'s', 'h', 'e', 'l', 'l'}; + WriteBytes(final, sentinel); - fesa::Hdf5ResultsWriter writer; - expectOutputFailure( - writer.Write(final, *fixture.domain, invalidState, {}), - "invalid-result-rows"); - EXPECT_EQ(readBytes(final), sentinel); - EXPECT_EQ(entryCount(directory.path()), 1U); + fesa::Hdf5ResultsWriter writer; + ExpectOutputFailure(writer.Write(final, *fixture.domain, invalid_state, {}), + "invalid-result-rows"); + EXPECT_EQ(ReadBytes(final), sentinel); + EXPECT_EQ(EntryCount(directory.Path()), 1U); }