#include "eigen_reference_comparison.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" #include "mitc4_drilling_reader.h" namespace fesa::test { namespace { constexpr char kModesPath[] = "/steps/Step-1/eigen/modes"; 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 EigenReferenceCase& reference_case, const std::string& code, const std::string& message) { return Status::Failure(FailureCategory::kModel, {{Severity::kError, code, {}, "FREQUENCY", reference_case.case_id, message}}); } class Hdf5Handle { public: using Closer = herr_t (*)(hid_t); Hdf5Handle() = default; Hdf5Handle(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; } ~Hdf5Handle() { if (value_ >= 0 && closer_ != nullptr) { (void)closer_(value_); } } hid_t Get() const noexcept { return value_; } private: hid_t value_{-1}; Closer closer_{nullptr}; }; hid_t RequireId(hid_t value, const char* message) { if (value < 0) { Fail("invalid-eigen-hdf5-schema", message); } return value; } void RequireHdf(herr_t value, const char* message) { if (value < 0) { Fail("invalid-eigen-hdf5-schema", message); } } std::string ReadUtf8Attribute(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()), "Unable to inspect an HDF5 attribute type."), H5Tclose}; if (H5Tget_class(type.Get()) != H5T_STRING || H5Tget_cset(type.Get()) != H5T_CSET_UTF8 || H5Tis_variable_str(type.Get()) <= 0) { Fail("invalid-eigen-hdf5-schema", "A required HDF5 string attribute has the wrong type."); } Hdf5Handle space{RequireId(H5Aget_space(attribute.Get()), "Unable to inspect an HDF5 attribute shape."), H5Sclose}; if (H5Sget_simple_extent_ndims(space.Get()) != 0) { Fail("invalid-eigen-hdf5-schema", "A required HDF5 string attribute is not scalar."); } char* value = nullptr; RequireHdf(H5Aread(attribute.Get(), type.Get(), &value), "Unable to read an HDF5 string attribute."); if (value == nullptr) { Fail("invalid-eigen-hdf5-schema", "An HDF5 string attribute is null."); } const std::string result{value}; RequireHdf(H5free_memory(value), "Unable to release HDF5 string memory."); return result; } std::uint64_t ReadUint64Attribute(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()), "Unable to inspect an HDF5 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) { Fail("invalid-eigen-hdf5-schema", "A required HDF5 integer attribute has the wrong type."); } Hdf5Handle space{RequireId(H5Aget_space(attribute.Get()), "Unable to inspect an HDF5 attribute shape."), H5Sclose}; if (H5Sget_simple_extent_ndims(space.Get()) != 0) { Fail("invalid-eigen-hdf5-schema", "A required HDF5 integer attribute is not scalar."); } std::uint64_t value = 0U; RequireHdf(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value), "Unable to read an HDF5 integer attribute."); return value; } struct Hdf5Mode { std::uint64_t mode_id; double eigenvalue; std::uint8_t classification; std::uint64_t cluster_id; }; std::vector ReadModes(hid_t file, std::uint64_t expected_count, const std::string& source_step_name) { Hdf5Handle dataset{RequireId(H5Dopen2(file, kModesPath, H5P_DEFAULT), "The eigen modes dataset is missing."), H5Dclose}; Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()), "Unable to inspect the modes dataspace."), H5Sclose}; if (H5Sget_simple_extent_ndims(space.Get()) != 1) { Fail("invalid-eigen-hdf5-schema", "The modes dataset must be rank one."); } hsize_t count = 0U; RequireHdf(H5Sget_simple_extent_dims(space.Get(), &count, nullptr), "Unable to inspect the modes dataset extent."); if (count != expected_count) { Fail("eigen-mode-identity-mismatch", "The modes dataset count does not equal requested mode count."); } Hdf5Handle type{RequireId(H5Dget_type(dataset.Get()), "Unable to inspect the modes dataset type."), H5Tclose}; if (H5Tget_class(type.Get()) != H5T_COMPOUND || H5Tget_nmembers(type.Get()) != 4 || H5Tget_member_index(type.Get(), "mode_id") < 0 || H5Tget_member_index(type.Get(), "eigenvalue") < 0 || H5Tget_member_index(type.Get(), "classification") < 0 || H5Tget_member_index(type.Get(), "cluster_id") < 0) { Fail("invalid-eigen-hdf5-schema", "The modes compound schema is invalid."); } const std::array expected_types{H5T_STD_U64LE, H5T_IEEE_F64LE, H5T_STD_U8LE, H5T_STD_U64LE}; const std::array names{"mode_id", "eigenvalue", "classification", "cluster_id"}; for (std::size_t index = 0U; index < names.size(); ++index) { Hdf5Handle member{ RequireId(H5Tget_member_type(type.Get(), static_cast(index)), "Unable to inspect a modes compound member."), H5Tclose}; char* raw_name = H5Tget_member_name(type.Get(), static_cast(index)); if (raw_name == nullptr) { Fail("invalid-eigen-hdf5-schema", "Unable to inspect a modes member name."); } const std::string member_name{raw_name}; RequireHdf(H5free_memory(raw_name), "Unable to release a modes member name."); if (member_name != names[index] || H5Tequal(member.Get(), expected_types[index]) <= 0) { Fail("invalid-eigen-hdf5-schema", "The modes compound member type or order is invalid."); } } Hdf5Handle memory_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(Hdf5Mode)), "Unable to create a modes memory type."), H5Tclose}; RequireHdf(H5Tinsert(memory_type.Get(), "mode_id", HOFFSET(Hdf5Mode, mode_id), H5T_NATIVE_UINT64), "Unable to define the mode identifier field."); RequireHdf(H5Tinsert(memory_type.Get(), "eigenvalue", HOFFSET(Hdf5Mode, eigenvalue), H5T_NATIVE_DOUBLE), "Unable to define the eigenvalue field."); RequireHdf(H5Tinsert(memory_type.Get(), "classification", HOFFSET(Hdf5Mode, classification), H5T_NATIVE_UINT8), "Unable to define the classification field."); RequireHdf(H5Tinsert(memory_type.Get(), "cluster_id", HOFFSET(Hdf5Mode, cluster_id), H5T_NATIVE_UINT64), "Unable to define the cluster field."); std::vector modes(static_cast(count)); RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, modes.data()), "Unable to read the modes dataset."); for (std::size_t index = 0U; index < modes.size(); ++index) { if (modes[index].mode_id != index + 1U || modes[index].mode_id > static_cast(( std::numeric_limits::max)()) || !std::isfinite(modes[index].eigenvalue)) { Fail("eigen-mode-identity-mismatch", "Mode IDs must be ordered one through requested mode count."); } } if (ReadUtf8Attribute(dataset.Get(), "quantity") != "eigenvalue" || ReadUint64Attribute(dataset.Get(), "eigen_schema_version") != 1U || ReadUtf8Attribute(dataset.Get(), "source_step_name") != source_step_name || ReadUtf8Attribute(dataset.Get(), "domain") != "mode" || ReadUtf8Attribute(dataset.Get(), "component_names") != "Value" || ReadUtf8Attribute(dataset.Get(), "component_unit_dimensions") != "time^-2" || ReadUtf8Attribute(dataset.Get(), "coordinate_system") != "scalar" || ReadUtf8Attribute(dataset.Get(), "location") != "modal-frame") { Fail("invalid-eigen-hdf5-schema", "Modes dataset metadata is invalid."); } return modes; } double ParseFiniteNumber(const std::string& token) { static const std::regex number{ R"([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)"}; if (!std::regex_match(token, number)) { Fail("invalid-eigen-reference-text", "A TXT numeric token is malformed."); } double value = 0.0; const char* first = token.data() + (token.front() == '+' ? 1U : 0U); const auto conversion = std::from_chars(first, token.data() + token.size(), value); if (conversion.ec != std::errc{} || conversion.ptr != token.data() + token.size() || !std::isfinite(value)) { Fail("invalid-eigen-reference-text", "A TXT Value is nonfinite."); } return value; } void RequireNumberSyntax(const std::string& token) { static const std::regex number{ R"([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)"}; if (!std::regex_match(token, number)) { Fail("invalid-eigen-reference-text", "A TXT numeric token is malformed."); } } std::map ReadReferenceText( const std::filesystem::path& path) { std::ifstream stream{path, std::ios::binary}; if (!stream) { Fail("needs-reference-artifacts", "The eigenvalue TXT artifact is missing."); } const std::regex row{ R"(^[ \t]*Frame:[ \t]+Mode[ \t]+([0-9]+):[ \t]+Value[ \t]*=[ \t]*([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)[ \t]+Freq[ \t]*=[ \t]*([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?)[ \t]+\(cycles/time\)[ \t]*$)"}; std::map rows; std::string line; while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } if (line.find_first_not_of(" \t") == std::string::npos) { continue; } std::smatch match; if (!std::regex_match(line, match, row)) { Fail("invalid-eigen-reference-text", "A TXT row is not exact Frame/Mode syntax."); } std::uint64_t mode_id = 0U; const std::string mode_text = match[1].str(); const auto parsed = std::from_chars( mode_text.data(), mode_text.data() + mode_text.size(), mode_id); if (parsed.ec != std::errc{} || parsed.ptr != mode_text.data() + mode_text.size() || mode_id == 0U || mode_id > static_cast( (std::numeric_limits::max)()) || !rows.emplace(mode_id, ParseFiniteNumber(match[2].str())).second) { Fail("eigen-mode-identity-mismatch", "TXT mode IDs must be unique positive integers."); } RequireNumberSyntax(match[3].str()); } if (rows.empty()) { Fail("invalid-eigen-reference-text", "The eigenvalue TXT artifact is empty."); } return rows; } } // namespace Result EigenReferenceComparison::Compare( const EigenReferenceCase& reference_case) { try { auto parsed = AbaqusInputReader{}.Read(reference_case.input_path); if (!parsed.HasValue()) { Fail("needs-reference-artifacts", "The eigen input cannot be parsed."); } auto domain = AbaqusDomainMapper{}.Map(parsed.Value()); if (!domain.HasValue() || domain.Value().Frequency() == nullptr) { Fail("needs-reference-artifacts", "The input has no sole frequency step."); } const FrequencyStepDefinition& frequency = *domain.Value().Frequency(); const std::uint64_t requested = static_cast(frequency.requested_mode_count); const auto reference = ReadReferenceText(reference_case.eigenvalues_text_path); Hdf5Handle file{ RequireId(H5Fopen(reference_case.results_hdf5_path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), "The eigen HDF5 result is missing."), H5Fclose}; Hdf5Handle metadata{ RequireId(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), "The metadata group is missing."), H5Gclose}; if (ReadUint64Attribute(metadata.Get(), "schema_version") != 0U || ReadUint64Attribute(metadata.Get(), "eigen_schema_version") != 1U || ReadUtf8Attribute(metadata.Get(), "feature_id") != "mitc4-shell-feast-eigen2" || ReadUtf8Attribute(metadata.Get(), "analysis_procedure") != "linear-frequency" || ReadUtf8Attribute(metadata.Get(), "mass_variant") != "derived-lumped" || ReadUtf8Attribute(metadata.Get(), "normalization") != "mass" || ReadUtf8Attribute(metadata.Get(), "eigensolver_backend") != "MKL FEAST") { Fail("invalid-eigen-hdf5-schema", "The eigen result discriminator is invalid."); } const std::string expected_identity = "path=" + parsed.Value().source_path.generic_string() + ";content_identity=" + parsed.Value().source_content_identity; if (ReadUtf8Attribute(metadata.Get(), "source_input_identity") != expected_identity) { Fail("eigen-source-identity-mismatch", "The HDF5 source input identity differs from the executed input."); } Hdf5Handle step{ RequireId(H5Gopen2(file.Get(), "/steps/Step-1", H5P_DEFAULT), "The eigen storage step is missing."), H5Gclose}; if (ReadUtf8Attribute(step.Get(), "source_step_name") != frequency.source_step_name_raw || ReadUint64Attribute(step.Get(), "requested_mode_count") != requested || ReadUint64Attribute(step.Get(), "returned_mode_count") != requested) { Fail("eigen-mode-identity-mismatch", "The HDF5 source step or requested mode count is inconsistent."); } const auto modes = ReadModes(file.Get(), requested, frequency.source_step_name_raw); if (reference.size() != modes.size()) { Fail("eigen-mode-identity-mismatch", "TXT and HDF5 mode inventories have different sizes."); } EigenReferenceComparisonReport report{}; report.drilling_diagnostics = ReadMitc4DrillingReport(file.Get()); if (report.drilling_diagnostics && !report.drilling_diagnostics->frames.empty()) Fail("schema-mismatch", "Eigen drilling diagnostics must have no equilibrium frames."); report.case_id = reference_case.case_id; report.source_step_name = frequency.source_step_name_raw; report.requested_mode_count = requested; report.policy_id = ReferenceTolerancePolicy::kMitc4EigenFirstImplementationPolicyId; report.relative_rms_tolerance = ReferenceTolerancePolicy::kRelativeTolerance; std::vector families; families.reserve(modes.size()); for (const auto& mode : modes) { const auto row = reference.find(mode.mode_id); if (row == reference.end()) { Fail("eigen-mode-identity-mismatch", "A HDF5 mode is missing from TXT."); } families.push_back({"case=" + reference_case.case_id + ";source_step=" + frequency.source_step_name_raw + ";mode_id=" + std::to_string(mode.mode_id) + ";component=Value", {"Value"}, true, {{mode.eigenvalue, row->second}}}); } const auto evaluation = ReferenceTolerancePolicy::EvaluateMitc4EigenFirstImplementation(families); if (!evaluation.valid || evaluation.families.size() != families.size()) { Fail("nonfinite-comparison-value", "Tolerance evaluation rejected modal values."); } report.families = evaluation.families; report.passed = evaluation.passed; for (std::size_t index = 0U; index < modes.size(); ++index) { const auto& policy = report.families[index]; report.rows.push_back( {frequency.source_step_name_raw, modes[index].mode_id, "Value", modes[index].eigenvalue, reference.at(modes[index].mode_id), policy.rows[0U]}); } 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, "eigen-reference-comparison-failure", exception.what())); } } } // namespace fesa::test