#include "mitc4_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 namespace fesa::test { namespace { constexpr const char* kDisplacementPath = "/steps/Step-1/frames/0/nodal/displacement"; 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 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)} {} 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 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 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; } 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; } 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; } struct IdentityKey { std::string instance_name; std::int64_t source_node_label; 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; }; 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() != 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); 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(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* client_data_{nullptr}; bool active_{false}; }; class Hdf5VlenReclaimer { 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(memory_type_, data_space_, H5P_DEFAULT, data_) < 0) { Fail("schema-mismatch", "Unable to reclaim HDF5 variable strings."); } } 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 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}; } 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::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 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."); } 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 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"); 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(), 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."); } 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* source_element_type; char* internal_formulation; }; 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 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(); } 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 source_element_type; std::string internal_formulation; std::string integration_rule; }; 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& 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 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(); } } // namespace 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 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( const Mitc4ComparisonReport& report, 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