985 lines
37 KiB
C++
985 lines
37 KiB
C++
#include "mitc4_reference_comparison.h"
|
|
|
|
#include <hdf5.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cerrno>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <limits>
|
|
#include <locale>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "reference_tolerance_policy.h"
|
|
|
|
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 std::array<const char*, 6> kComponents{"U1", "U2", "U3",
|
|
"UR1", "UR2", "UR3"};
|
|
const std::vector<std::string> 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<unsigned char>(character));
|
|
});
|
|
const auto end =
|
|
std::find_if_not(value.rbegin(), value.rend(), [&](const char character) {
|
|
return is_space(static_cast<unsigned char>(character));
|
|
}).base();
|
|
return begin < end ? std::string{begin, end} : std::string{};
|
|
}
|
|
|
|
std::vector<std::string> SplitCsvLine(const std::string& line) {
|
|
std::vector<std::string> 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<char>(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<double, 6> values;
|
|
};
|
|
|
|
std::vector<WideRow> 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<WideRow> rows;
|
|
std::map<IdentityKey, std::size_t> 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<hsize_t> 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<hsize_t> result(static_cast<std::size_t>(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<const char*>& 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<int>(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<unsigned>(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<WideRow> 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<hsize_t>((std::numeric_limits<std::size_t>::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<std::size_t>(shape[0U]);
|
|
std::vector<NodeReadRow> 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<WideRow> rows;
|
|
rows.reserve(count);
|
|
std::map<IdentityKey, std::size_t> 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<hsize_t>((std::numeric_limits<std::size_t>::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<std::size_t>(shape[0U]);
|
|
std::vector<ElementIdentityReadRow> 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<double> ReadDisplacement(const hid_t file,
|
|
const std::size_t node_count) {
|
|
auto dataset = OpenDataset(file, kDisplacementPath);
|
|
if (Dimensions(dataset.Get()) !=
|
|
std::vector<hsize_t>{static_cast<hsize_t>(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<double> 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<WideRow> 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<std::ptrdiff_t>(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<double>::max_digits10)
|
|
<< value;
|
|
return stream.str();
|
|
}
|
|
|
|
const char* BranchName(const ReferenceToleranceBranch branch) {
|
|
switch (branch) {
|
|
case ReferenceToleranceBranch::kNearZero:
|
|
return "near-zero";
|
|
case ReferenceToleranceBranch::kRelative:
|
|
return "relative";
|
|
case ReferenceToleranceBranch::kZeroScaleExact:
|
|
return "zero-scale-exact";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
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<unsigned>(character) << std::dec
|
|
<< std::setfill(' ');
|
|
} else {
|
|
stream << static_cast<char>(character);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return stream.str();
|
|
}
|
|
|
|
void WriteJsonStrings(std::ostream& stream,
|
|
const std::vector<std::string>& values) {
|
|
stream << '[';
|
|
for (std::size_t index = 0U; index < values.size(); ++index) {
|
|
if (index != 0U) {
|
|
stream << ',';
|
|
}
|
|
stream << '"' << JsonEscape(values[index]) << '"';
|
|
}
|
|
stream << ']';
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Result<Mitc4ComparisonReport> 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<IdentityKey, const WideRow*> 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.");
|
|
}
|
|
}
|
|
|
|
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<std::vector<std::size_t>, 2> family_rows;
|
|
|
|
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.");
|
|
}
|
|
for (std::size_t component = 0U; component < kComponents.size();
|
|
++component) {
|
|
const bool blocking = component < 3U;
|
|
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],
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
false,
|
|
{},
|
|
blocking,
|
|
false});
|
|
family_rows[blocking ? 0U : 1U].push_back(row_index);
|
|
}
|
|
}
|
|
|
|
std::vector<ReferenceToleranceFamily> families;
|
|
families.reserve(2U);
|
|
for (std::size_t family_index = 0U; family_index < family_rows.size();
|
|
++family_index) {
|
|
ReferenceToleranceFamily family{};
|
|
family.identity = family_index == 0U ? "displacement-translation"
|
|
: "displacement-rotation";
|
|
family.components = family_index == 0U
|
|
? std::vector<std::string>{"U1", "U2", "U3"}
|
|
: std::vector<std::string>{"UR1", "UR2", "UR3"};
|
|
family.blocking = family_index == 0U;
|
|
family.rows.reserve(family_rows[family_index].size());
|
|
for (const std::size_t row_index : family_rows[family_index]) {
|
|
const auto& row = report.rows[row_index];
|
|
family.rows.push_back({row.fesa_value, row.reference_value});
|
|
}
|
|
families.push_back(std::move(family));
|
|
}
|
|
const auto evaluation = ReferenceTolerancePolicy::Evaluate(families);
|
|
if (!evaluation.valid || evaluation.families.size() != families.size()) {
|
|
Fail("schema-mismatch",
|
|
"A finite row produced a nonfinite comparison metric.");
|
|
}
|
|
|
|
report.metrics.reserve(evaluation.families.size());
|
|
double maximum_absolute_error = -1.0;
|
|
for (std::size_t family_index = 0U;
|
|
family_index < evaluation.families.size(); ++family_index) {
|
|
const auto& policy = evaluation.families[family_index];
|
|
for (std::size_t local = 0U; local < family_rows[family_index].size();
|
|
++local) {
|
|
const std::size_t row_index = family_rows[family_index][local];
|
|
auto& row = report.rows[row_index];
|
|
const auto& decision = policy.rows[local];
|
|
row.absolute_error = decision.absolute_error;
|
|
row.tolerance = decision.threshold;
|
|
row.normalized_error = decision.relative_error;
|
|
row.reference_scale = policy.reference_scale;
|
|
row.near_zero_band = policy.near_zero_band;
|
|
row.relative_error_applicable = decision.relative_error_applicable;
|
|
row.tolerance_branch = BranchName(decision.branch);
|
|
row.within_tolerance = decision.passed;
|
|
if (row.absolute_error > maximum_absolute_error) {
|
|
maximum_absolute_error = row.absolute_error;
|
|
report.worst_row = row_index;
|
|
}
|
|
if (!row.within_tolerance) {
|
|
if (row.blocking) {
|
|
report.passed = false;
|
|
} else {
|
|
const std::string message =
|
|
"case=" + reference_case.case_id +
|
|
";instance=" + row.instance_name +
|
|
";node=" + std::to_string(row.source_node_label) +
|
|
";component=" + row.component +
|
|
";absolute_error=" + FiniteText(row.absolute_error) +
|
|
";threshold=" + FiniteText(row.tolerance);
|
|
report.warnings.push_back(
|
|
{"rotation-reference-exceedance", row_index, message});
|
|
}
|
|
}
|
|
}
|
|
const std::size_t worst_row = family_rows[family_index][policy.worst_row];
|
|
report.metrics.push_back(
|
|
{policy.identity, policy.components, policy.blocking,
|
|
policy.reference_scale, policy.near_zero_band,
|
|
policy.near_zero_count, policy.maximum_absolute_error,
|
|
policy.relative_rms, worst_row, policy.rms_passed, policy.passed,
|
|
policy.diagnostic_code, policy.row_count});
|
|
if (policy.blocking && !policy.passed) {
|
|
report.passed = false;
|
|
}
|
|
if (!policy.blocking && !policy.rms_passed) {
|
|
report.warnings.push_back(
|
|
{"rotation-reference-rms-exceedance", worst_row,
|
|
"case=" + reference_case.case_id + ";family=" + policy.identity +
|
|
";relative_rms=" + FiniteText(policy.relative_rms)});
|
|
}
|
|
}
|
|
for (std::size_t node = 0U; node < hdf5.rows.size(); ++node) {
|
|
const std::size_t row_offset = node * kComponents.size();
|
|
report.vector_metrics.push_back(
|
|
{report.rows[row_offset].instance_name,
|
|
report.rows[row_offset].source_node_label,
|
|
std::hypot(report.rows[row_offset].absolute_error,
|
|
report.rows[row_offset + 1U].absolute_error,
|
|
report.rows[row_offset + 2U].absolute_error),
|
|
std::hypot(report.rows[row_offset + 3U].absolute_error,
|
|
report.rows[row_offset + 4U].absolute_error,
|
|
report.rows[row_offset + 5U].absolute_error)});
|
|
}
|
|
return Result<Mitc4ComparisonReport>::Success(std::move(report));
|
|
} catch (const ComparisonFailure& exception) {
|
|
return Result<Mitc4ComparisonReport>::Failure(FailureStatus(
|
|
reference_case.case_id, exception.Code(), exception.what()));
|
|
} catch (const std::exception& exception) {
|
|
return Result<Mitc4ComparisonReport>::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<double>::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
|
|
<< ",\"threshold\":" << row.tolerance
|
|
<< ",\"relative_error\":" << row.normalized_error
|
|
<< ",\"reference_scale\":" << row.reference_scale
|
|
<< ",\"near_zero_band\":" << row.near_zero_band
|
|
<< ",\"relative_error_applicable\":"
|
|
<< (row.relative_error_applicable ? "true" : "false")
|
|
<< ",\"tolerance_branch\":\"" << JsonEscape(row.tolerance_branch)
|
|
<< "\""
|
|
<< ",\"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 << "{\"family_identity\":\"" << JsonEscape(metric.family_identity)
|
|
<< "\",\"components\":";
|
|
WriteJsonStrings(stream, metric.components);
|
|
stream << ",\"blocking\":" << (metric.blocking ? "true" : "false")
|
|
<< ",\"reference_scale\":" << metric.reference_scale
|
|
<< ",\"near_zero_band\":" << metric.near_zero_band
|
|
<< ",\"near_zero_count\":" << metric.near_zero_count
|
|
<< ",\"row_count\":" << metric.row_count
|
|
<< ",\"maximum_absolute_error\":" << metric.maximum_absolute_error
|
|
<< ",\"relative_rms\":" << metric.relative_rms
|
|
<< ",\"worst_row\":" << metric.worst_row
|
|
<< ",\"rms_passed\":" << (metric.rms_passed ? "true" : "false")
|
|
<< ",\"passed\":" << (metric.passed ? "true" : "false")
|
|
<< ",\"diagnostic_code\":\"" << JsonEscape(metric.diagnostic_code)
|
|
<< "\"}";
|
|
}
|
|
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
|