22c9d4435c
Implement MITC4 mass, constrained whitening, MKL FEAST, modal HDF5, and deterministic Value comparison. MSVC Debug builds; 394 of 395 tests pass. Original-model all-c residual and the documented S3 static contract conflict remain unresolved; this is an implementation checkpoint, not feature acceptance.
433 lines
18 KiB
C++
433 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"
|
|
|
|
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.case_id = reference_case.case_id;
|
|
report.source_step_name = frequency.source_step_name_raw;
|
|
report.requested_mode_count = requested;
|
|
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::Evaluate(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
|