feat: add eigen2 frequency pipeline and fail-closed validation

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.
This commit is contained in:
KOKO\Mimi
2026-09-09 03:14:30 +09:00
parent e74ac247db
commit 22c9d4435c
39 changed files with 7240 additions and 541 deletions
@@ -0,0 +1,70 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include "eigen_reference_comparison.h"
#include "fesa/app/fesa_application.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 {
std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary};
if (!stream) {
throw std::runtime_error{"Unable to read declared eigen artifact."};
}
return {std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}};
}
// E2-C25.
TEST(EigenReference, RunsOriginalInputAndComparesAllModalFrameIdentities) {
const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR};
const std::filesystem::path reference_directory =
source_root / "reference" / "shellNL-eigen-meshfine";
const std::filesystem::path input =
reference_directory / "shellNL-eigen-meshfine.inp";
const std::filesystem::path text =
reference_directory / "shellNL-eigen-meshfine eigenvalues.txt";
const std::string input_before = ReadBytes(input);
const std::string text_before = ReadBytes(text);
fesa::FesaApplication application;
const int exit_code = application.Run({input.string()});
EXPECT_EQ(ReadBytes(input), input_before);
EXPECT_EQ(ReadBytes(text), text_before);
ASSERT_EQ(exit_code, 0);
const std::filesystem::path results =
reference_directory / "shellNL-eigen-meshfine_results.h5";
ASSERT_TRUE(std::filesystem::is_regular_file(results));
const auto comparison = fesa::test::EigenReferenceComparison::Compare(
{"shellnl-eigen-meshfine", input, text, results});
ASSERT_TRUE(comparison.HasValue());
EXPECT_TRUE(comparison.Value().passed);
EXPECT_EQ(comparison.Value().source_step_name, "Step-2");
EXPECT_EQ(comparison.Value().requested_mode_count, 10U);
ASSERT_EQ(comparison.Value().rows.size(), 10U);
ASSERT_EQ(comparison.Value().families.size(), 10U);
for (std::size_t index = 0U; index < comparison.Value().rows.size();
++index) {
EXPECT_EQ(comparison.Value().rows[index].mode_id, index + 1U);
EXPECT_EQ(comparison.Value().rows[index].component, "Value");
EXPECT_EQ(comparison.Value().families[index].row_count, 1U);
EXPECT_TRUE(comparison.Value().families[index].passed);
}
EXPECT_EQ(ReadBytes(input), input_before);
EXPECT_EQ(ReadBytes(text), text_before);
}
} // namespace
@@ -0,0 +1,432 @@
#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
@@ -0,0 +1,49 @@
#ifndef FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
#define FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
#include "fesa/core/status.h"
#include "reference_tolerance_policy.h"
namespace fesa::test {
struct EigenReferenceCase {
std::string case_id;
std::filesystem::path input_path;
std::filesystem::path eigenvalues_text_path;
std::filesystem::path results_hdf5_path;
};
struct EigenReferenceRowDecision {
std::string source_step_name;
std::uint64_t mode_id;
std::string component;
double fesa_value;
double reference_value;
ReferenceToleranceRowDecision tolerance;
};
struct EigenReferenceComparisonReport {
std::string case_id;
std::string source_step_name;
std::uint64_t requested_mode_count;
std::vector<EigenReferenceRowDecision> rows;
std::vector<ReferenceToleranceFamilyDecision> families;
bool passed;
};
/// @brief Compares modal eigenvalues without changing the approved input or
/// TXT.
class EigenReferenceComparison {
public:
static Result<EigenReferenceComparisonReport> Compare(
const EigenReferenceCase& reference_case);
};
} // namespace fesa::test
#endif // FESA_TESTS_REFERENCE_EIGEN_REFERENCE_COMPARISON_H_
@@ -0,0 +1,165 @@
#include "eigen_reference_comparison.h"
#include <gtest/gtest.h>
#include <hdf5.h>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include "fesa/io/abaqus/input_reader.h"
// The direct fixture is intentionally kept in this test file: it exercises the
// comparator contract without depending on the application or HDF5 writer.
namespace {
std::filesystem::path UniqueFixtureDirectory() {
static std::atomic<std::uint64_t> serial{0U};
const auto ticks =
std::chrono::steady_clock::now().time_since_epoch().count();
return std::filesystem::temp_directory_path() /
("fesa-eigen-direct-" + std::to_string(ticks) + "-" +
std::to_string(serial.fetch_add(1U)));
}
struct Mode {
std::uint64_t mode_id;
double eigenvalue;
std::uint8_t classification;
std::uint64_t cluster_id;
};
void Str(hid_t o, const char* n, const std::string& v) {
hid_t t = H5Tcopy(H5T_C_S1);
H5Tset_size(t, H5T_VARIABLE);
H5Tset_cset(t, H5T_CSET_UTF8);
hid_t s = H5Screate(H5S_SCALAR),
a = H5Acreate2(o, n, t, s, H5P_DEFAULT, H5P_DEFAULT);
const char* p = v.c_str();
H5Awrite(a, t, &p);
H5Aclose(a);
H5Sclose(s);
H5Tclose(t);
}
void U(hid_t o, const char* n, std::uint64_t v) {
hid_t s = H5Screate(H5S_SCALAR),
a = H5Acreate2(o, n, H5T_STD_U64LE, s, H5P_DEFAULT, H5P_DEFAULT);
H5Awrite(a, H5T_NATIVE_UINT64, &v);
H5Aclose(a);
H5Sclose(s);
}
TEST(EigenReferenceComparison,
DirectSchemaFixtureAcceptsRepeatedReorderedModes) {
const auto d = UniqueFixtureDirectory();
std::error_code e;
std::filesystem::create_directories(d, e);
const auto inp = d / "x.inp", txt = d / "x.txt", h5 = d / "x.h5";
std::ofstream(inp) << "*Part,name=P\n*Node\n1,0,0,0\n2,1,0,0\n3,1,1,0\n4,0,1,"
"0\n*Element,type=S4\n1,1,2,3,4\n*Elset,elset=E\n1\n*"
"Shell Section,elset=E,material=M\n1.\n*End "
"Part\n*Assembly,name=A\n*Instance,name=I,part=P\n*End "
"Instance\n*Nset,nset=R,instance=I\n1\n*End "
"Assembly\n*Material,name=M\n*Density\n1.\n*Elastic\n1."
",0.3\n*Boundary\nR,1,6\n*Step,name=Step-2,nlgeom=NO\n*"
"Frequency\n2\n*End Step\n";
std::ofstream(txt)
<< "Frame: Mode 2: Value = 100 Freq = 1e999999 (cycles/time)\nFrame: "
"Mode 1: Value = 100 Freq = 1 (cycles/time)\n";
auto p = fesa::AbaqusInputReader{}.Read(inp);
ASSERT_TRUE(p.HasValue());
hid_t f = H5Fcreate(h5.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT,
H5P_DEFAULT),
m = H5Gcreate2(f, "/metadata", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
U(m, "schema_version", 0);
U(m, "eigen_schema_version", 1);
Str(m, "feature_id", "mitc4-shell-feast-eigen2");
Str(m, "analysis_procedure", "linear-frequency");
Str(m, "mass_variant", "derived-lumped");
Str(m, "normalization", "mass");
Str(m, "eigensolver_backend", "MKL FEAST");
Str(m, "source_input_identity",
"path=" + p.Value().source_path.generic_string() +
";content_identity=" + p.Value().source_content_identity);
H5Gclose(m);
hid_t ss = H5Gcreate2(f, "/steps", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
s = H5Gcreate2(ss, "Step-1", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
Str(s, "source_step_name", "Step-2");
U(s, "requested_mode_count", 2);
U(s, "returned_mode_count", 2);
hid_t g = H5Gcreate2(s, "eigen", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
hsize_t n = 2;
hid_t sp = H5Screate_simple(1, &n, nullptr),
t = H5Tcreate(H5T_COMPOUND, sizeof(Mode));
H5Tinsert(t, "mode_id", HOFFSET(Mode, mode_id), H5T_STD_U64LE);
H5Tinsert(t, "eigenvalue", HOFFSET(Mode, eigenvalue), H5T_IEEE_F64LE);
H5Tinsert(t, "classification", HOFFSET(Mode, classification), H5T_STD_U8LE);
H5Tinsert(t, "cluster_id", HOFFSET(Mode, cluster_id), H5T_STD_U64LE);
hid_t ds =
H5Dcreate2(g, "modes", t, sp, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
Mode v[] = {{1, 100, 1, 1}, {2, 100, 1, 1}};
H5Dwrite(ds, t, H5S_ALL, H5S_ALL, H5P_DEFAULT, v);
U(ds, "eigen_schema_version", 1);
Str(ds, "source_step_name", "Step-2");
Str(ds, "domain", "mode");
Str(ds, "quantity", "eigenvalue");
Str(ds, "component_names", "Value");
Str(ds, "component_unit_dimensions", "time^-2");
Str(ds, "coordinate_system", "scalar");
Str(ds, "location", "modal-frame");
H5Dclose(ds);
H5Tclose(t);
H5Sclose(sp);
H5Gclose(g);
H5Gclose(s);
H5Gclose(ss);
H5Fclose(f);
auto reference_case = fesa::test::EigenReferenceCase{"x", inp, txt, h5};
auto ok = fesa::test::EigenReferenceComparison::Compare(reference_case);
ASSERT_TRUE(ok.HasValue());
EXPECT_EQ(ok.Value().families.size(), 2U);
struct TextMutation {
std::string text;
std::string expected_code;
};
const std::vector<TextMutation> invalid_texts{
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"},
{"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"},
{"Frame: Mode 1: Value = nan Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"invalid-eigen-reference-text"},
{"Frame: Mode 184467440737095516160: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"},
{"Frame: Mode -1: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"invalid-eigen-reference-text"},
{"Frame: Mode 3: Value = 100 Freq = 1 (cycles/time)\n"
"Frame: Mode 2: Value = 100 Freq = 1 (cycles/time)\n",
"eigen-mode-identity-mismatch"}};
for (const TextMutation& invalid_text : invalid_texts) {
std::ofstream(txt, std::ios::trunc) << invalid_text.text;
const auto comparison =
fesa::test::EigenReferenceComparison::Compare(reference_case);
ASSERT_FALSE(comparison.HasValue());
ASSERT_EQ(comparison.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(comparison.GetStatus().Diagnostics()[0U].code,
invalid_text.expected_code);
}
std::ofstream(txt, std::ios::trunc)
<< "Frame: Mode 1: Value = 100 Freq = 1 (cycles/time)\nFrame: Mode 2: "
"Value = 100 Freq = 1 (cycles/time)\n";
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
H5Adelete(m, "source_input_identity");
Str(m, "source_input_identity", "wrong");
H5Gclose(m);
H5Fclose(f);
EXPECT_FALSE(
fesa::test::EigenReferenceComparison::Compare(reference_case).HasValue());
std::filesystem::remove_all(d, e);
}
} // namespace
@@ -1,5 +1,3 @@
#include "nonlinear_static_reference_comparison.h"
#include <gtest/gtest.h>
#include <algorithm>
@@ -13,6 +11,8 @@
#include <string>
#include <vector>
#include "nonlinear_static_reference_comparison.h"
#ifndef FESA_TEST_SOURCE_DIR
#error FESA_TEST_SOURCE_DIR must identify the repository root.
#endif
@@ -34,13 +34,10 @@ std::string ReadBytes(const std::filesystem::path& path) {
fesa::test::NonlinearReferenceCase MakeCase(const std::string& case_id) {
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
const bool fine = case_id == "shellnl-meshfine-s4-final";
const bool nonlinear = case_id == "shellnl-s4-final" || fine;
const bool nonlinear = case_id == "shellnl-s4-final";
const std::filesystem::path directory =
root / "reference" / (fine ? "shellNL-meshfine"
: (nonlinear ? "shellNL" : "shell"));
const std::string stem = fine ? "shellNL-meshfine"
: (nonlinear ? "shellNL" : "shell");
root / "reference" / (nonlinear ? "shellNL" : "shell");
const std::string stem = nonlinear ? "shellNL" : "shell";
return {case_id,
directory / (stem + ".inp"),
directory / (stem + " displacements.csv"),
@@ -105,9 +102,8 @@ void ExpectInternalFamiliesPassed(
<< family.family_identity << " rows=" << family.row_count
<< " numerator=" << family.numerator << " scale=" << family.scale
<< " metric="
<< (family.normalized_metric.has_value()
? *family.normalized_metric
: -1.0)
<< (family.normalized_metric.has_value() ? *family.normalized_metric
: -1.0)
<< " diagnostic=" << family.diagnostic_code;
}
}
@@ -159,14 +155,14 @@ TEST(NonlinearStaticReference,
EXPECT_EQ(generated.Value().copied_input_path, copied.Value());
EXPECT_TRUE(std::filesystem::is_regular_file(
generated.Value().generated_results_hdf5));
EXPECT_TRUE(std::filesystem::is_regular_file(
generated.Value().generated_log_path));
EXPECT_TRUE(
std::filesystem::is_regular_file(generated.Value().generated_log_path));
EXPECT_EQ(generated.Value().generated_results_hdf5.filename(),
case_id == "shellnl-s4-final" ? "shellNL_results.h5"
: "shell_results.h5");
EXPECT_EQ(generated.Value().generated_log_path.filename(),
case_id == "shellnl-s4-final" ? "shellNL_log.txt"
: "shell_log.txt");
: "shell_results.h5");
EXPECT_EQ(
generated.Value().generated_log_path.filename(),
case_id == "shellnl-s4-final" ? "shellNL_log.txt" : "shell_log.txt");
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
reference_case, generated.Value().generated_results_hdf5);
@@ -182,13 +178,12 @@ TEST(NonlinearStaticReference,
ExpectFamily(comparison.Value(),
case_id == "shellnl-s4-final" ? "coarse.translation"
: "linear.translation",
: "linear.translation",
147U, fesa::test::NonlinearReferenceDisposition::kBlocking);
ExpectFamily(comparison.Value(),
case_id == "shellnl-s4-final" ? "coarse.rotation"
: "linear.rotation",
147U,
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
ExpectFamily(
comparison.Value(),
case_id == "shellnl-s4-final" ? "coarse.rotation" : "linear.rotation",
147U, fesa::test::NonlinearReferenceDisposition::kWarningOnly);
if (case_id == "shellnl-s4-final") {
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force",
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
@@ -219,9 +214,9 @@ TEST(NonlinearStaticReference,
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
stream << "not an HDF5 file";
stream.close();
EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case,
invalid_hdf5)
.HasValue());
EXPECT_FALSE(
fesa::test::CompareHdf5ToReferenceCsv(reference_case, invalid_hdf5)
.HasValue());
fesa::test::NonlinearReferenceComparisonReport report{};
report.blocking_passed = true;
@@ -230,17 +225,29 @@ TEST(NonlinearStaticReference,
{"UR1", "UR2", "UR3"},
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
147U,
{"nonlinear.rotation", {"UR1", "UR2", "UR3"}, false, 1.0, 0.01,
0U, 0.02, 0.02, 0U, false, false, "relative-rms-exceeded", {}, 147U}});
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
{"nonlinear.rotation",
{"UR1", "UR2", "UR3"},
false,
1.0,
0.01,
0U,
0.02,
0.02,
0U,
false,
false,
"relative-rms-exceeded",
{},
147U}});
EXPECT_EQ(
fesa::test::ClassifyReferenceComparison(report),
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
}
// NSNR-REF-020.
TEST(NonlinearStaticReference, AuditsFinePrimaryAndCoarseCompanionArtifacts) {
const std::array<std::string, 3> case_ids{
"shellnl-meshfine-s4-final", "shellnl-s4-final",
"shell-s4-linear-limit"};
TEST(NonlinearStaticReference, AuditsRetainedStaticArtifacts) {
const std::array<std::string, 2> case_ids{"shellnl-s4-final",
"shell-s4-linear-limit"};
for (const auto& case_id : case_ids) {
const auto reference_case = MakeCase(case_id);
const auto before = fesa::test::ArtifactCheck(reference_case);
@@ -281,16 +288,14 @@ TEST(NonlinearStaticReference,
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment",
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment", 48U,
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
EXPECT_TRUE(comparison.Value().external_blocking_passed);
}
// NSNR-REF-022.
TEST(NonlinearStaticReference,
BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
for (const std::string case_id :
{"shellnl-meshfine-s4-final", "shellnl-s4-final"}) {
TEST(NonlinearStaticReference, BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
for (const std::string case_id : {"shellnl-s4-final"}) {
const auto reference_case = MakeCase(case_id);
const auto copied =
fesa::test::CopyInputToExecutionDirectory(reference_case);
@@ -302,9 +307,7 @@ TEST(NonlinearStaticReference,
reference_case, generated.Value().generated_results_hdf5);
ASSERT_TRUE(comparison.HasValue()) << case_id;
const std::string prefix = case_id == "shellnl-meshfine-s4-final"
? "fine."
: "coarse.";
const std::string prefix = "coarse.";
const std::array<std::string, 4> identities{
prefix + "global_force_balance", prefix + "global_moment_balance",
prefix + "accepted_free_force_residual",
@@ -313,7 +316,8 @@ TEST(NonlinearStaticReference,
const auto found = std::find_if(
comparison.Value().internal_equilibrium.begin(),
comparison.Value().internal_equilibrium.end(),
[&identity](const fesa::test::NonlinearInternalEquilibriumReport& row) {
[&identity](
const fesa::test::NonlinearInternalEquilibriumReport& row) {
return row.family_identity == identity;
});
ASSERT_NE(found, comparison.Value().internal_equilibrium.end())
@@ -321,9 +325,8 @@ TEST(NonlinearStaticReference,
EXPECT_TRUE(found->passed)
<< identity << " numerator=" << found->numerator
<< " scale=" << found->scale << " metric="
<< (found->normalized_metric.has_value()
? *found->normalized_metric
: -1.0)
<< (found->normalized_metric.has_value() ? *found->normalized_metric
: -1.0)
<< " diagnostic=" << found->diagnostic_code;
}
EXPECT_TRUE(comparison.Value().internal_blocking_passed);
@@ -334,19 +337,26 @@ TEST(NonlinearStaticReference,
}
// NSNR-REF-023.
TEST(NonlinearStaticReference,
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
TEST(
NonlinearStaticReference,
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
fesa::test::NonlinearReferenceComparisonReport report{};
report.blocking_passed = true;
report.external_blocking_passed = true;
report.internal_blocking_passed = true;
report.precheck_findings = {
{"blocking", fesa::test::NonlinearReferenceDisposition::kBlocking,
"missing-row", {}},
{"warning", fesa::test::NonlinearReferenceDisposition::kWarningOnly,
"duplicate-row", {}},
{"review", fesa::test::NonlinearReferenceDisposition::kReviewOnly,
"nonfinite-row", {}}};
{"blocking",
fesa::test::NonlinearReferenceDisposition::kBlocking,
"missing-row",
{}},
{"warning",
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
"duplicate-row",
{}},
{"review",
fesa::test::NonlinearReferenceDisposition::kReviewOnly,
"nonfinite-row",
{}}};
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
fesa::test::NonlinearReferenceClassification::kFailed);
@@ -359,22 +369,22 @@ TEST(NonlinearStaticReference,
GlobalForceAndMomentBalanceUseApprovedInclusiveOneEighthGate) {
for (const std::string& identity :
{"fine.global_force_balance", "fine.global_moment_balance"}) {
const auto below = fesa::test::EvaluateGlobalBalance(
identity, 5.0e-9, 1.0, 0.0);
const auto below =
fesa::test::EvaluateGlobalBalance(identity, 5.0e-9, 1.0, 0.0);
EXPECT_TRUE(below.passed) << identity;
ASSERT_TRUE(below.normalized_metric.has_value()) << identity;
EXPECT_EQ(*below.normalized_metric, 5.0e-9) << identity;
const auto boundary = fesa::test::EvaluateGlobalBalance(
identity, 1.0e-8, 1.0, 0.0);
const auto boundary =
fesa::test::EvaluateGlobalBalance(identity, 1.0e-8, 1.0, 0.0);
EXPECT_TRUE(boundary.passed) << identity;
ASSERT_TRUE(boundary.normalized_metric.has_value()) << identity;
EXPECT_EQ(*boundary.normalized_metric, 1.0e-8) << identity;
const auto above = fesa::test::EvaluateGlobalBalance(
identity, std::nextafter(1.0e-8,
std::numeric_limits<double>::infinity()),
1.0, 0.0);
identity,
std::nextafter(1.0e-8, std::numeric_limits<double>::infinity()), 1.0,
0.0);
EXPECT_FALSE(above.passed) << identity;
ASSERT_TRUE(above.normalized_metric.has_value()) << identity;
EXPECT_GT(*above.normalized_metric, 1.0e-8) << identity;
@@ -1,198 +1,15 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
#include "nonlinear_static_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::NonlinearReferenceCase MakePressureCase() {
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
const std::filesystem::path directory =
root / "reference" / "shellNL-meshfine-pressure";
return {"shellnl-meshfine-pressure-s4-follower",
directory / "shellNL-meshfine-pressure.inp",
directory / "shellNL-meshfine-pressure displacements.csv",
directory / "shellNL-meshfine-pressure reactions.csv",
directory / "shellNL-meshfine-pressure stresses.csv",
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" /
"shell-surface-pressure-load",
fesa::test::NonlinearReferenceArtifactDisposition::kRequired,
fesa::test::NonlinearReferenceArtifactDisposition::kRequired,
fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly,
fesa::test::NonlinearReferenceArtifactDisposition::kExcluded};
}
std::optional<std::filesystem::path> FindCachedResults(
const fesa::test::NonlinearReferenceCase& reference_case) {
const std::filesystem::path solver_library =
std::filesystem::path{FESA_TEST_BINARY_DIR} / "src" / "fesa" / "Debug" /
"fesa_solver.lib";
std::error_code error;
const auto solver_time =
std::filesystem::last_write_time(solver_library, error);
if (error ||
!std::filesystem::is_directory(reference_case.execution_directory)) {
return std::nullopt;
}
std::vector<std::filesystem::path> candidates;
for (const auto& entry : std::filesystem::recursive_directory_iterator(
reference_case.execution_directory, error)) {
if (error) {
return std::nullopt;
}
if (entry.path().filename() == "shellNL-meshfine-pressure_results.h5" &&
std::filesystem::is_regular_file(entry.path()) &&
std::filesystem::last_write_time(entry.path(), error) >= solver_time &&
!error) {
candidates.push_back(entry.path());
}
}
std::sort(candidates.begin(), candidates.end());
return candidates.empty()
? std::nullopt
: std::optional<std::filesystem::path>{candidates.back()};
}
const fesa::test::NonlinearReferenceComparisonReport& RunPressureComparison() {
static const auto report = [] {
const auto reference_case = MakePressureCase();
const auto cached = FindCachedResults(reference_case);
if (cached.has_value()) {
const auto comparison =
fesa::test::CompareHdf5ToReferenceCsv(reference_case, *cached);
if (comparison.HasValue()) {
return comparison.Value();
}
}
const auto copied =
fesa::test::CopyInputToExecutionDirectory(reference_case);
if (!copied.HasValue()) {
throw std::runtime_error{"Unable to copy pressure reference input."};
}
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
reference_case, copied.Value());
if (!generated.HasValue()) {
throw std::runtime_error{"Unable to run pressure reference input."};
}
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
reference_case, generated.Value().generated_results_hdf5);
if (!comparison.HasValue()) {
throw std::runtime_error{"Unable to compare pressure reference output."};
}
return comparison.Value();
}();
return report;
}
const fesa::test::NonlinearReferenceFamilyReport& FindFamily(
const fesa::test::NonlinearReferenceComparisonReport& report,
const std::string& identity) {
const auto found = std::find_if(
report.families.begin(), report.families.end(),
[&identity](const fesa::test::NonlinearReferenceFamilyReport& family) {
return family.family_identity == identity;
});
if (found == report.families.end()) {
throw std::runtime_error{"Required comparison family is absent."};
}
return *found;
}
fesa::test::ReferenceToleranceFamilyDecision MakeTolerance(const bool passed) {
fesa::test::ReferenceToleranceFamilyDecision result{};
result.passed = passed;
return result;
}
} // namespace
TEST(ShellSurfacePressureReference,
AuditsExactRequiredArtifactsAndIgnoresOptionalReviewArtifacts) {
const auto artifacts = fesa::test::ArtifactCheck(MakePressureCase());
ASSERT_TRUE(artifacts.HasValue());
EXPECT_EQ(MakePressureCase().reaction_artifact_disposition,
fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly);
EXPECT_EQ(MakePressureCase().stress_artifact_disposition,
fesa::test::NonlinearReferenceArtifactDisposition::kExcluded);
ASSERT_EQ(artifacts.Value().size(), 2U);
EXPECT_EQ(artifacts.Value()[0U].byte_size, 56665U);
EXPECT_EQ(artifacts.Value()[0U].sha256,
"53718CCC9B41C23FCC1B509A51D66F1347E87ACA43A74F1D6CEC4D2085F4B90E");
EXPECT_EQ(artifacts.Value()[1U].byte_size, 111182U);
EXPECT_EQ(artifacts.Value()[1U].sha256,
"2FE53F98EAC9F04779100721C3EDF8049EB1DBFE733EBCBD819AEEED03B81198");
}
TEST(ShellSurfacePressureReference,
SelectsUniqueSchemaOneFactorOneAcceptedFrame) {
const auto& report = RunPressureComparison();
EXPECT_TRUE(report.blocking_passed);
EXPECT_EQ(report.case_id, "shellnl-meshfine-pressure-s4-follower");
}
TEST(ShellSurfacePressureReference,
Projects729WideRowsTo2187LogicalRowsPerFamily) {
const auto& report = RunPressureComparison();
EXPECT_EQ(FindFamily(report, "pressure_nodal_translation_blocking").row_count,
2187U);
EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").row_count,
2187U);
}
TEST(ShellSurfacePressureReference,
MatchesByInstanceNodeLabelAndComponentNotRowOrder) {
const auto& report = RunPressureComparison();
const auto& translation =
FindFamily(report, "pressure_nodal_translation_blocking");
EXPECT_EQ(translation.components,
(std::vector<std::string>{"U1", "U2", "U3"}));
EXPECT_EQ(translation.tolerance.rows.size(), 2187U);
}
TEST(
ShellSurfacePressureReference,
RejectsSchemaIdentityCardinalityMissingExtraDuplicateAndNonfiniteBeforeTolerance) {
const auto& report = RunPressureComparison();
const std::filesystem::path invalid_hdf5 =
report.generated_results_hdf5.parent_path() / "invalid-pressure.h5";
{
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
stream << "not an HDF5 file";
}
EXPECT_FALSE(
fesa::test::CompareHdf5ToReferenceCsv(MakePressureCase(), invalid_hdf5)
.HasValue());
fesa::test::NonlinearReferenceComparisonReport precheck_report{};
precheck_report.blocking_passed = true;
precheck_report.precheck_findings = {
{"pressure_nodal_translation_blocking",
fesa::test::NonlinearReferenceDisposition::kBlocking,
"missing-extra-duplicate-or-nonfinite",
{}}};
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(precheck_report),
fesa::test::NonlinearReferenceClassification::kFailed);
}
TEST(ShellSurfacePressureReference,
ClassifiesTranslationBlockingAndRotationWarningOnly) {
fesa::test::NonlinearReferenceComparisonReport report{};
@@ -207,55 +24,9 @@ TEST(ShellSurfacePressureReference,
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
2187U,
MakeTolerance(false)}};
EXPECT_EQ(
fesa::test::ClassifyReferenceComparison(report),
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
}
TEST(ShellSurfacePressureReference,
ExcludesReactionAndStressFamiliesFromVerdict) {
const auto& report = RunPressureComparison();
EXPECT_EQ(report.families.size(), 2U);
EXPECT_EQ(
FindFamily(report, "pressure_nodal_translation_blocking").disposition,
fesa::test::NonlinearReferenceDisposition::kBlocking);
EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").disposition,
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
}
TEST(ShellSurfacePressureReference,
UsesUnchangedCommonScaleNearZeroRelativeAndRmsPolicy) {
const auto& translation = FindFamily(RunPressureComparison(),
"pressure_nodal_translation_blocking");
EXPECT_EQ(translation.tolerance.near_zero_band,
fesa::test::ReferenceTolerancePolicy::kNearZeroRatio *
translation.tolerance.reference_scale);
EXPECT_LE(translation.tolerance.relative_rms,
fesa::test::ReferenceTolerancePolicy::kRelativeRmsTolerance);
EXPECT_TRUE(translation.tolerance.rms_passed);
}
TEST(ShellSurfacePressureReference, WritesDeterministicComparisonReport) {
const auto& first = RunPressureComparison();
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
MakePressureCase(), first.generated_results_hdf5);
ASSERT_TRUE(comparison.HasValue());
ASSERT_EQ(first.families.size(), comparison.Value().families.size());
for (std::size_t index = 0U; index < first.families.size(); ++index) {
const auto& expected = first.families[index];
const auto& actual = comparison.Value().families[index];
EXPECT_EQ(actual.family_identity, expected.family_identity);
EXPECT_EQ(actual.components, expected.components);
EXPECT_EQ(actual.disposition, expected.disposition);
EXPECT_EQ(actual.row_count, expected.row_count);
EXPECT_DOUBLE_EQ(actual.tolerance.reference_scale,
expected.tolerance.reference_scale);
EXPECT_DOUBLE_EQ(actual.tolerance.relative_rms,
expected.tolerance.relative_rms);
EXPECT_EQ(actual.tolerance.worst_row, expected.tolerance.worst_row);
}
}
} // namespace