#include "nonlinear_static_reference_comparison.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/app/fesa_application.h" #include "fesa/elements/finite_rotation.h" #include "fesa/math/vector3.h" #include "mitc4_reference_comparison.h" #ifndef FESA_TEST_SOURCE_DIR #error FESA_TEST_SOURCE_DIR must identify the repository root. #endif #ifndef FESA_TEST_BINARY_DIR #error FESA_TEST_BINARY_DIR must identify the CMake binary root. #endif namespace fesa::test { namespace { constexpr std::array kDisplacementComponents{ "U1", "U2", "U3", "UR1", "UR2", "UR3"}; constexpr std::array kReactionComponents{ "RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}; constexpr double kInternalGlobalBalanceRelativeTolerance = 1.0e-8; constexpr const char* kNonlinearFrame = "Increment 10: Step Time = 1.000"; constexpr std::array kCoarseConstrainedNodeLabels{ 1, 3, 4, 5, 6, 7, 8, 9, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}; [[noreturn]] void Fail(const std::string& code, const std::string& message); struct CaseContract { std::size_t node_count; bool nonlinear; std::string family_prefix; }; const CaseContract& ContractFor(const NonlinearReferenceCase& reference_case) { static const CaseContract kCoarse{49U, true, "coarse."}; static const CaseContract kFine{729U, true, "fine."}; static const CaseContract kLinear{49U, false, "linear."}; if (reference_case.case_id == "shellnl-s4-final") { return kCoarse; } if (reference_case.case_id == "shellnl-meshfine-s4-final") { return kFine; } if (reference_case.case_id == "shell-s4-linear-limit") { return kLinear; } Fail("needs-reference-artifacts", "The reference case identifier is unknown."); } 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)} {} const std::string& Code() const noexcept { return code_; } private: std::string code_; }; [[noreturn]] void Fail(const std::string& code, const std::string& message) { throw ComparisonFailure{code, message}; } Status FailureStatus(const NonlinearReferenceCase& reference_case, const std::string& code, const std::string& message) { return Status::Failure(FailureCategory::kModel, {{Severity::kError, code, {}, "", reference_case.case_id, message}}); } std::filesystem::path Normalized(const std::filesystem::path& path) { std::error_code error; const std::filesystem::path absolute = std::filesystem::absolute(path, error); return (error ? path : absolute).lexically_normal(); } bool IsDescendantOf(const std::filesystem::path& path, const std::filesystem::path& parent) { const auto normalized_path = Normalized(path); const auto normalized_parent = Normalized(parent); auto path_part = normalized_path.begin(); auto parent_part = normalized_parent.begin(); for (; parent_part != normalized_parent.end(); ++parent_part, ++path_part) { if (path_part == normalized_path.end() || *path_part != *parent_part) { return false; } } return true; } std::string GenericPath(const std::filesystem::path& path) { return Normalized(path).generic_u8string(); } std::string Trim(const std::string& value) { const auto is_space = [](const unsigned char character) { return character == ' ' || character == '\t' || character == '\r' || character == '\n' || character == '\f' || character == '\v'; }; const auto begin = std::find_if_not( value.begin(), value.end(), [&is_space](const char character) { return is_space(static_cast(character)); }); const auto end = std::find_if_not( value.rbegin(), value.rend(), [&is_space](const char character) { return is_space(static_cast(character)); }) .base(); return begin < end ? std::string{begin, end} : std::string{}; } 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 ReadBytes(const std::filesystem::path& path) { std::ifstream stream{path, std::ios::binary}; if (!stream) { Fail("needs-reference-artifacts", "A declared reference artifact is missing or unreadable."); } return {std::istreambuf_iterator{stream}, std::istreambuf_iterator{}}; } std::uint32_t RotateRight(const std::uint32_t value, const std::uint32_t amount) { return (value >> amount) | (value << (32U - amount)); } std::string Sha256(const std::string& input) { constexpr std::array kRoundConstants{ 0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U, 0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U, 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU, 0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, 0x06ca6351U, 0x14292967U, 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U, 0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, 0x682e6ff3U, 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U}; std::vector bytes; bytes.reserve(input.size() + 72U); for (const unsigned char byte : input) { bytes.push_back(byte); } const std::uint64_t bit_length = static_cast(bytes.size()) * 8U; bytes.push_back(0x80U); while ((bytes.size() % 64U) != 56U) { bytes.push_back(0U); } for (int shift = 56; shift >= 0; shift -= 8) { bytes.push_back(static_cast(bit_length >> shift)); } std::array state{0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU, 0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U}; for (std::size_t offset = 0U; offset < bytes.size(); offset += 64U) { std::array words{}; for (std::size_t index = 0U; index < 16U; ++index) { const std::size_t byte = offset + index * 4U; words[index] = (static_cast(bytes[byte]) << 24U) | (static_cast(bytes[byte + 1U]) << 16U) | (static_cast(bytes[byte + 2U]) << 8U) | static_cast(bytes[byte + 3U]); } for (std::size_t index = 16U; index < words.size(); ++index) { const std::uint32_t sigma0 = RotateRight(words[index - 15U], 7U) ^ RotateRight(words[index - 15U], 18U) ^ (words[index - 15U] >> 3U); const std::uint32_t sigma1 = RotateRight(words[index - 2U], 17U) ^ RotateRight(words[index - 2U], 19U) ^ (words[index - 2U] >> 10U); words[index] = words[index - 16U] + sigma0 + words[index - 7U] + sigma1; } std::uint32_t a = state[0U]; std::uint32_t b = state[1U]; std::uint32_t c = state[2U]; std::uint32_t d = state[3U]; std::uint32_t e = state[4U]; std::uint32_t f = state[5U]; std::uint32_t g = state[6U]; std::uint32_t h = state[7U]; for (std::size_t index = 0U; index < words.size(); ++index) { const std::uint32_t sigma1 = RotateRight(e, 6U) ^ RotateRight(e, 11U) ^ RotateRight(e, 25U); const std::uint32_t choose = (e & f) ^ (~e & g); const std::uint32_t temporary1 = h + sigma1 + choose + kRoundConstants[index] + words[index]; const std::uint32_t sigma0 = RotateRight(a, 2U) ^ RotateRight(a, 13U) ^ RotateRight(a, 22U); const std::uint32_t majority = (a & b) ^ (a & c) ^ (b & c); const std::uint32_t temporary2 = sigma0 + majority; h = g; g = f; f = e; e = d + temporary1; d = c; c = b; b = a; a = temporary1 + temporary2; } state[0U] += a; state[1U] += b; state[2U] += c; state[3U] += d; state[4U] += e; state[5U] += f; state[6U] += g; state[7U] += h; } std::ostringstream result; result << std::uppercase << std::hex << std::setfill('0'); for (const std::uint32_t word : state) { result << std::setw(8) << word; } return result.str(); } std::string Fnv1a64(const std::string& input) { std::uint64_t hash = 14695981039346656037ULL; for (const unsigned char byte : input) { hash ^= static_cast(byte); hash *= 1099511628211ULL; } std::ostringstream result; result << "fnv1a64:" << std::hex << std::setfill('0') << std::setw(16) << hash; return result.str(); } struct DeclaredArtifact { std::filesystem::path path; const char* sha256; }; std::vector DeclaredArtifacts( const NonlinearReferenceCase& reference_case) { const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; if (reference_case.case_id == "shellnl-meshfine-s4-final") { const auto directory = root / "reference" / "shellNL-meshfine"; const std::vector artifacts{ {directory / "shellNL-meshfine.inp", "40DF1CE77311C8D799564A21A60F3BDEBB8148D52CAE22B7292FDFAD32018220"}, {directory / "shellNL-meshfine displacements.csv", "740819FF8AED0E279961581F7CDBFA12784CFBA3A488C35A652E80103F64433F"}, {directory / "shellNL-meshfine reactions.csv", "AEB01C98C596792B0E5F2B86A81945CAB0BEB0C6969CFC6738C6F81DF1F94363"}, {directory / "shellNL-meshfine stresses.csv", "6D6213AB5DB9D6B1F5FBD641367D08BACC805967B6C5A95BCE0E1A5395377782"}}; if (Normalized(reference_case.original_input_path) != Normalized(artifacts[0U].path) || Normalized(reference_case.displacement_csv_path) != Normalized(artifacts[1U].path) || Normalized(reference_case.reaction_csv_path) != Normalized(artifacts[2U].path) || Normalized(reference_case.stress_inventory_path) != Normalized(artifacts[3U].path)) { Fail("needs-reference-artifacts", "The fine nonlinear reference case does not use declared paths."); } return artifacts; } if (reference_case.case_id == "shellnl-s4-final") { const auto directory = root / "reference" / "shellNL"; const std::vector artifacts{ {directory / "shellNL.inp", "4BF3ECB0F7F1E8462C573E993DBC97AC5D920C91CF6F23AD4C6891D77FFBADA2"}, {directory / "shellNL displacements.csv", "4E0F99CA9372B1ADF7FAF102F43BC0B887DC743FDEDA378E4342D2CF8E0C0786"}, {directory / "shellNL reactions.csv", "001CB1541A9E94424E5E650509D46BC8724970DF4DE47184D9DB09C7938AB069"}, {directory / "shellNL stresses.csv", "5500E5114B1C44887528E30CEC054A1EB0F68BE3F4B8CE81FA59FC74A881483F"}}; if (Normalized(reference_case.original_input_path) != Normalized(artifacts[0U].path) || Normalized(reference_case.displacement_csv_path) != Normalized(artifacts[1U].path) || Normalized(reference_case.reaction_csv_path) != Normalized(artifacts[2U].path) || Normalized(reference_case.stress_inventory_path) != Normalized(artifacts[3U].path)) { Fail("needs-reference-artifacts", "The nonlinear reference case does not use its declared paths."); } return artifacts; } if (reference_case.case_id == "shell-s4-linear-limit") { const auto directory = root / "reference" / "shell"; const std::vector artifacts{ {directory / "shell.inp", "4005851E1AB22FD3A16AC17A8D5DA3E051233F69F37419079F3553AD134ECFCF"}, {directory / "shell displacements.csv", "C81D94E0B4A849F87AA0F79C83A79B94D5661AC79E44ED826919AB432C87746B"}, {directory / "shell reactions.csv", "0E34EEC27E700C3C0C744C1DAFD8A3B95EFEF34B8616FE96665876586FE82D41"}, {directory / "shell stresses.csv", "2B75A5B0DEB045A283C9BB7E9B1153B79F7537FF487214BF4A23316B59B660A7"}}; if (Normalized(reference_case.original_input_path) != Normalized(artifacts[0U].path) || Normalized(reference_case.displacement_csv_path) != Normalized(artifacts[1U].path) || Normalized(reference_case.reaction_csv_path) != Normalized(artifacts[2U].path) || Normalized(reference_case.stress_inventory_path) != Normalized(artifacts[3U].path)) { Fail("needs-reference-artifacts", "The linear control case does not use its declared paths."); } return artifacts; } Fail("needs-reference-artifacts", "The reference case identifier is unknown."); } ArtifactSnapshot Snapshot(const DeclaredArtifact& artifact) { std::error_code error; if (!std::filesystem::is_regular_file(artifact.path, error) || error) { Fail("needs-reference-artifacts", "A declared reference artifact is missing or unreadable."); } const std::string bytes = ReadBytes(artifact.path); const std::string hash = Sha256(bytes); if (hash != artifact.sha256) { Fail("reference-artifact-hash-mismatch", "A declared reference artifact does not match its approved hash."); } const std::uintmax_t size = std::filesystem::file_size(artifact.path, error); if (error) { Fail("needs-reference-artifacts", "A declared reference artifact size cannot be read."); } const auto write_time = std::filesystem::last_write_time(artifact.path, error); if (error) { Fail("needs-reference-artifacts", "A declared reference artifact timestamp cannot be read."); } return {Normalized(artifact.path), hash, size, write_time}; } std::vector SplitCsv(const std::string& line) { std::vector fields; std::string field; bool quoted = false; for (std::size_t index = 0U; index < line.size(); ++index) { const char character = line[index]; if (character == '"') { if (quoted && index + 1U < line.size() && line[index + 1U] == '"') { field.push_back(character); ++index; } else { quoted = !quoted; } } else if (character == ',' && !quoted) { fields.push_back(Trim(field)); field.clear(); } else { field.push_back(character); } } if (quoted) { Fail("schema-mismatch", "A reference CSV row has an unclosed quote."); } fields.push_back(Trim(field)); 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 reference CSV node label is invalid."); } return value; } double ParseFiniteDouble(const std::string& field) { char* end = nullptr; const double value = std::strtod(field.c_str(), &end); if (field.empty() || end == field.c_str() || end == nullptr || *end != '\0' || !std::isfinite(value)) { Fail("schema-mismatch", "A reference CSV value is nonfinite or invalid."); } return value; } struct CsvWideRow { std::string instance_name; std::int64_t source_node_label; std::array values; }; std::vector ReadCsvWideRows( const std::filesystem::path& path, const bool requires_frame, const std::array& components, const std::size_t expected_node_count) { std::ifstream stream{path, std::ios::binary}; if (!stream) { Fail("needs-reference-artifacts", "A declared comparison CSV is missing or unreadable."); } std::string line; if (!std::getline(stream, line)) { Fail("schema-mismatch", "A declared comparison CSV is empty."); } if (!line.empty() && line.back() == '\r') { line.pop_back(); } std::vector expected_header; if (requires_frame) { expected_header.push_back("Frame"); } expected_header.push_back("Part Instance Name"); expected_header.push_back("Node Label"); for (const char* component : components) { expected_header.push_back(std::string{components == kDisplacementComponents ? (std::string{"U-"} + component) : (std::string{"RF-"} + component)}); } if (components == kDisplacementComponents) { expected_header[requires_frame ? 3U : 2U] = "U-U1"; expected_header[requires_frame ? 4U : 3U] = "U-U2"; expected_header[requires_frame ? 5U : 4U] = "U-U3"; expected_header[requires_frame ? 6U : 5U] = "UR-UR1"; expected_header[requires_frame ? 7U : 6U] = "UR-UR2"; expected_header[requires_frame ? 8U : 7U] = "UR-UR3"; } else { expected_header[requires_frame ? 3U : 2U] = "RF-RF1"; expected_header[requires_frame ? 4U : 3U] = "RF-RF2"; expected_header[requires_frame ? 5U : 4U] = "RF-RF3"; expected_header[requires_frame ? 6U : 5U] = "RM-RM1"; expected_header[requires_frame ? 7U : 6U] = "RM-RM2"; expected_header[requires_frame ? 8U : 7U] = "RM-RM3"; } if (SplitCsv(line) != expected_header) { Fail("schema-mismatch", "A comparison CSV header is out of contract."); } std::vector rows; std::map, std::size_t> identities; while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } if (line.empty()) { Fail("schema-mismatch", "A comparison CSV has a blank row."); } const auto fields = SplitCsv(line); if (fields.size() != expected_header.size()) { Fail("schema-mismatch", "A comparison CSV row has the wrong width."); } const std::size_t shift = requires_frame ? 1U : 0U; if (requires_frame && fields[0U] != kNonlinearFrame) { Fail("schema-mismatch", "A nonlinear CSV row has the wrong Frame value."); } if (fields[shift].empty()) { Fail("schema-mismatch", "A comparison CSV instance is empty."); } CsvWideRow row{fields[shift], ParsePositiveLabel(fields[shift + 1U]), {}}; for (std::size_t component = 0U; component < row.values.size(); ++component) { row.values[component] = ParseFiniteDouble(fields[shift + 2U + component]); } const auto identity = std::make_pair(UppercaseAscii(row.instance_name), row.source_node_label); if (!identities.emplace(identity, rows.size()).second) { Fail("schema-mismatch", "A comparison CSV wide-row identity is duplicated."); } rows.push_back(std::move(row)); } if (rows.size() != expected_node_count) { Fail("schema-mismatch", "A comparison CSV node inventory is out of contract."); } return rows; } class Hdf5Handle { 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_); } } hid_t Get() const noexcept { return value_; } private: hid_t value_{-1}; Closer closer_{nullptr}; }; class Hdf5ErrorSilencer { public: Hdf5ErrorSilencer() { if (H5Eget_auto2(H5E_DEFAULT, &callback_, &client_data_) >= 0 && H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { active_ = true; } } ~Hdf5ErrorSilencer() { if (active_) { (void)H5Eset_auto2(H5E_DEFAULT, callback_, client_data_); } } private: H5E_auto2_t callback_{nullptr}; void* client_data_{nullptr}; bool active_{false}; }; class Hdf5VlenReclaimer { public: Hdf5VlenReclaimer(const hid_t memory_type, const hid_t data_space, void* const data) : memory_type_{memory_type}, data_space_{data_space}, data_{data} {} ~Hdf5VlenReclaimer() { if (active_) { (void)H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_); } } void Reclaim() { active_ = false; if (H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_) < 0) { Fail("schema-mismatch", "HDF5 variable-length strings cannot be freed."); } } 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; } void RequireHdf(const herr_t value, const char* message) { if (value < 0) { Fail("schema-mismatch", message); } } Hdf5Handle OpenDataset(const hid_t file, const std::string& path) { return {RequireId(H5Dopen2(file, path.c_str(), H5P_DEFAULT), "A required HDF5 dataset is missing."), H5Dclose}; } std::vector Dimensions(const hid_t dataset) { Hdf5Handle space{RequireId(H5Dget_space(dataset), "HDF5 dimensions cannot be inspected."), H5Sclose}; const int rank = H5Sget_simple_extent_ndims(space.Get()); if (rank < 0) { Fail("schema-mismatch", "HDF5 rank cannot be inspected."); } std::vector dimensions(static_cast(rank)); if (rank > 0) { RequireHdf(H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr), "HDF5 extents cannot be inspected."); } return dimensions; } Hdf5Handle MakeUtf8StringType() { Hdf5Handle type{RequireId(H5Tcopy(H5T_C_S1), "An HDF5 string type cannot be created."), H5Tclose}; RequireHdf(H5Tset_size(type.Get(), H5T_VARIABLE), "An HDF5 variable string type cannot be configured."); RequireHdf(H5Tset_cset(type.Get(), H5T_CSET_UTF8), "An HDF5 UTF-8 string type cannot be configured."); return type; } std::string ReadStringAttribute(const hid_t object, const char* name) { Hdf5Handle attribute{RequireId(H5Aopen(object, name, H5P_DEFAULT), "A required HDF5 attribute is missing."), H5Aclose}; Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()), "An HDF5 attribute type cannot be read."), 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 is out of contract."); } char* raw = nullptr; RequireHdf(H5Aread(attribute.Get(), type.Get(), &raw), "An HDF5 string attribute cannot be read."); if (raw == nullptr) { Fail("schema-mismatch", "An HDF5 string attribute is null."); } const std::string result{raw}; RequireHdf(H5free_memory(raw), "HDF5 attribute memory cannot be freed."); return result; } std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) { Hdf5Handle attribute{RequireId(H5Aopen(object, name, H5P_DEFAULT), "A required HDF5 attribute is missing."), H5Aclose}; std::uint64_t value = 0U; RequireHdf(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value), "An HDF5 integer attribute cannot be read."); return 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."); } } struct NodeReadRow { std::uint64_t internal_node_id; char* instance_name; char* source_label; double coordinates[3]; }; struct Hdf5Node { std::string instance_name; std::int64_t source_node_label; std::array coordinates; }; std::vector ReadNodes(const hid_t file, const std::size_t expected_node_count) { auto dataset = OpenDataset(file, "/model/nodes"); const auto shape = Dimensions(dataset.Get()); if (shape.size() != 1U || shape[0U] != expected_node_count) { Fail("schema-mismatch", "The HDF5 node inventory is out of contract."); } auto string_type = MakeUtf8StringType(); const hsize_t coordinate_dimensions[] = {3U}; Hdf5Handle coordinates{RequireId(H5Tarray_create2( H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), "An HDF5 coordinate type cannot be created."), H5Tclose}; Hdf5Handle memory_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), "An HDF5 node type cannot be created."), H5Tclose}; RequireHdf(H5Tinsert(memory_type.Get(), "internal_node_id", HOFFSET(NodeReadRow, internal_node_id), H5T_NATIVE_UINT64), "An HDF5 node identifier cannot be read."); RequireHdf(H5Tinsert(memory_type.Get(), "instance_name", HOFFSET(NodeReadRow, instance_name), string_type.Get()), "An HDF5 node instance cannot be read."); RequireHdf(H5Tinsert(memory_type.Get(), "source_label", HOFFSET(NodeReadRow, source_label), string_type.Get()), "An HDF5 node label cannot be read."); RequireHdf(H5Tinsert(memory_type.Get(), "coordinates", HOFFSET(NodeReadRow, coordinates), coordinates.Get()), "An HDF5 node coordinates cannot be read."); std::vector raw(static_cast(shape[0U])); RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, raw.data()), "HDF5 node identities cannot be read."); Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()), "HDF5 node space cannot be read."), H5Sclose}; Hdf5VlenReclaimer reclaimer{memory_type.Get(), space.Get(), raw.data()}; std::map, std::size_t> identities; std::vector nodes; nodes.reserve(raw.size()); for (std::size_t index = 0U; index < raw.size(); ++index) { if (raw[index].internal_node_id != index || raw[index].instance_name == nullptr || raw[index].source_label == nullptr || !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 identity is invalid."); } Hdf5Node node{raw[index].instance_name, ParsePositiveLabel(raw[index].source_label), {raw[index].coordinates[0U], raw[index].coordinates[1U], raw[index].coordinates[2U]}}; const auto identity = std::make_pair(UppercaseAscii(node.instance_name), node.source_node_label); if (!identities.emplace(identity, nodes.size()).second) { Fail("schema-mismatch", "An HDF5 node identity is duplicated."); } nodes.push_back(std::move(node)); } reclaimer.Reclaim(); return nodes; } struct AcceptedFrameReadRow { std::uint64_t frame_index; std::uint64_t increment_index; double load_factor; std::uint64_t iteration_count; std::uint8_t is_factor_one; }; std::uint64_t ReadFactorOneFrame(const hid_t file) { auto dataset = OpenDataset(file, "/steps/Step-1/accepted_frames"); const auto shape = Dimensions(dataset.Get()); if (shape.size() != 1U || shape[0U] == 0U) { Fail("schema-mismatch", "The accepted-frame table is invalid."); } Hdf5Handle type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(AcceptedFrameReadRow)), "An accepted-frame type cannot be created."), H5Tclose}; RequireHdf(H5Tinsert(type.Get(), "frame_index", HOFFSET(AcceptedFrameReadRow, frame_index), H5T_NATIVE_UINT64), "An accepted-frame index cannot be read."); RequireHdf(H5Tinsert(type.Get(), "increment_index", HOFFSET(AcceptedFrameReadRow, increment_index), H5T_NATIVE_UINT64), "An accepted-frame increment cannot be read."); RequireHdf(H5Tinsert(type.Get(), "load_factor", HOFFSET(AcceptedFrameReadRow, load_factor), H5T_NATIVE_DOUBLE), "An accepted-frame load factor cannot be read."); RequireHdf(H5Tinsert(type.Get(), "iteration_count", HOFFSET(AcceptedFrameReadRow, iteration_count), H5T_NATIVE_UINT64), "An accepted-frame iteration count cannot be read."); RequireHdf(H5Tinsert(type.Get(), "is_factor_one", HOFFSET(AcceptedFrameReadRow, is_factor_one), H5T_NATIVE_UINT8), "An accepted-frame factor-one flag cannot be read."); std::vector rows(static_cast(shape[0U])); RequireHdf(H5Dread(dataset.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, rows.data()), "The accepted-frame table cannot be read."); std::vector factor_one; for (const auto& row : rows) { if (!std::isfinite(row.load_factor) || row.increment_index == 0U || row.iteration_count == 0U || row.is_factor_one > 1U) { Fail("schema-mismatch", "An accepted-frame row is invalid."); } if (row.is_factor_one == 1U) { if (row.load_factor != 1.0) { Fail("schema-mismatch", "A factor-one frame must have load factor one."); } factor_one.push_back(row.frame_index); } } if (factor_one.size() != 1U) { Fail("schema-mismatch", "The HDF5 result needs one factor-one frame."); } return factor_one.front(); } std::vector ReadNodalDataset(const hid_t file, const std::string& path, const std::string& expected_components, const std::size_t node_count) { auto dataset = OpenDataset(file, path); if (Dimensions(dataset.Get()) != std::vector{node_count, 6U}) { Fail("schema-mismatch", "A nodal HDF5 dataset has the wrong shape."); } RequireStringAttribute(dataset.Get(), "component_names", expected_components); std::vector values(node_count * 6U); RequireHdf(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), "A nodal HDF5 dataset cannot be read."); if (!std::all_of(values.begin(), values.end(), [](const double value) { return std::isfinite(value); })) { Fail("schema-mismatch", "A nodal HDF5 value is nonfinite."); } return values; } std::vector ReadConstraintMask(const hid_t file, const std::size_t node_count) { auto dataset = OpenDataset(file, "/model/nodal_constraint_mask"); if (Dimensions(dataset.Get()) != std::vector{node_count, 6U}) { Fail("schema-mismatch", "The HDF5 constraint mask has the wrong shape."); } std::vector values(node_count * 6U); RequireHdf(H5Dread(dataset.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), "The HDF5 constraint mask cannot be read."); if (!std::all_of(values.begin(), values.end(), [](const std::uint8_t value) { return value <= 1U; })) { Fail("schema-mismatch", "The HDF5 constraint mask contains an invalid flag."); } return values; } struct Hdf5Projection { std::filesystem::path copied_input_path; std::vector nodes; std::vector displacement; std::vector internal_force; std::vector external_force; std::vector residual; std::vector reaction; std::vector constraint_mask; std::array global_equilibrium; }; std::filesystem::path CopiedInputPathFor( const std::filesystem::path& hdf5_results_path) { const std::string stem = hdf5_results_path.stem().string(); constexpr const char* kSuffix = "_results"; const std::size_t suffix_size = std::char_traits::length(kSuffix); if (stem.size() <= suffix_size || stem.compare(stem.size() - suffix_size, suffix_size, kSuffix) != 0) { Fail("schema-mismatch", "The generated HDF5 file does not have a sibling stem."); } return hdf5_results_path.parent_path() / (stem.substr(0U, stem.size() - suffix_size) + ".inp"); } std::array ReadGlobalBalance(const hid_t file, const std::string& path) { auto dataset = OpenDataset(file, path); if (Dimensions(dataset.Get()) != std::vector{6U}) { Fail("schema-mismatch", "The global balance dataset has the wrong shape."); } RequireStringAttribute(dataset.Get(), "component_names", "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3"); RequireStringAttribute(dataset.Get(), "location", "global-balance-error"); std::array values{}; RequireHdf(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), "The global balance dataset cannot be read."); if (!std::all_of(values.begin(), values.end(), [](const double value) { return std::isfinite(value); })) { Fail("schema-mismatch", "The global balance dataset contains nonfinite data."); } return values; } Hdf5Projection ReadNonlinearHdf5( const NonlinearReferenceCase& reference_case, const std::filesystem::path& hdf5_results_path) { Hdf5ErrorSilencer silence; Hdf5Handle file{RequireId(H5Fopen(hdf5_results_path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), "The generated HDF5 result 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") != 1U) { Fail("schema-mismatch", "The nonlinear HDF5 schema version is invalid."); } RequireStringAttribute(metadata.Get(), "feature_id", "nonlinear-static-newton-raphson"); RequireStringAttribute(metadata.Get(), "analysis_procedure", "nonlinear-static-full-newton"); const CaseContract& contract = ContractFor(reference_case); const std::filesystem::path copied_input = CopiedInputPathFor(hdf5_results_path); const std::string expected_identity = "path=" + GenericPath(copied_input) + ";content_identity=" + Fnv1a64(ReadBytes(reference_case.original_input_path)); if (ReadStringAttribute(metadata.Get(), "source_input_identity") != expected_identity) { Fail("schema-mismatch", "The HDF5 source path or source content identity is out of contract."); } const auto nodes = ReadNodes(file.Get(), contract.node_count); const std::uint64_t frame_index = ReadFactorOneFrame(file.Get()); const std::string root = "/steps/Step-1/frames/" + std::to_string(frame_index) + "/nodal/"; return {copied_input, nodes, ReadNodalDataset(file.Get(), root + "displacement", "UX,UY,UZ,URX,URY,URZ", contract.node_count), ReadNodalDataset(file.Get(), root + "generalized_internal_force", "FI1,FI2,FI3,MI_PHI1,MI_PHI2,MI_PHI3", contract.node_count), ReadNodalDataset(file.Get(), root + "generalized_external_force", "FE1,FE2,FE3,ME_PHI1,ME_PHI2,ME_PHI3", contract.node_count), ReadNodalDataset(file.Get(), root + "generalized_residual", "RU1,RU2,RU3,RPHI1,RPHI2,RPHI3", contract.node_count), ReadNodalDataset(file.Get(), root + "reaction", "RF1,RF2,RF3,RM1,RM2,RM3", contract.node_count), ReadConstraintMask(file.Get(), contract.node_count), ReadGlobalBalance(file.Get(), "/steps/Step-1/frames/" + std::to_string(frame_index) + "/global/equilibrium")}; } void RequireExactNodeLabels(const std::vector& rows, const std::size_t expected_node_count) { std::vector labels; labels.reserve(rows.size()); for (const auto& row : rows) { labels.push_back(row.source_node_label); } std::sort(labels.begin(), labels.end()); if (labels.size() != expected_node_count) { Fail("schema-mismatch", "The CSV node inventory is out of contract."); } for (std::size_t index = 0U; index < labels.size(); ++index) { if (labels[index] != static_cast(index + 1U)) { Fail("schema-mismatch", "The CSV node labels are out of contract."); } } } void RequireExactNodeLabels(const std::vector& nodes, const std::size_t expected_node_count) { std::vector labels; labels.reserve(nodes.size()); for (const auto& node : nodes) { labels.push_back(node.source_node_label); } std::sort(labels.begin(), labels.end()); if (labels.size() != expected_node_count) { Fail("schema-mismatch", "The HDF5 node inventory is out of contract."); } for (std::size_t index = 0U; index < labels.size(); ++index) { if (labels[index] != static_cast(index + 1U)) { Fail("schema-mismatch", "The HDF5 node labels are out of contract."); } } } std::string Key(const std::string& instance_name, const std::int64_t source_node_label, const std::string& component) { return UppercaseAscii(instance_name) + "\x1f" + std::to_string(source_node_label) + "\x1f" + component; } bool IsSelected(const std::int64_t label, const std::vector& selected_labels) { return selected_labels.empty() || std::find(selected_labels.begin(), selected_labels.end(), label) != selected_labels.end(); } std::map ProjectCsv( const std::vector& rows, const std::array& components, const std::vector& selected_components, const std::vector& selected_labels) { std::map projected; for (const auto& row : rows) { if (!IsSelected(row.source_node_label, selected_labels)) { continue; } for (const std::size_t component : selected_components) { const std::string key = Key(row.instance_name, row.source_node_label, components[component]); if (!projected.emplace(key, row.values[component]).second) { Fail("schema-mismatch", "A CSV component-row identity is duplicated."); } } } return projected; } std::map ProjectHdf5( const Hdf5Projection& projection, const std::vector& values, const std::array& components, const std::vector& selected_components, const std::vector& selected_labels) { std::map projected; if (values.size() != projection.nodes.size() * 6U) { Fail("schema-mismatch", "An HDF5 nodal vector has the wrong row count."); } for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { if (!IsSelected(projection.nodes[node].source_node_label, selected_labels)) { continue; } for (const std::size_t component : selected_components) { const std::string key = Key(projection.nodes[node].instance_name, projection.nodes[node].source_node_label, components[component]); const double value = values[node * 6U + component]; if (!std::isfinite(value) || !projected.emplace(key, value).second) { Fail("schema-mismatch", "An HDF5 component-row identity is invalid."); } } } return projected; } ReferenceToleranceFamilyDecision EvaluateFamily( const std::string& identity, const std::vector& components, const bool blocking, const std::map& hdf5, const std::map& reference) { if (hdf5.empty() || hdf5.size() != reference.size()) { Fail("schema-mismatch", "HDF5 and CSV required row inventories differ."); } ReferenceToleranceFamily family{identity, components, blocking, {}}; family.rows.reserve(hdf5.size()); auto hdf5_row = hdf5.begin(); auto reference_row = reference.begin(); for (; hdf5_row != hdf5.end(); ++hdf5_row, ++reference_row) { if (hdf5_row->first != reference_row->first || !std::isfinite(hdf5_row->second) || !std::isfinite(reference_row->second)) { Fail("schema-mismatch", "HDF5 and CSV required row keys differ or are nonfinite."); } family.rows.push_back({hdf5_row->second, reference_row->second}); } const auto evaluation = ReferenceTolerancePolicy::Evaluate({family}); if (!evaluation.valid || evaluation.families.size() != 1U) { Fail("schema-mismatch", "The common tolerance policy rejected a finite family."); } return evaluation.families.front(); } void AppendFamily(NonlinearReferenceComparisonReport& report, const std::string& identity, const std::vector& components, const NonlinearReferenceDisposition disposition, const std::map& hdf5, const std::map& reference) { const bool blocking = disposition == NonlinearReferenceDisposition::kBlocking; const auto tolerance = EvaluateFamily(identity, components, blocking, hdf5, reference); if (tolerance.row_count != hdf5.size()) { Fail("schema-mismatch", "The tolerance family row count is inconsistent."); } report.families.push_back( {identity, components, disposition, hdf5.size(), tolerance}); if (blocking && !tolerance.passed) { report.external_blocking_passed = false; } } std::vector ExpectedConstrainedLabels( const NonlinearReferenceCase& reference_case) { if (reference_case.case_id == "shellnl-s4-final") { return {kCoarseConstrainedNodeLabels.begin(), kCoarseConstrainedNodeLabels.end()}; } if (reference_case.case_id == "shellnl-meshfine-s4-final") { std::vector labels{1}; for (std::int64_t label = 3; label <= 9; ++label) { labels.push_back(label); } for (std::int64_t label = 34; label <= 57; ++label) { labels.push_back(label); } for (std::int64_t label = 82; label <= 153; ++label) { labels.push_back(label); } return labels; } Fail("schema-mismatch", "A linear case does not define nonlinear constraints."); } std::vector RequiredConstrainedLabels( const NonlinearReferenceCase& reference_case, const Hdf5Projection& projection) { std::vector constrained; for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { const auto begin = projection.constraint_mask.begin() + static_cast(node * 6U); if (std::all_of(begin, begin + 6U, [](const std::uint8_t value) { return value == 1U; })) { constrained.push_back(projection.nodes[node].source_node_label); } } std::sort(constrained.begin(), constrained.end()); if (constrained != ExpectedConstrainedLabels(reference_case)) { Fail("schema-mismatch", "The HDF5 constrained-node selection is out of contract."); } return constrained; } fesa::Vector3 VectorAt(const std::vector& values, const std::size_t offset) { return {values[offset], values[offset + 1U], values[offset + 2U]}; } fesa::Vector3 SpatialMoment(const fesa::Vector3& phi, const fesa::Vector3& generalized) { const Matrix transform = InverseTransposeLeftJacobianSO3(phi); return {transform(0U, 0U) * generalized.X() + transform(0U, 1U) * generalized.Y() + transform(0U, 2U) * generalized.Z(), transform(1U, 0U) * generalized.X() + transform(1U, 1U) * generalized.Y() + transform(1U, 2U) * generalized.Z(), transform(2U, 0U) * generalized.X() + transform(2U, 1U) * generalized.Y() + transform(2U, 2U) * generalized.Z()}; } fesa::Vector3 MaskedResidual(const Hdf5Projection& projection, const std::size_t offset) { return {projection.constraint_mask[offset] == 1U ? projection.residual[offset] : 0.0, projection.constraint_mask[offset + 1U] == 1U ? projection.residual[offset + 1U] : 0.0, projection.constraint_mask[offset + 2U] == 1U ? projection.residual[offset + 2U] : 0.0}; } double InfinityNorm(const Hdf5Projection& projection, const std::vector& values, const bool rotations) { double norm = 0.0; const std::size_t first_component = rotations ? 3U : 0U; for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { const std::size_t offset = node * 6U; for (std::size_t component = 0U; component < 3U; ++component) { const std::size_t dof = offset + first_component + component; if (projection.constraint_mask[dof] == 0U) { norm = (std::max)(norm, std::abs(values[dof])); } } } return norm; } std::size_t FreeRowCount(const Hdf5Projection& projection, const bool rotations) { std::size_t count = 0U; const std::size_t first_component = rotations ? 3U : 0U; for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { const std::size_t offset = node * 6U; for (std::size_t component = 0U; component < 3U; ++component) { if (projection.constraint_mask[offset + first_component + component] == 0U) { ++count; } } } return count; } void AppendInternal(NonlinearReferenceComparisonReport* report, const NonlinearInternalEquilibriumReport& row) { report->internal_equilibrium.push_back(row); if (!row.passed) { report->internal_blocking_passed = false; } } void AppendFreeResidual(NonlinearReferenceComparisonReport* report, const std::string& identity, const Hdf5Projection& projection, const bool rotations) { const double residual = InfinityNorm(projection, projection.residual, rotations); const double internal = InfinityNorm(projection, projection.internal_force, rotations); const double external = InfinityNorm(projection, projection.external_force, rotations); if (external == 0.0) { AppendInternal(report, {identity, FreeRowCount(projection, rotations), residual, 0.0, std::nullopt, residual <= 1.0e-3, "accepted-free-residual-exact-zero-scale"}); return; } const double scale = (std::max)(internal, external); const double metric = residual / scale; AppendInternal(report, {identity, FreeRowCount(projection, rotations), residual, scale, metric, metric <= 1.0e-2, "accepted-free-residual-relative"}); } void AppendGlobalBalance(NonlinearReferenceComparisonReport* report, const std::string& identity, const fesa::Vector3& balance, const double applied_scale, const double reaction_scale) { AppendInternal(report, EvaluateGlobalBalance(identity, balance.Norm(), applied_scale, reaction_scale)); } void AppendInternalEquilibrium(NonlinearReferenceComparisonReport* report, const NonlinearReferenceCase& reference_case, const Hdf5Projection& projection) { fesa::Vector3 reaction_force; fesa::Vector3 reaction_moment; double applied_force_scale = 0.0; double reaction_force_scale = 0.0; double applied_moment_scale = 0.0; double reaction_moment_scale = 0.0; for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { const std::size_t offset = node * 6U; const fesa::Vector3 position = fesa::Vector3{projection.nodes[node].coordinates} + VectorAt(projection.displacement, offset); const fesa::Vector3 phi = VectorAt(projection.displacement, offset + 3U); const fesa::Vector3 applied_force = VectorAt(projection.external_force, offset); const fesa::Vector3 constrained_force = MaskedResidual(projection, offset); const fesa::Vector3 applied_moment = SpatialMoment(phi, VectorAt(projection.external_force, offset + 3U)); const fesa::Vector3 constrained_moment = SpatialMoment(phi, MaskedResidual(projection, offset + 3U)); const fesa::Vector3 applied_resultant = position.Cross(applied_force) + applied_moment; const fesa::Vector3 constrained_resultant = position.Cross(constrained_force) + constrained_moment; reaction_force = reaction_force + constrained_force; reaction_moment = reaction_moment + constrained_resultant; applied_force_scale += applied_force.Norm(); reaction_force_scale += constrained_force.Norm(); applied_moment_scale += applied_resultant.Norm(); reaction_moment_scale += constrained_resultant.Norm(); } report->constrained_reaction_resultant = { reaction_force.Components(), reaction_moment.Components()}; const std::string prefix = ContractFor(reference_case).family_prefix; AppendGlobalBalance(report, prefix + "global_force_balance", {projection.global_equilibrium[0U], projection.global_equilibrium[1U], projection.global_equilibrium[2U]}, applied_force_scale, reaction_force_scale); AppendGlobalBalance(report, prefix + "global_moment_balance", {projection.global_equilibrium[3U], projection.global_equilibrium[4U], projection.global_equilibrium[5U]}, applied_moment_scale, reaction_moment_scale); AppendFreeResidual(report, prefix + "accepted_free_force_residual", projection, false); AppendFreeResidual(report, prefix + "accepted_free_moment_residual", projection, true); } std::filesystem::path DerivedLogPath(const std::filesystem::path& copied_input) { return copied_input.parent_path() / (copied_input.stem().string() + "_log.txt"); } std::filesystem::path DerivedResultsPath( const std::filesystem::path& copied_input) { return copied_input.parent_path() / (copied_input.stem().string() + "_results.h5"); } NonlinearReferenceComparisonReport MakeReport( const NonlinearReferenceCase& reference_case, const std::filesystem::path& hdf5_results_path, const std::vector& before, const std::vector& after) { const auto copied_input = CopiedInputPathFor(hdf5_results_path); return {reference_case.case_id, reference_case.original_input_path, copied_input, hdf5_results_path, DerivedLogPath(copied_input), before, after, {}, true, {}, {}, {}, true, true}; } void CompareLinearCase(NonlinearReferenceComparisonReport& report, const NonlinearReferenceCase& reference_case) { const Mitc4ReferenceCase legacy_case{ reference_case.case_id, "S4", report.copied_input_path, reference_case.displacement_csv_path, report.generated_results_hdf5}; const auto legacy = Mitc4ReferenceComparison::Compare(legacy_case); if (!legacy.HasValue()) { const auto& diagnostics = legacy.GetStatus().Diagnostics(); const std::string message = diagnostics.empty() ? "The linear control comparison failed." : diagnostics.front().message; Fail("schema-mismatch", message); } for (const auto& metric : legacy.Value().metrics) { ReferenceToleranceFamilyDecision tolerance{}; tolerance.identity = metric.blocking ? "linear.translation" : "linear.rotation"; tolerance.components = metric.components; tolerance.blocking = metric.blocking; tolerance.reference_scale = metric.reference_scale; tolerance.near_zero_band = metric.near_zero_band; tolerance.near_zero_count = metric.near_zero_count; tolerance.relative_rms = metric.relative_rms; tolerance.maximum_absolute_error = metric.maximum_absolute_error; tolerance.worst_row = metric.worst_row; tolerance.rms_passed = metric.rms_passed; tolerance.passed = metric.passed; tolerance.diagnostic_code = metric.diagnostic_code; tolerance.row_count = metric.row_count; const auto disposition = metric.blocking ? NonlinearReferenceDisposition::kBlocking : NonlinearReferenceDisposition::kWarningOnly; report.families.push_back({tolerance.identity, tolerance.components, disposition, tolerance.row_count, tolerance}); if (metric.blocking && !metric.passed) { report.external_blocking_passed = false; } } } void CompareNonlinearCase(NonlinearReferenceComparisonReport& report, const NonlinearReferenceCase& reference_case) { const CaseContract& contract = ContractFor(reference_case); const auto hdf5 = ReadNonlinearHdf5(reference_case, report.generated_results_hdf5); report.copied_input_path = hdf5.copied_input_path; report.generated_log_path = DerivedLogPath(hdf5.copied_input_path); RequireExactNodeLabels(hdf5.nodes, contract.node_count); const auto displacement_rows = ReadCsvWideRows( reference_case.displacement_csv_path, true, kDisplacementComponents, contract.node_count); const auto reaction_rows = ReadCsvWideRows(reference_case.reaction_csv_path, true, kReactionComponents, contract.node_count); RequireExactNodeLabels(displacement_rows, contract.node_count); RequireExactNodeLabels(reaction_rows, contract.node_count); const std::vector translations{0U, 1U, 2U}; const std::vector rotations{3U, 4U, 5U}; const std::vector inplane_forces{0U, 1U}; const std::vector transverse_force{2U}; const std::vector physical_moments{3U, 4U}; const std::vector all_nodes; const auto constrained = RequiredConstrainedLabels(reference_case, hdf5); AppendFamily(report, contract.family_prefix + "translation", {"U1", "U2", "U3"}, NonlinearReferenceDisposition::kBlocking, ProjectHdf5(hdf5, hdf5.displacement, kDisplacementComponents, translations, all_nodes), ProjectCsv(displacement_rows, kDisplacementComponents, translations, all_nodes)); AppendFamily(report, contract.family_prefix + "rotation", {"UR1", "UR2", "UR3"}, NonlinearReferenceDisposition::kWarningOnly, ProjectHdf5(hdf5, hdf5.displacement, kDisplacementComponents, rotations, all_nodes), ProjectCsv(displacement_rows, kDisplacementComponents, rotations, all_nodes)); AppendFamily(report, contract.family_prefix + "local_reaction_inplane_force", {"RF1", "RF2"}, NonlinearReferenceDisposition::kReviewOnly, ProjectHdf5(hdf5, hdf5.reaction, kReactionComponents, inplane_forces, constrained), ProjectCsv(reaction_rows, kReactionComponents, inplane_forces, constrained)); AppendFamily(report, contract.family_prefix + "local_reaction_transverse_force", {"RF3"}, NonlinearReferenceDisposition::kReviewOnly, ProjectHdf5(hdf5, hdf5.reaction, kReactionComponents, transverse_force, constrained), ProjectCsv(reaction_rows, kReactionComponents, transverse_force, constrained)); AppendFamily(report, contract.family_prefix + "local_reaction_physical_moment", {"RM1", "RM2"}, NonlinearReferenceDisposition::kReviewOnly, ProjectHdf5(hdf5, hdf5.reaction, kReactionComponents, physical_moments, constrained), ProjectCsv(reaction_rows, kReactionComponents, physical_moments, constrained)); AppendInternalEquilibrium(&report, reference_case, hdf5); } } // namespace NonlinearInternalEquilibriumReport EvaluateGlobalBalance( const std::string& family_identity, const double numerator, const double applied_scale, const double reaction_scale) { const double scale = (std::max)(applied_scale, reaction_scale); if (scale == 0.0) { return {family_identity, 3U, numerator, scale, std::nullopt, numerator == 0.0, "zero-reference-scale-nonzero-error"}; } const double metric = numerator / scale; return {family_identity, 3U, numerator, scale, metric, metric <= kInternalGlobalBalanceRelativeTolerance, "global-balance-relative"}; } Result> ArtifactCheck( const NonlinearReferenceCase& reference_case) { try { const auto artifacts = DeclaredArtifacts(reference_case); std::vector snapshots; snapshots.reserve(artifacts.size()); for (const auto& artifact : artifacts) { snapshots.push_back(Snapshot(artifact)); } return Result>::Success(std::move(snapshots)); } catch (const ComparisonFailure& exception) { return Result>::Failure( FailureStatus(reference_case, exception.Code(), exception.what())); } catch (const std::exception& exception) { return Result>::Failure( FailureStatus(reference_case, "reference-artifact-check-failed", exception.what())); } } Result CopyInputToExecutionDirectory( const NonlinearReferenceCase& reference_case) { try { const auto check = ArtifactCheck(reference_case); if (!check.HasValue()) { return Result::Failure(check.GetStatus()); } const std::filesystem::path execution_root = Normalized(reference_case.execution_directory); if (!IsDescendantOf(execution_root, std::filesystem::path{FESA_TEST_BINARY_DIR}) || IsDescendantOf(execution_root, reference_case.original_input_path.parent_path())) { Fail("invalid-execution-directory", "Reference execution must use a build-local directory outside reference."); } std::error_code error; std::filesystem::create_directories(execution_root, error); if (error) { Fail("copy-input-failed", "The build-local execution directory cannot be created."); } static std::atomic sequence{0U}; const auto tick = std::chrono::steady_clock::now().time_since_epoch().count(); const std::filesystem::path run_directory = execution_root / (reference_case.case_id + "-" + std::to_string(tick) + "-" + std::to_string(sequence.fetch_add(1U))); if (!std::filesystem::create_directory(run_directory, error) || error) { Fail("copy-input-failed", "A unique execution directory cannot be created."); } const std::filesystem::path copied_input = run_directory / reference_case.original_input_path.filename(); if (!std::filesystem::copy_file(reference_case.original_input_path, copied_input, std::filesystem::copy_options::none, error) || error || ReadBytes(copied_input) != ReadBytes(reference_case.original_input_path)) { Fail("copy-input-failed", "The copied input does not preserve source bytes."); } return Result::Success(copied_input); } catch (const ComparisonFailure& exception) { return Result::Failure( FailureStatus(reference_case, exception.Code(), exception.what())); } catch (const std::exception& exception) { return Result::Failure( FailureStatus(reference_case, "copy-input-failed", exception.what())); } } Result RunFesaApplicationOnCopiedInput( const NonlinearReferenceCase& reference_case, const std::filesystem::path& copied_input_path) { try { if (Normalized(copied_input_path) == Normalized(reference_case.original_input_path)) { Fail("original-reference-input-forbidden", "FesaApplication must never receive the original reference input."); } if (!IsDescendantOf(copied_input_path, Normalized(reference_case.execution_directory)) || !std::filesystem::is_regular_file(copied_input_path)) { Fail("invalid-execution-input", "The execution input is not a copied artifact."); } const int exit_code = FesaApplication{}.Run({copied_input_path.string()}); if (exit_code != 0) { Fail("fesa-application-failed", "FesaApplication did not produce an accepted reference run."); } const auto results = DerivedResultsPath(copied_input_path); const auto log = DerivedLogPath(copied_input_path); if (!std::filesystem::is_regular_file(results) || !std::filesystem::is_regular_file(log)) { Fail("generated-artifact-missing", "The copied execution did not produce sibling result and log artifacts."); } return Result::Success({copied_input_path, results, log}); } catch (const ComparisonFailure& exception) { return Result::Failure( FailureStatus(reference_case, exception.Code(), exception.what())); } catch (const std::exception& exception) { return Result::Failure( FailureStatus(reference_case, "fesa-application-failed", exception.what())); } } Result CompareHdf5ToReferenceCsv( const NonlinearReferenceCase& reference_case, const std::filesystem::path& hdf5_results_path) { try { const auto before = ArtifactCheck(reference_case); if (!before.HasValue()) { return Result::Failure( before.GetStatus()); } if (!std::filesystem::is_regular_file(hdf5_results_path)) { Fail("schema-mismatch", "The generated HDF5 result is missing."); } auto report = MakeReport(reference_case, hdf5_results_path, before.Value(), {}); if (ContractFor(reference_case).nonlinear) { CompareNonlinearCase(report, reference_case); } else if (reference_case.case_id == "shell-s4-linear-limit") { CompareLinearCase(report, reference_case); } else { Fail("schema-mismatch", "The requested comparison case is unknown."); } const auto after = ArtifactCheck(reference_case); if (!after.HasValue()) { return Result::Failure(after.GetStatus()); } report.post_run_artifacts = after.Value(); report.blocking_passed = report.external_blocking_passed && report.internal_blocking_passed; return Result::Success(std::move(report)); } catch (const ComparisonFailure& exception) { return Result::Failure( FailureStatus(reference_case, exception.Code(), exception.what())); } catch (const std::exception& exception) { return Result::Failure( FailureStatus(reference_case, "reference-comparison-failed", exception.what())); } } NonlinearReferenceClassification ClassifyReferenceComparison( const NonlinearReferenceComparisonReport& report) { const bool blocking_precheck = std::any_of(report.precheck_findings.begin(), report.precheck_findings.end(), [](const NonlinearReferencePrecheckFinding& finding) { return finding.disposition == NonlinearReferenceDisposition::kBlocking; }); if (!report.blocking_passed || blocking_precheck) { return NonlinearReferenceClassification::kFailed; } const bool warning_only_difference = std::any_of(report.families.begin(), report.families.end(), [](const NonlinearReferenceFamilyReport& family) { return family.disposition == NonlinearReferenceDisposition::kWarningOnly && !family.tolerance.passed; }); const bool warning_precheck = std::any_of(report.precheck_findings.begin(), report.precheck_findings.end(), [](const NonlinearReferencePrecheckFinding& finding) { return finding.disposition == NonlinearReferenceDisposition::kWarningOnly; }); return warning_only_difference || warning_precheck ? NonlinearReferenceClassification::kWarningOnlyDifferences : NonlinearReferenceClassification::kPassed; } NonlinearReferenceComparisonReport BuildReferenceComparisonReport( const NonlinearReferenceCase& reference_case, const std::filesystem::path& hdf5_results_path) { NonlinearReferenceComparisonReport report{}; report.case_id = reference_case.case_id; report.original_input_path = reference_case.original_input_path; report.generated_results_hdf5 = hdf5_results_path; report.blocking_passed = false; const auto artifacts = ArtifactCheck(reference_case); if (artifacts.HasValue()) { report.pre_run_artifacts = artifacts.Value(); report.post_run_artifacts = artifacts.Value(); } if (hdf5_results_path.has_filename()) { try { report.copied_input_path = CopiedInputPathFor(hdf5_results_path); report.generated_log_path = DerivedLogPath(report.copied_input_path); } catch (const ComparisonFailure&) { // This builder is non-throwing by contract; comparison reports surface // the malformed result path through CompareHdf5ToReferenceCsv instead. } } return report; } } // namespace fesa::test