Files
FESADev/tests/reference/eigen_reference_comparison.cpp
T

442 lines
18 KiB
C++

#include "eigen_reference_comparison.h"
#include <hdf5.h>
#include <array>
#include <charconv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <limits>
#include <map>
#include <regex>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#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<Hdf5Mode> 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<hid_t, 4> expected_types{H5T_STD_U64LE, H5T_IEEE_F64LE,
H5T_STD_U8LE, H5T_STD_U64LE};
const std::array<const char*, 4> 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<unsigned>(index)),
"Unable to inspect a modes compound member."),
H5Tclose};
char* raw_name =
H5Tget_member_name(type.Get(), static_cast<unsigned>(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<Hdf5Mode> modes(static_cast<std::size_t>(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::uint64_t>((
std::numeric_limits<std::int64_t>::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<std::uint64_t, double> 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<std::uint64_t, double> 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::uint64_t>(
(std::numeric_limits<std::int64_t>::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<EigenReferenceComparisonReport> 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<std::uint64_t>(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<ReferenceToleranceFamily> 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<EigenReferenceComparisonReport>::Success(std::move(report));
} catch (const ComparisonFailure& exception) {
return Result<EigenReferenceComparisonReport>::Failure(
FailureStatus(reference_case, exception.Code(), exception.what()));
} catch (const std::exception& exception) {
return Result<EigenReferenceComparisonReport>::Failure(
FailureStatus(reference_case, "eigen-reference-comparison-failure",
exception.what()));
}
}
} // namespace fesa::test