1511 lines
58 KiB
C++
1511 lines
58 KiB
C++
#include "reference_comparison.hpp"
|
|
|
|
#include "fesa/analysis/analysis_model.hpp"
|
|
#include "fesa/assembly/load_assembler.hpp"
|
|
#include "fesa/fem/dof_manager.hpp"
|
|
#include "fesa/io/abaqus/domain_mapper.hpp"
|
|
#include "fesa/io/abaqus/input_reader.hpp"
|
|
#include "fesa/results/result_recovery.hpp"
|
|
|
|
#include <hdf5.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <cctype>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <locale>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace fesa::test {
|
|
namespace {
|
|
|
|
constexpr const char* kModelId = "cantilever-beam-b33";
|
|
constexpr const char* kStepName = "Step-1";
|
|
constexpr std::size_t kFrameIndex = 0U;
|
|
constexpr const char* kFrameText = "Increment 1: Step Time = 1.000";
|
|
constexpr const char* kInputName = "cantilever beam.inp";
|
|
constexpr const char* kDisplacementName = "cantilever beam displacements.csv";
|
|
constexpr const char* kReactionName = "cantilever beam reactions.csv";
|
|
constexpr const char* kSectionName = "cantilever beam elemental forces.csv";
|
|
constexpr const char* kDisplacementPath =
|
|
"/steps/Step-1/frames/0/nodal/displacement";
|
|
constexpr const char* kReactionPath =
|
|
"/steps/Step-1/frames/0/nodal/reaction";
|
|
constexpr const char* kSectionPath =
|
|
"/steps/Step-1/frames/0/element/section_resultant";
|
|
constexpr const char* kStressPath =
|
|
"/steps/Step-1/frames/0/element/stress_s11";
|
|
constexpr double kKinematicFloor = 1.0e-9;
|
|
constexpr double kForceMomentFloor = 1.0e-3;
|
|
constexpr double kRelativeCoefficient = 1.0e-6;
|
|
|
|
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 comparisonFailureStatus(
|
|
const std::string& code, const std::string& message) {
|
|
return Status::failure(
|
|
FailureCategory::model,
|
|
{{Severity::error, code, {}, "", kModelId, message}});
|
|
}
|
|
|
|
std::string trim(const std::string& value) {
|
|
const auto isSpace = [](const unsigned char character) {
|
|
return std::isspace(character) != 0;
|
|
};
|
|
const auto begin = std::find_if_not(
|
|
value.begin(), value.end(), [&](const char character) {
|
|
return isSpace(static_cast<unsigned char>(character));
|
|
});
|
|
const auto end = std::find_if_not(
|
|
value.rbegin(), value.rend(), [&](const char character) {
|
|
return isSpace(static_cast<unsigned char>(character));
|
|
}).base();
|
|
return begin < end ? std::string{begin, end} : std::string{};
|
|
}
|
|
|
|
std::string collapseWhitespace(const std::string& value) {
|
|
std::string result;
|
|
bool pendingSpace = false;
|
|
for (const char character : trim(value)) {
|
|
if (std::isspace(static_cast<unsigned char>(character)) != 0) {
|
|
pendingSpace = !result.empty();
|
|
} else {
|
|
if (pendingSpace) {
|
|
result.push_back(' ');
|
|
}
|
|
result.push_back(character);
|
|
pendingSpace = false;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string asciiLower(std::string value) {
|
|
std::transform(
|
|
value.begin(), value.end(), value.begin(), [](const char character) {
|
|
if (character >= 'A' && character <= 'Z') {
|
|
return static_cast<char>(character - 'A' + 'a');
|
|
}
|
|
return character;
|
|
});
|
|
return value;
|
|
}
|
|
|
|
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 CSV or HDF5 source-node label is invalid.");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
double parseFiniteDouble(const std::string& field) {
|
|
if (field.empty()) {
|
|
fail("schema-mismatch", "A reference 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 reference numeric field is nonfinite or invalid.");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
struct WideReferenceRow {
|
|
std::string instanceName;
|
|
std::int64_t sourceNodeLabel;
|
|
std::vector<double> values;
|
|
};
|
|
|
|
struct ReferenceTable {
|
|
std::vector<WideReferenceRow> rows;
|
|
};
|
|
|
|
ReferenceTable readReferenceCsv(
|
|
const std::filesystem::path& path,
|
|
const std::vector<std::string>& expectedHeader) {
|
|
std::ifstream stream{path};
|
|
if (!stream) {
|
|
fail("needs-reference-artifacts", "An approved reference CSV is missing.");
|
|
}
|
|
std::string line;
|
|
if (!std::getline(stream, line)) {
|
|
fail("schema-mismatch", "An approved reference CSV is empty.");
|
|
}
|
|
if (!line.empty() && line.back() == '\r') {
|
|
line.pop_back();
|
|
}
|
|
if (splitCsvLine(line) != expectedHeader) {
|
|
fail("schema-mismatch", "An approved reference CSV header is not exact.");
|
|
}
|
|
|
|
ReferenceTable table;
|
|
while (std::getline(stream, line)) {
|
|
if (!line.empty() && line.back() == '\r') {
|
|
line.pop_back();
|
|
}
|
|
if (line.empty()) {
|
|
fail("schema-mismatch", "Blank reference CSV rows are not allowed.");
|
|
}
|
|
const auto fields = splitCsvLine(line);
|
|
if (fields.size() != expectedHeader.size() ||
|
|
collapseWhitespace(fields[0U]) != kFrameText ||
|
|
fields[1U].empty()) {
|
|
fail("schema-mismatch", "A reference CSV row has invalid schema or frame identity.");
|
|
}
|
|
WideReferenceRow row{};
|
|
row.instanceName = fields[1U];
|
|
row.sourceNodeLabel = parsePositiveLabel(fields[2U]);
|
|
row.values.reserve(fields.size() - 3U);
|
|
for (std::size_t field = 3U; field < fields.size(); ++field) {
|
|
row.values.push_back(parseFiniteDouble(fields[field]));
|
|
}
|
|
const auto duplicate = std::find_if(
|
|
table.rows.begin(),
|
|
table.rows.end(),
|
|
[&](const WideReferenceRow& existing) {
|
|
return asciiLower(existing.instanceName) ==
|
|
asciiLower(row.instanceName) &&
|
|
existing.sourceNodeLabel == row.sourceNodeLabel;
|
|
});
|
|
if (duplicate != table.rows.end()) {
|
|
fail("schema-mismatch", "A reference CSV row identity is duplicated.");
|
|
}
|
|
table.rows.push_back(std::move(row));
|
|
}
|
|
if (table.rows.empty()) {
|
|
fail("schema-mismatch", "An approved reference CSV has no data rows.");
|
|
}
|
|
return table;
|
|
}
|
|
|
|
void requireExactArtifactInventory(
|
|
const std::filesystem::path& legacyDirectory) {
|
|
std::error_code error;
|
|
if (!std::filesystem::is_directory(legacyDirectory, error) || error) {
|
|
fail("needs-reference-artifacts", "The approved legacy directory is missing.");
|
|
}
|
|
std::vector<std::string> names;
|
|
for (std::filesystem::directory_iterator iterator{legacyDirectory, error}, end;
|
|
iterator != end && !error;
|
|
iterator.increment(error)) {
|
|
if (!iterator->is_regular_file(error) || error) {
|
|
fail("needs-reference-artifacts", "The legacy bundle contains a non-file entry.");
|
|
}
|
|
names.push_back(iterator->path().filename().string());
|
|
}
|
|
if (error) {
|
|
fail("needs-reference-artifacts", "The legacy bundle cannot be inspected.");
|
|
}
|
|
std::sort(names.begin(), names.end());
|
|
std::vector<std::string> expected = {
|
|
kDisplacementName, kInputName, kReactionName, kSectionName};
|
|
std::sort(expected.begin(), expected.end());
|
|
if (names != expected) {
|
|
fail("needs-reference-artifacts", "The legacy bundle inventory is not exact.");
|
|
}
|
|
}
|
|
|
|
Domain readApprovedDomain(const std::filesystem::path& inputPath) {
|
|
AbaqusInputReader reader;
|
|
auto parsed = reader.read(inputPath);
|
|
if (!parsed.hasValue()) {
|
|
fail("needs-reference-artifacts", "The approved reference input cannot be parsed.");
|
|
}
|
|
AbaqusDomainMapper mapper;
|
|
auto domain = mapper.map(parsed.value());
|
|
if (!domain.hasValue()) {
|
|
fail(
|
|
"needs-reference-artifacts",
|
|
"The approved reference input is not the required B33 model.");
|
|
}
|
|
return std::move(domain.value());
|
|
}
|
|
|
|
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& operator=(Hdf5Handle&& other) noexcept {
|
|
if (this != &other) {
|
|
reset();
|
|
value_ = other.value_;
|
|
closer_ = other.closer_;
|
|
other.value_ = -1;
|
|
other.closer_ = nullptr;
|
|
}
|
|
return *this;
|
|
}
|
|
~Hdf5Handle() { reset(); }
|
|
|
|
hid_t get() const noexcept { return value_; }
|
|
|
|
private:
|
|
void reset() noexcept {
|
|
if (value_ >= 0 && closer_ != nullptr) {
|
|
(void)closer_(value_);
|
|
}
|
|
value_ = -1;
|
|
closer_ = nullptr;
|
|
}
|
|
|
|
hid_t value_{-1};
|
|
Closer closer_{nullptr};
|
|
};
|
|
|
|
class Hdf5ErrorSilencer {
|
|
public:
|
|
Hdf5ErrorSilencer() {
|
|
if (H5Eget_auto2(H5E_DEFAULT, &callback_, &clientData_) >= 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_, clientData_);
|
|
}
|
|
}
|
|
|
|
private:
|
|
H5E_auto2_t callback_{nullptr};
|
|
void* clientData_{nullptr};
|
|
bool active_{false};
|
|
};
|
|
|
|
class Hdf5VlenReclaimer {
|
|
public:
|
|
Hdf5VlenReclaimer(
|
|
const hid_t memoryType,
|
|
const hid_t dataSpace,
|
|
void* const data) noexcept
|
|
: memoryType_{memoryType}, dataSpace_{dataSpace}, data_{data} {}
|
|
Hdf5VlenReclaimer(const Hdf5VlenReclaimer&) = delete;
|
|
Hdf5VlenReclaimer& operator=(const Hdf5VlenReclaimer&) = delete;
|
|
~Hdf5VlenReclaimer() {
|
|
if (active_) {
|
|
(void)H5Dvlen_reclaim(
|
|
memoryType_, dataSpace_, H5P_DEFAULT, data_);
|
|
}
|
|
}
|
|
|
|
herr_t reclaim() noexcept {
|
|
active_ = false;
|
|
return H5Dvlen_reclaim(
|
|
memoryType_, dataSpace_, H5P_DEFAULT, data_);
|
|
}
|
|
|
|
private:
|
|
hid_t memoryType_;
|
|
hid_t dataSpace_;
|
|
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 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> datasetDimensions(const hid_t dataset) {
|
|
Hdf5Handle space{
|
|
requireId(H5Dget_space(dataset), "Unable to inspect an HDF5 dataspace."),
|
|
H5Sclose};
|
|
const int rank = H5Sget_simple_extent_ndims(space.get());
|
|
if (rank < 0) {
|
|
fail("schema-mismatch", "Unable to inspect an HDF5 dataset rank.");
|
|
}
|
|
std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank));
|
|
if (rank > 0) {
|
|
requireHdf(
|
|
H5Sget_simple_extent_dims(space.get(), dimensions.data(), nullptr),
|
|
"Unable to inspect HDF5 dataset dimensions.");
|
|
}
|
|
return dimensions;
|
|
}
|
|
|
|
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 HDF5 string 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 value{raw};
|
|
requireHdf(H5free_memory(raw), "Unable to release HDF5 string memory.");
|
|
return value;
|
|
}
|
|
|
|
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 HDF5 integer 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 value = 0U;
|
|
requireHdf(
|
|
H5Aread(attribute.get(), H5T_NATIVE_UINT64, &value),
|
|
"Unable to read an HDF5 integer attribute.");
|
|
return value;
|
|
}
|
|
|
|
void requireStringAttribute(
|
|
const hid_t object, const char* name, const char* expected) {
|
|
if (readStringAttribute(object, name) != expected) {
|
|
fail("schema-mismatch", "An HDF5 string attribute has the wrong value.");
|
|
}
|
|
}
|
|
|
|
void requireResultAttributes(
|
|
const hid_t dataset,
|
|
const char* components,
|
|
const char* units,
|
|
const char* coordinateSystem,
|
|
const char* location) {
|
|
requireStringAttribute(dataset, "component_names", components);
|
|
requireStringAttribute(dataset, "component_unit_dimensions", units);
|
|
requireStringAttribute(dataset, "coordinate_system", coordinateSystem);
|
|
requireStringAttribute(dataset, "location", location);
|
|
requireStringAttribute(dataset, "step_name", kStepName);
|
|
if (readUint64Attribute(dataset, "frame_index") != kFrameIndex) {
|
|
fail("schema-mismatch", "An HDF5 result has the wrong frame identity.");
|
|
}
|
|
}
|
|
|
|
std::vector<double> readDoubleDataset(
|
|
const hid_t file,
|
|
const char* path,
|
|
const std::vector<hsize_t>& expectedDimensions,
|
|
const char* components,
|
|
const char* units,
|
|
const char* coordinateSystem,
|
|
const char* location) {
|
|
auto dataset = openDataset(file, path);
|
|
if (datasetDimensions(dataset.get()) != expectedDimensions) {
|
|
fail("schema-mismatch", "An HDF5 result dataset has the wrong shape.");
|
|
}
|
|
Hdf5Handle type{
|
|
requireId(H5Dget_type(dataset.get()),
|
|
"Unable to inspect an HDF5 result 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", "An HDF5 result dataset is not float64.");
|
|
}
|
|
requireResultAttributes(
|
|
dataset.get(), components, units, coordinateSystem, location);
|
|
std::size_t count = 1U;
|
|
for (const hsize_t dimension : expectedDimensions) {
|
|
if (dimension > (std::numeric_limits<std::size_t>::max)() / count) {
|
|
fail("schema-mismatch", "An HDF5 result shape overflows size_t.");
|
|
}
|
|
count *= static_cast<std::size_t>(dimension);
|
|
}
|
|
std::vector<double> values(count);
|
|
if (!values.empty()) {
|
|
requireHdf(
|
|
H5Dread(
|
|
dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
|
H5P_DEFAULT, values.data()),
|
|
"Unable to read an HDF5 result dataset.");
|
|
}
|
|
if (!std::all_of(values.begin(), values.end(), [](const double value) {
|
|
return std::isfinite(value);
|
|
})) {
|
|
fail("schema-mismatch", "An HDF5 comparison value is nonfinite.");
|
|
}
|
|
return values;
|
|
}
|
|
|
|
void requireCompoundMembers(
|
|
const hid_t dataset, const std::vector<std::string>& 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 dataset has the wrong schema.");
|
|
}
|
|
for (std::size_t index = 0U; index < expected.size(); ++index) {
|
|
char* raw = H5Tget_member_name(type.get(), static_cast<unsigned>(index));
|
|
if (raw == nullptr) {
|
|
fail("schema-mismatch", "Unable to inspect an HDF5 member name.");
|
|
}
|
|
const std::string actual{raw};
|
|
requireHdf(H5free_memory(raw), "Unable to release an HDF5 member name.");
|
|
if (actual != expected[index]) {
|
|
fail("schema-mismatch", "An HDF5 compound member has the wrong name.");
|
|
}
|
|
}
|
|
}
|
|
|
|
Hdf5Handle makeVariableStringType() {
|
|
Hdf5Handle type{
|
|
requireId(H5Tcopy(H5T_C_S1), "Unable to create an HDF5 string type."),
|
|
H5Tclose};
|
|
requireHdf(
|
|
H5Tset_size(type.get(), H5T_VARIABLE),
|
|
"Unable to size an HDF5 string type.");
|
|
requireHdf(
|
|
H5Tset_cset(type.get(), H5T_CSET_UTF8),
|
|
"Unable to configure an HDF5 string type.");
|
|
return type;
|
|
}
|
|
|
|
struct NodeMemoryRow {
|
|
std::uint64_t internalNodeId;
|
|
char* instanceName;
|
|
char* sourceLabel;
|
|
double coordinates[3];
|
|
};
|
|
|
|
struct ElementMemoryRow {
|
|
std::uint64_t internalElementId;
|
|
char* instanceName;
|
|
char* sourceLabel;
|
|
std::uint64_t nodeInternalIds[2];
|
|
double localAxes[9];
|
|
};
|
|
|
|
struct HdfNode {
|
|
std::uint64_t internalNodeId;
|
|
std::string instanceName;
|
|
std::int64_t sourceNodeLabel;
|
|
std::string sourceNodeLabelText;
|
|
std::array<double, 3> coordinates;
|
|
};
|
|
|
|
struct HdfElement {
|
|
std::uint64_t internalElementId;
|
|
std::string instanceName;
|
|
std::int64_t sourceElementLabel;
|
|
std::string sourceElementLabelText;
|
|
std::array<std::uint64_t, 2> nodeInternalIds;
|
|
std::array<double, 9> localAxes;
|
|
};
|
|
|
|
std::vector<HdfNode> readNodeRows(const hid_t file) {
|
|
auto dataset = openDataset(file, "/model/nodes");
|
|
const auto dimensions = datasetDimensions(dataset.get());
|
|
if (dimensions.size() != 1U || dimensions[0U] == 0U) {
|
|
fail("schema-mismatch", "The HDF5 node table has the wrong shape.");
|
|
}
|
|
requireCompoundMembers(
|
|
dataset.get(),
|
|
{"internal_node_id", "instance_name", "source_label", "coordinates"});
|
|
requireStringAttribute(dataset.get(), "coordinate_system", "global-cartesian");
|
|
requireStringAttribute(dataset.get(), "units_label", "length");
|
|
|
|
auto stringType = makeVariableStringType();
|
|
const hsize_t coordinateDimensions[1] = {3U};
|
|
Hdf5Handle coordinateType{
|
|
requireId(
|
|
H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions),
|
|
"Unable to create the node coordinate memory type."),
|
|
H5Tclose};
|
|
Hdf5Handle memoryType{
|
|
requireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeMemoryRow)),
|
|
"Unable to create the node memory type."),
|
|
H5Tclose};
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "internal_node_id",
|
|
HOFFSET(NodeMemoryRow, internalNodeId), H5T_NATIVE_UINT64),
|
|
"Unable to define the node ID memory field.");
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "instance_name",
|
|
HOFFSET(NodeMemoryRow, instanceName), stringType.get()),
|
|
"Unable to define the node instance memory field.");
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "source_label",
|
|
HOFFSET(NodeMemoryRow, sourceLabel), stringType.get()),
|
|
"Unable to define the node label memory field.");
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "coordinates",
|
|
HOFFSET(NodeMemoryRow, coordinates), coordinateType.get()),
|
|
"Unable to define the node coordinate memory field.");
|
|
|
|
std::vector<NodeMemoryRow> raw(static_cast<std::size_t>(dimensions[0U]));
|
|
Hdf5Handle space{
|
|
requireId(H5Dget_space(dataset.get()),
|
|
"Unable to reopen the node dataspace."),
|
|
H5Sclose};
|
|
requireHdf(
|
|
H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL,
|
|
H5P_DEFAULT, raw.data()),
|
|
"Unable to read the HDF5 node table.");
|
|
Hdf5VlenReclaimer strings{memoryType.get(), space.get(), raw.data()};
|
|
std::vector<HdfNode> rows;
|
|
rows.reserve(raw.size());
|
|
for (const auto& row : raw) {
|
|
if (row.instanceName == nullptr || row.sourceLabel == nullptr) {
|
|
fail("schema-mismatch", "An HDF5 node identity is null.");
|
|
}
|
|
const std::array<double, 3> coordinates = {
|
|
row.coordinates[0U], row.coordinates[1U], row.coordinates[2U]};
|
|
if (!std::all_of(
|
|
coordinates.begin(), coordinates.end(), [](const double value) {
|
|
return std::isfinite(value);
|
|
})) {
|
|
fail("schema-mismatch", "An HDF5 node coordinate is nonfinite.");
|
|
}
|
|
rows.push_back({
|
|
row.internalNodeId,
|
|
row.instanceName,
|
|
parsePositiveLabel(row.sourceLabel),
|
|
row.sourceLabel,
|
|
coordinates});
|
|
}
|
|
requireHdf(
|
|
strings.reclaim(),
|
|
"Unable to reclaim HDF5 node strings.");
|
|
return rows;
|
|
}
|
|
|
|
std::vector<HdfElement> readElementRows(const hid_t file) {
|
|
auto dataset = openDataset(file, "/model/elements");
|
|
const auto dimensions = datasetDimensions(dataset.get());
|
|
if (dimensions.size() != 1U || dimensions[0U] == 0U) {
|
|
fail("schema-mismatch", "The HDF5 element table has the wrong shape.");
|
|
}
|
|
requireCompoundMembers(
|
|
dataset.get(),
|
|
{"internal_element_id", "instance_name", "source_label",
|
|
"node_internal_ids", "local_axes"});
|
|
requireStringAttribute(dataset.get(), "formulation", "B33-3D-Euler-Bernoulli");
|
|
|
|
auto stringType = makeVariableStringType();
|
|
const hsize_t connectivityDimensions[1] = {2U};
|
|
const hsize_t axesDimensions[2] = {3U, 3U};
|
|
Hdf5Handle connectivityType{
|
|
requireId(
|
|
H5Tarray_create2(H5T_NATIVE_UINT64, 1, connectivityDimensions),
|
|
"Unable to create the connectivity memory type."),
|
|
H5Tclose};
|
|
Hdf5Handle axesType{
|
|
requireId(
|
|
H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axesDimensions),
|
|
"Unable to create the local-axis memory type."),
|
|
H5Tclose};
|
|
Hdf5Handle memoryType{
|
|
requireId(H5Tcreate(H5T_COMPOUND, sizeof(ElementMemoryRow)),
|
|
"Unable to create the element memory type."),
|
|
H5Tclose};
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "internal_element_id",
|
|
HOFFSET(ElementMemoryRow, internalElementId),
|
|
H5T_NATIVE_UINT64),
|
|
"Unable to define the element ID memory field.");
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "instance_name",
|
|
HOFFSET(ElementMemoryRow, instanceName), stringType.get()),
|
|
"Unable to define the element instance memory field.");
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "source_label",
|
|
HOFFSET(ElementMemoryRow, sourceLabel), stringType.get()),
|
|
"Unable to define the element label memory field.");
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "node_internal_ids",
|
|
HOFFSET(ElementMemoryRow, nodeInternalIds),
|
|
connectivityType.get()),
|
|
"Unable to define the connectivity memory field.");
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "local_axes",
|
|
HOFFSET(ElementMemoryRow, localAxes), axesType.get()),
|
|
"Unable to define the local-axis memory field.");
|
|
|
|
std::vector<ElementMemoryRow> raw(static_cast<std::size_t>(dimensions[0U]));
|
|
Hdf5Handle space{
|
|
requireId(H5Dget_space(dataset.get()),
|
|
"Unable to reopen the element dataspace."),
|
|
H5Sclose};
|
|
requireHdf(
|
|
H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL,
|
|
H5P_DEFAULT, raw.data()),
|
|
"Unable to read the HDF5 element table.");
|
|
Hdf5VlenReclaimer strings{memoryType.get(), space.get(), raw.data()};
|
|
std::vector<HdfElement> rows;
|
|
rows.reserve(raw.size());
|
|
for (const auto& row : raw) {
|
|
if (row.instanceName == nullptr || row.sourceLabel == nullptr) {
|
|
fail("schema-mismatch", "An HDF5 element identity is null.");
|
|
}
|
|
HdfElement converted{};
|
|
converted.internalElementId = row.internalElementId;
|
|
converted.instanceName = row.instanceName;
|
|
converted.sourceElementLabel = parsePositiveLabel(row.sourceLabel);
|
|
converted.sourceElementLabelText = row.sourceLabel;
|
|
converted.nodeInternalIds = {
|
|
row.nodeInternalIds[0U], row.nodeInternalIds[1U]};
|
|
std::copy(
|
|
std::begin(row.localAxes), std::end(row.localAxes),
|
|
converted.localAxes.begin());
|
|
if (!std::all_of(
|
|
converted.localAxes.begin(), converted.localAxes.end(),
|
|
[](const double value) { return std::isfinite(value); })) {
|
|
fail("schema-mismatch", "An HDF5 local axis is nonfinite.");
|
|
}
|
|
rows.push_back(std::move(converted));
|
|
}
|
|
requireHdf(
|
|
strings.reclaim(),
|
|
"Unable to reclaim HDF5 element strings.");
|
|
return rows;
|
|
}
|
|
|
|
void requireFiniteStress(const hid_t file) {
|
|
auto dataset = openDataset(file, kStressPath);
|
|
const auto dimensions = datasetDimensions(dataset.get());
|
|
if (dimensions.size() != 1U || dimensions[0U] == 0U) {
|
|
fail("schema-mismatch", "The mandatory stress dataset has no rows.");
|
|
}
|
|
requireCompoundMembers(
|
|
dataset.get(),
|
|
{"internal_element_id", "gauss_point_index", "section_point_index",
|
|
"x1", "x2", "source", "S11"});
|
|
requireResultAttributes(
|
|
dataset.get(), "S11", "force/length^2", "beam-local", "section-point");
|
|
struct StressValue {
|
|
double s11;
|
|
};
|
|
Hdf5Handle memoryType{
|
|
requireId(H5Tcreate(H5T_COMPOUND, sizeof(StressValue)),
|
|
"Unable to create a stress memory type."),
|
|
H5Tclose};
|
|
requireHdf(
|
|
H5Tinsert(memoryType.get(), "S11", HOFFSET(StressValue, s11),
|
|
H5T_NATIVE_DOUBLE),
|
|
"Unable to define the stress memory field.");
|
|
std::vector<StressValue> values(static_cast<std::size_t>(dimensions[0U]));
|
|
requireHdf(
|
|
H5Dread(dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL,
|
|
H5P_DEFAULT, values.data()),
|
|
"Unable to read the stress dataset.");
|
|
if (!std::all_of(values.begin(), values.end(), [](const StressValue& value) {
|
|
return std::isfinite(value.s11);
|
|
})) {
|
|
fail("schema-mismatch", "The mandatory stress dataset is nonfinite.");
|
|
}
|
|
}
|
|
|
|
std::array<double, 9> expectedLocalAxes(
|
|
const Domain& domain, const EulerBeam3DDefinition& element) {
|
|
const auto& first = domain.nodes()[element.nodeIndices[0U]].coordinates;
|
|
const auto& second = domain.nodes()[element.nodeIndices[1U]].coordinates;
|
|
const auto& guide = domain.sections()[element.sectionIndex].firstAxis;
|
|
const std::array<double, 3> delta = {
|
|
second[0U] - first[0U],
|
|
second[1U] - first[1U],
|
|
second[2U] - first[2U]};
|
|
const double length = std::hypot(delta[0U], delta[1U], delta[2U]);
|
|
const std::array<double, 3> x = {
|
|
delta[0U] / length, delta[1U] / length, delta[2U] / length};
|
|
const double projection =
|
|
guide[0U] * x[0U] + guide[1U] * x[1U] + guide[2U] * x[2U];
|
|
const std::array<double, 3> yTrial = {
|
|
guide[0U] - projection * x[0U],
|
|
guide[1U] - projection * x[1U],
|
|
guide[2U] - projection * x[2U]};
|
|
const double yNorm = std::hypot(yTrial[0U], yTrial[1U], yTrial[2U]);
|
|
const std::array<double, 3> y = {
|
|
yTrial[0U] / yNorm, yTrial[1U] / yNorm, yTrial[2U] / yNorm};
|
|
const std::array<double, 3> z = {
|
|
x[1U] * y[2U] - x[2U] * y[1U],
|
|
x[2U] * y[0U] - x[0U] * y[2U],
|
|
x[0U] * y[1U] - x[1U] * y[0U]};
|
|
return {
|
|
x[0U], x[1U], x[2U],
|
|
y[0U], y[1U], y[2U],
|
|
z[0U], z[1U], z[2U]};
|
|
}
|
|
|
|
struct HdfProjection {
|
|
std::vector<HdfNode> nodes;
|
|
std::vector<HdfElement> elements;
|
|
std::vector<double> displacement;
|
|
std::vector<double> reaction;
|
|
std::vector<double> sectionResultants;
|
|
};
|
|
|
|
HdfProjection readHdfProjection(
|
|
const std::filesystem::path& results,
|
|
const std::filesystem::path& input,
|
|
const Domain& domain) {
|
|
std::error_code error;
|
|
if (!std::filesystem::is_regular_file(results, error) || error) {
|
|
fail("needs-solver-results", "The authoritative FESA results.h5 is missing.");
|
|
}
|
|
Hdf5ErrorSilencer silence;
|
|
if (H5Fis_hdf5(results.string().c_str()) <= 0) {
|
|
fail("schema-mismatch", "The solver result is not an HDF5 file.");
|
|
}
|
|
Hdf5Handle file{
|
|
requireId(H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
|
|
"Unable to open the solver HDF5 file read-only."),
|
|
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") != kFrameIndex) {
|
|
fail("schema-mismatch", "The HDF5 schema or frame version is wrong.");
|
|
}
|
|
requireStringAttribute(
|
|
metadata.get(), "feature_id", "linear-static-3d-euler-beam");
|
|
requireStringAttribute(
|
|
metadata.get(), "unit_system_label", "user-consistent-unspecified");
|
|
requireStringAttribute(
|
|
metadata.get(), "coordinate_convention",
|
|
"global-cartesian; beam-local=(t,n1,t-cross-n1)");
|
|
requireStringAttribute(
|
|
metadata.get(), "element_formulation", "B33-3D-Euler-Bernoulli");
|
|
requireStringAttribute(metadata.get(), "step_name", kStepName);
|
|
const std::string sourceIdentity =
|
|
readStringAttribute(metadata.get(), "source_input_identity");
|
|
const std::string normalizedInput =
|
|
std::filesystem::absolute(input).lexically_normal().generic_u8string();
|
|
const std::string expectedIdentity =
|
|
"path=" + normalizedInput +
|
|
";content_identity=" + domain.sourceContentIdentity();
|
|
if (sourceIdentity != expectedIdentity) {
|
|
fail("schema-mismatch", "The HDF5 source-input identity is inconsistent.");
|
|
}
|
|
|
|
HdfProjection projection{};
|
|
projection.nodes = readNodeRows(file.get());
|
|
projection.elements = readElementRows(file.get());
|
|
if (projection.nodes.size() != domain.nodes().size() ||
|
|
projection.elements.size() != domain.elements().size()) {
|
|
fail("schema-mismatch", "HDF5 model identity counts do not match the input.");
|
|
}
|
|
for (std::size_t node = 0U; node < projection.nodes.size(); ++node) {
|
|
const auto& actual = projection.nodes[node];
|
|
const auto& expected = domain.nodes()[node];
|
|
if (actual.internalNodeId != node ||
|
|
actual.instanceName != expected.sourceId.instanceName ||
|
|
actual.sourceNodeLabel != expected.sourceId.sourceLabel ||
|
|
actual.sourceNodeLabelText != expected.sourceId.sourceLabelText ||
|
|
actual.coordinates != expected.coordinates) {
|
|
fail("schema-mismatch", "An HDF5 node identity does not match the input.");
|
|
}
|
|
}
|
|
for (std::size_t element = 0U; element < projection.elements.size(); ++element) {
|
|
const auto& actual = projection.elements[element];
|
|
const auto& expected = domain.elements()[element];
|
|
if (actual.internalElementId != element ||
|
|
actual.instanceName != expected.sourceId.instanceName ||
|
|
actual.sourceElementLabel != expected.sourceId.sourceLabel ||
|
|
actual.sourceElementLabelText != expected.sourceId.sourceLabelText ||
|
|
actual.nodeInternalIds[0U] != expected.nodeIndices[0U] ||
|
|
actual.nodeInternalIds[1U] != expected.nodeIndices[1U]) {
|
|
fail("schema-mismatch", "An HDF5 element identity does not match the input.");
|
|
}
|
|
const auto axes = expectedLocalAxes(domain, expected);
|
|
for (std::size_t component = 0U; component < axes.size(); ++component) {
|
|
if (std::abs(actual.localAxes[component] - axes[component]) > 1.0e-12) {
|
|
fail("schema-mismatch", "An HDF5 local axis does not match the input.");
|
|
}
|
|
}
|
|
}
|
|
|
|
const hsize_t nodeCount = static_cast<hsize_t>(projection.nodes.size());
|
|
const hsize_t elementCount = static_cast<hsize_t>(projection.elements.size());
|
|
projection.displacement = readDoubleDataset(
|
|
file.get(), kDisplacementPath, {nodeCount, 6U},
|
|
"UX,UY,UZ,URX,URY,URZ",
|
|
"length,length,length,radian,radian,radian",
|
|
"global-cartesian", "nodal");
|
|
projection.reaction = readDoubleDataset(
|
|
file.get(), kReactionPath, {nodeCount, 6U},
|
|
"RF1,RF2,RF3,RM1,RM2,RM3",
|
|
"force,force,force,force*length,force*length,force*length",
|
|
"global-cartesian", "nodal");
|
|
projection.sectionResultants = readDoubleDataset(
|
|
file.get(), kSectionPath, {elementCount, 2U, 4U},
|
|
"N,T,My,Mz",
|
|
"force,force*length,force*length,force*length",
|
|
"beam-local", "endpoint-positive-local-x-section-cut");
|
|
requireFiniteStress(file.get());
|
|
return projection;
|
|
}
|
|
|
|
std::vector<const WideReferenceRow*> orderedRows(
|
|
const ReferenceTable& table, const std::vector<HdfNode>& nodes) {
|
|
if (table.rows.size() != nodes.size()) {
|
|
fail("schema-mismatch", "The FESA/reference projected row sets differ.");
|
|
}
|
|
std::vector<const WideReferenceRow*> ordered;
|
|
ordered.reserve(nodes.size());
|
|
for (const auto& node : nodes) {
|
|
const auto found = std::find_if(
|
|
table.rows.begin(), table.rows.end(), [&](const WideReferenceRow& row) {
|
|
return asciiLower(row.instanceName) ==
|
|
asciiLower(node.instanceName) &&
|
|
row.sourceNodeLabel == node.sourceNodeLabel;
|
|
});
|
|
if (found == table.rows.end() || found->instanceName != node.instanceName) {
|
|
fail("schema-mismatch", "A reference row identity does not match HDF5.");
|
|
}
|
|
ordered.push_back(&*found);
|
|
}
|
|
return ordered;
|
|
}
|
|
|
|
double tableScale(const ReferenceTable& table, const std::size_t valueIndex) {
|
|
double scale = 0.0;
|
|
for (const auto& row : table.rows) {
|
|
if (valueIndex >= row.values.size()) {
|
|
fail("schema-mismatch", "A reference row has the wrong component arity.");
|
|
}
|
|
scale = (std::max)(scale, std::abs(row.values[valueIndex]));
|
|
}
|
|
return scale;
|
|
}
|
|
|
|
std::vector<NodeStationResultRow> normalizeStations(
|
|
const Domain& domain,
|
|
const HdfProjection& hdf,
|
|
const ReferenceTable& sectionTable) {
|
|
auto modelResult = AnalysisModel::create(domain);
|
|
if (!modelResult.hasValue()) {
|
|
fail("schema-mismatch", "The approved input cannot create an analysis view.");
|
|
}
|
|
const AnalysisModel model = std::move(modelResult.value());
|
|
const std::array<double, 4> tolerances = {
|
|
kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 0U),
|
|
kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 3U),
|
|
kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 1U),
|
|
kForceMomentFloor + kRelativeCoefficient * tableScale(sectionTable, 2U)};
|
|
std::vector<EndpointResultRow> endpoints;
|
|
endpoints.reserve(hdf.elements.size() * 2U);
|
|
for (std::size_t element = 0U; element < hdf.elements.size(); ++element) {
|
|
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
|
|
const auto node = static_cast<std::size_t>(
|
|
hdf.elements[element].nodeInternalIds[endpoint]);
|
|
std::array<double, 4> values{};
|
|
for (std::size_t component = 0U; component < values.size(); ++component) {
|
|
values[component] =
|
|
hdf.sectionResultants[(element * 2U + endpoint) * 4U + component];
|
|
}
|
|
endpoints.push_back({
|
|
static_cast<EntityIndex>(element),
|
|
static_cast<int>(endpoint),
|
|
domain.nodes()[node].sourceId,
|
|
{},
|
|
values});
|
|
}
|
|
}
|
|
auto normalized = ResultRecovery::normalizeSectionResultantsToNodeStations(
|
|
model, endpoints, tolerances);
|
|
if (!normalized.hasValue()) {
|
|
const auto& diagnostics = normalized.status().diagnostics();
|
|
const std::string code = diagnostics.empty() ? std::string{} : diagnostics[0U].code;
|
|
if (code == "node-station-tolerance-failure") {
|
|
fail("tolerance-failure", "Interior endpoint section resultants disagree.");
|
|
}
|
|
fail("schema-mismatch", "A node station is not eligible for legacy projection.");
|
|
}
|
|
return std::move(normalized.value());
|
|
}
|
|
|
|
const NodeStationResultRow& findStation(
|
|
const std::vector<NodeStationResultRow>& stations,
|
|
const HdfNode& node) {
|
|
const auto found = std::find_if(
|
|
stations.begin(), stations.end(), [&](const NodeStationResultRow& row) {
|
|
return row.node.instanceName == node.instanceName &&
|
|
row.node.sourceLabel == node.sourceNodeLabel;
|
|
});
|
|
if (found == stations.end()) {
|
|
fail("schema-mismatch", "A projected HDF5 node station is missing.");
|
|
}
|
|
return *found;
|
|
}
|
|
|
|
CanonicalComparisonRow canonicalRow(
|
|
const HdfNode& node,
|
|
const ComparisonQuantity quantity,
|
|
std::string component,
|
|
const double value,
|
|
std::string unit,
|
|
std::string coordinateSystem,
|
|
std::string datasetPath) {
|
|
return {
|
|
kModelId,
|
|
kStepName,
|
|
kFrameIndex,
|
|
node.instanceName,
|
|
node.sourceNodeLabel,
|
|
quantity,
|
|
std::move(component),
|
|
value,
|
|
std::move(unit),
|
|
std::move(coordinateSystem),
|
|
std::move(datasetPath)};
|
|
}
|
|
|
|
void appendNodalRows(
|
|
ComparisonReport& report,
|
|
const HdfProjection& hdf,
|
|
const std::vector<const WideReferenceRow*>& reference,
|
|
const ComparisonQuantity quantity,
|
|
const std::array<std::string, 6>& components,
|
|
const std::array<std::string, 6>& units,
|
|
const std::vector<double>& fesaValues,
|
|
const char* datasetPath) {
|
|
for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) {
|
|
for (std::size_t component = 0U; component < components.size(); ++component) {
|
|
auto fesa = canonicalRow(
|
|
hdf.nodes[node], quantity, components[component],
|
|
fesaValues[node * 6U + component], units[component],
|
|
"global-cartesian", datasetPath);
|
|
auto abaqus = canonicalRow(
|
|
hdf.nodes[node], quantity, components[component],
|
|
reference[node]->values[component], units[component],
|
|
"global-cartesian", datasetPath);
|
|
report.rows.push_back(
|
|
{std::move(fesa), std::move(abaqus), 0.0, 0.0, false});
|
|
}
|
|
}
|
|
}
|
|
|
|
void appendSectionRows(
|
|
ComparisonReport& report,
|
|
const HdfProjection& hdf,
|
|
const std::vector<const WideReferenceRow*>& reference,
|
|
const std::vector<NodeStationResultRow>& stations) {
|
|
const std::array<std::string, 4> components = {"N", "T", "My", "Mz"};
|
|
const std::array<std::string, 4> units = {
|
|
"force", "force*length", "force*length", "force*length"};
|
|
const std::array<std::size_t, 4> referenceColumns = {0U, 3U, 1U, 2U};
|
|
for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) {
|
|
const auto& station = findStation(stations, hdf.nodes[node]);
|
|
for (std::size_t component = 0U; component < components.size(); ++component) {
|
|
auto fesa = canonicalRow(
|
|
hdf.nodes[node], ComparisonQuantity::sectionResultant,
|
|
components[component], station.sectionResultant[component],
|
|
units[component], "beam-local", kSectionPath);
|
|
auto abaqus = canonicalRow(
|
|
hdf.nodes[node], ComparisonQuantity::sectionResultant,
|
|
components[component],
|
|
reference[node]->values[referenceColumns[component]],
|
|
units[component], "beam-local", kSectionPath);
|
|
report.rows.push_back(
|
|
{std::move(fesa), std::move(abaqus), 0.0, 0.0, false});
|
|
}
|
|
}
|
|
}
|
|
|
|
double absoluteFloor(
|
|
const ComparisonQuantity quantity, const std::string&) {
|
|
return quantity == ComparisonQuantity::displacement
|
|
? kKinematicFloor
|
|
: kForceMomentFloor;
|
|
}
|
|
|
|
void evaluateGroup(
|
|
ComparisonReport& report,
|
|
const ComparisonQuantity quantity,
|
|
const std::string& component) {
|
|
std::vector<std::size_t> rowIndices;
|
|
for (std::size_t index = 0U; index < report.rows.size(); ++index) {
|
|
if (report.rows[index].reference.quantity == quantity &&
|
|
report.rows[index].reference.component == component) {
|
|
rowIndices.push_back(index);
|
|
}
|
|
}
|
|
if (rowIndices.empty()) {
|
|
fail("schema-mismatch", "A canonical comparison component has no rows.");
|
|
}
|
|
double referenceScale = 0.0;
|
|
for (const std::size_t index : rowIndices) {
|
|
referenceScale = (std::max)(
|
|
referenceScale, std::abs(report.rows[index].reference.value));
|
|
}
|
|
const double tolerance =
|
|
absoluteFloor(quantity, component) +
|
|
kRelativeCoefficient * referenceScale;
|
|
double maximumAbsolute = -1.0;
|
|
double maximumNormalized = 0.0;
|
|
std::size_t worstRow = rowIndices.front();
|
|
long double squaredError = 0.0L;
|
|
for (const std::size_t index : rowIndices) {
|
|
auto& row = report.rows[index];
|
|
row.absoluteError = std::abs(row.fesa.value - row.reference.value);
|
|
if (!std::isfinite(row.absoluteError)) {
|
|
fail("schema-mismatch", "A canonical row error is nonfinite.");
|
|
}
|
|
row.tolerance = tolerance;
|
|
row.passed = row.absoluteError <= tolerance;
|
|
report.passed = report.passed && row.passed;
|
|
const double normalized = row.absoluteError / tolerance;
|
|
if (row.absoluteError > maximumAbsolute) {
|
|
maximumAbsolute = row.absoluteError;
|
|
worstRow = index;
|
|
}
|
|
maximumNormalized = (std::max)(maximumNormalized, normalized);
|
|
const long double error = static_cast<long double>(row.absoluteError);
|
|
squaredError += error * error;
|
|
}
|
|
const double normError = std::sqrt(static_cast<double>(squaredError));
|
|
const double rmsError = std::sqrt(
|
|
static_cast<double>(squaredError /
|
|
static_cast<long double>(rowIndices.size())));
|
|
if (!std::isfinite(normError) || !std::isfinite(rmsError)) {
|
|
fail("schema-mismatch", "A component aggregate error is nonfinite.");
|
|
}
|
|
report.metrics.push_back({
|
|
quantity,
|
|
component,
|
|
referenceScale,
|
|
maximumAbsolute,
|
|
maximumNormalized,
|
|
rmsError,
|
|
normError,
|
|
worstRow});
|
|
}
|
|
|
|
PhysicsEvidence makePhysicsEvidence(
|
|
const Domain& domain,
|
|
const HdfProjection& hdf) {
|
|
auto modelResult = AnalysisModel::create(domain);
|
|
if (!modelResult.hasValue()) {
|
|
fail("schema-mismatch", "The approved input cannot create physics evidence.");
|
|
}
|
|
const AnalysisModel model = std::move(modelResult.value());
|
|
auto dofsResult = DofManager::create(model);
|
|
if (!dofsResult.hasValue()) {
|
|
fail("schema-mismatch", "The approved input cannot create a DOF map.");
|
|
}
|
|
const DofManager dofs = std::move(dofsResult.value());
|
|
auto loadResult = LoadAssembler::assembleFullNodalLoad(model, dofs);
|
|
if (!loadResult.hasValue()) {
|
|
fail("schema-mismatch", "The approved input load cannot be assembled.");
|
|
}
|
|
const Vector load = std::move(loadResult.value());
|
|
if (load.size() != hdf.reaction.size()) {
|
|
fail("schema-mismatch", "The load and reaction spaces are inconsistent.");
|
|
}
|
|
|
|
PhysicsEvidence evidence{};
|
|
long double residualSquared = 0.0L;
|
|
for (const std::size_t freeDof : dofs.freeDofs()) {
|
|
const long double value =
|
|
static_cast<long double>(hdf.reaction[freeDof]);
|
|
residualSquared += value * value;
|
|
}
|
|
evidence.freeResidualNorm =
|
|
std::sqrt(static_cast<double>(residualSquared));
|
|
for (std::size_t node = 0U; node < domain.nodes().size(); ++node) {
|
|
const auto& coordinates = domain.nodes()[node].coordinates;
|
|
const std::array<double, 3> applied = {
|
|
load[node * 6U + 0U],
|
|
load[node * 6U + 1U],
|
|
load[node * 6U + 2U]};
|
|
const std::array<double, 3> reaction = {
|
|
hdf.reaction[node * 6U + 0U],
|
|
hdf.reaction[node * 6U + 1U],
|
|
hdf.reaction[node * 6U + 2U]};
|
|
for (std::size_t component = 0U; component < 3U; ++component) {
|
|
evidence.appliedForce[component] += applied[component];
|
|
evidence.reactionForce[component] += reaction[component];
|
|
evidence.appliedMomentAboutOrigin[component] +=
|
|
load[node * 6U + 3U + component];
|
|
evidence.reactionMomentAboutOrigin[component] +=
|
|
hdf.reaction[node * 6U + 3U + component];
|
|
}
|
|
evidence.appliedMomentAboutOrigin[0U] +=
|
|
coordinates[1U] * applied[2U] - coordinates[2U] * applied[1U];
|
|
evidence.appliedMomentAboutOrigin[1U] +=
|
|
coordinates[2U] * applied[0U] - coordinates[0U] * applied[2U];
|
|
evidence.appliedMomentAboutOrigin[2U] +=
|
|
coordinates[0U] * applied[1U] - coordinates[1U] * applied[0U];
|
|
evidence.reactionMomentAboutOrigin[0U] +=
|
|
coordinates[1U] * reaction[2U] - coordinates[2U] * reaction[1U];
|
|
evidence.reactionMomentAboutOrigin[1U] +=
|
|
coordinates[2U] * reaction[0U] - coordinates[0U] * reaction[2U];
|
|
evidence.reactionMomentAboutOrigin[2U] +=
|
|
coordinates[0U] * reaction[1U] - coordinates[1U] * reaction[0U];
|
|
}
|
|
evidence.endpointConsistencyPassed = true;
|
|
return evidence;
|
|
}
|
|
|
|
const char* quantityName(const ComparisonQuantity quantity) {
|
|
switch (quantity) {
|
|
case ComparisonQuantity::displacement:
|
|
return "displacement";
|
|
case ComparisonQuantity::reaction:
|
|
return "reaction";
|
|
case ComparisonQuantity::sectionResultant:
|
|
return "section_resultant";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
void writeJsonString(std::ostream& stream, const std::string& value) {
|
|
static constexpr char digits[] = "0123456789abcdef";
|
|
stream.put('"');
|
|
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" << digits[character >> 4U]
|
|
<< digits[character & 0x0fU];
|
|
} else {
|
|
stream.put(static_cast<char>(character));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
stream.put('"');
|
|
}
|
|
|
|
void writeCanonicalRow(
|
|
std::ostream& stream, const CanonicalComparisonRow& row) {
|
|
stream << "{\"model_id\":";
|
|
writeJsonString(stream, row.modelId);
|
|
stream << ",\"step_name\":";
|
|
writeJsonString(stream, row.stepName);
|
|
stream << ",\"frame_index\":" << row.frameIndex
|
|
<< ",\"instance_name\":";
|
|
writeJsonString(stream, row.instanceName);
|
|
stream << ",\"source_node_label\":" << row.sourceNodeLabel
|
|
<< ",\"quantity\":";
|
|
writeJsonString(stream, quantityName(row.quantity));
|
|
stream << ",\"component\":";
|
|
writeJsonString(stream, row.component);
|
|
stream << ",\"value\":" << row.value << ",\"unit_dimension\":";
|
|
writeJsonString(stream, row.unitDimension);
|
|
stream << ",\"coordinate_system\":";
|
|
writeJsonString(stream, row.coordinateSystem);
|
|
stream << ",\"hdf5_dataset_path\":";
|
|
writeJsonString(stream, row.hdf5DatasetPath);
|
|
stream << '}';
|
|
}
|
|
|
|
void writeArray(std::ostream& stream, const std::array<double, 3>& values) {
|
|
stream << '[' << values[0U] << ',' << values[1U] << ',' << values[2U]
|
|
<< ']';
|
|
}
|
|
|
|
bool finiteReport(const ComparisonReport& report) {
|
|
const auto finiteArray = [](const std::array<double, 3>& values) {
|
|
return std::all_of(values.begin(), values.end(), [](const double value) {
|
|
return std::isfinite(value);
|
|
});
|
|
};
|
|
if (!std::isfinite(report.physicsEvidence.freeResidualNorm) ||
|
|
!finiteArray(report.physicsEvidence.appliedForce) ||
|
|
!finiteArray(report.physicsEvidence.reactionForce) ||
|
|
!finiteArray(report.physicsEvidence.appliedMomentAboutOrigin) ||
|
|
!finiteArray(report.physicsEvidence.reactionMomentAboutOrigin)) {
|
|
return false;
|
|
}
|
|
for (const auto& row : report.rows) {
|
|
if (!std::isfinite(row.fesa.value) ||
|
|
!std::isfinite(row.reference.value) ||
|
|
!std::isfinite(row.absoluteError) ||
|
|
!std::isfinite(row.tolerance)) {
|
|
return false;
|
|
}
|
|
}
|
|
return std::all_of(
|
|
report.metrics.begin(), report.metrics.end(),
|
|
[](const ComponentMetrics& metric) {
|
|
return std::isfinite(metric.referenceScale) &&
|
|
std::isfinite(metric.maximumAbsoluteError) &&
|
|
std::isfinite(metric.maximumNormalizedError) &&
|
|
std::isfinite(metric.rmsError) &&
|
|
std::isfinite(metric.normError);
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Result<ComparisonReport> ReferenceComparison::compare(
|
|
const std::filesystem::path& resultsHdf5,
|
|
const std::filesystem::path& legacyReferenceDirectory) {
|
|
try {
|
|
requireExactArtifactInventory(legacyReferenceDirectory);
|
|
const auto input = legacyReferenceDirectory / kInputName;
|
|
Domain domain = readApprovedDomain(input);
|
|
const ReferenceTable displacement = readReferenceCsv(
|
|
legacyReferenceDirectory / kDisplacementName,
|
|
{"Frame", "Part Instance Name", "Node Label", "U-U1", "U-U2",
|
|
"U-U3", "UR-UR1", "UR-UR2", "UR-UR3"});
|
|
const ReferenceTable reaction = readReferenceCsv(
|
|
legacyReferenceDirectory / kReactionName,
|
|
{"Frame", "Part Instance Name", "Node Label", "RF-RF1", "RF-RF2",
|
|
"RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"});
|
|
const ReferenceTable section = readReferenceCsv(
|
|
legacyReferenceDirectory / kSectionName,
|
|
{"Frame", "Part Instance Name", "Node Label", "SF-SF1", "SM-SM1",
|
|
"SM-SM2", "SM-SM3"});
|
|
HdfProjection hdf = readHdfProjection(resultsHdf5, input, domain);
|
|
const auto displacementRows = orderedRows(displacement, hdf.nodes);
|
|
const auto reactionRows = orderedRows(reaction, hdf.nodes);
|
|
const auto sectionRows = orderedRows(section, hdf.nodes);
|
|
const auto stations = normalizeStations(domain, hdf, section);
|
|
if (stations.size() != hdf.nodes.size()) {
|
|
fail("schema-mismatch", "The HDF5 node-station row set is incomplete.");
|
|
}
|
|
|
|
ComparisonReport report{};
|
|
report.passed = true;
|
|
appendNodalRows(
|
|
report,
|
|
hdf,
|
|
displacementRows,
|
|
ComparisonQuantity::displacement,
|
|
{"UX", "UY", "UZ", "URX", "URY", "URZ"},
|
|
{"length", "length", "length", "radian", "radian", "radian"},
|
|
hdf.displacement,
|
|
kDisplacementPath);
|
|
appendNodalRows(
|
|
report,
|
|
hdf,
|
|
reactionRows,
|
|
ComparisonQuantity::reaction,
|
|
{"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"},
|
|
{"force", "force", "force", "force*length", "force*length",
|
|
"force*length"},
|
|
hdf.reaction,
|
|
kReactionPath);
|
|
appendSectionRows(report, hdf, sectionRows, stations);
|
|
|
|
for (const std::string& component :
|
|
{"UX", "UY", "UZ", "URX", "URY", "URZ"}) {
|
|
evaluateGroup(report, ComparisonQuantity::displacement, component);
|
|
}
|
|
for (const std::string& component :
|
|
{"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}) {
|
|
evaluateGroup(report, ComparisonQuantity::reaction, component);
|
|
}
|
|
for (const std::string& component : {"N", "T", "My", "Mz"}) {
|
|
evaluateGroup(report, ComparisonQuantity::sectionResultant, component);
|
|
}
|
|
report.physicsEvidence = makePhysicsEvidence(domain, hdf);
|
|
report.stressComparisonApplicable = false;
|
|
report.stressComparisonReason =
|
|
"Abaqus beam stress comparison is N/A; analytical/unit and HDF5 "
|
|
"schema tests provide stress evidence.";
|
|
return Result<ComparisonReport>::success(std::move(report));
|
|
} catch (const ComparisonFailure& failure) {
|
|
return Result<ComparisonReport>::failure(
|
|
comparisonFailureStatus(failure.code(), failure.what()));
|
|
} catch (const std::exception& failure) {
|
|
return Result<ComparisonReport>::failure(comparisonFailureStatus(
|
|
"schema-mismatch", failure.what()));
|
|
}
|
|
}
|
|
|
|
Status ReferenceComparison::writeDeterministicJson(
|
|
const ComparisonReport& report,
|
|
const std::filesystem::path& outputJson) {
|
|
if (outputJson.empty() || outputJson.filename().empty() ||
|
|
!finiteReport(report)) {
|
|
return Status::failure(
|
|
FailureCategory::output,
|
|
{{Severity::error,
|
|
"comparison-json-write-failure",
|
|
{},
|
|
"",
|
|
kModelId,
|
|
"The deterministic comparison report or output path is invalid."}});
|
|
}
|
|
std::ofstream stream{outputJson, std::ios::binary | std::ios::trunc};
|
|
if (!stream) {
|
|
return Status::failure(
|
|
FailureCategory::output,
|
|
{{Severity::error,
|
|
"comparison-json-write-failure",
|
|
{},
|
|
"",
|
|
kModelId,
|
|
"The deterministic comparison JSON cannot be opened."}});
|
|
}
|
|
stream.imbue(std::locale::classic());
|
|
stream << std::setprecision(std::numeric_limits<double>::max_digits10);
|
|
stream << "{\"rows\":[";
|
|
for (std::size_t index = 0U; index < report.rows.size(); ++index) {
|
|
if (index != 0U) {
|
|
stream << ',';
|
|
}
|
|
const auto& row = report.rows[index];
|
|
stream << "{\"fesa\":";
|
|
writeCanonicalRow(stream, row.fesa);
|
|
stream << ",\"reference\":";
|
|
writeCanonicalRow(stream, row.reference);
|
|
stream << ",\"absolute_error\":" << row.absoluteError
|
|
<< ",\"tolerance\":" << row.tolerance
|
|
<< ",\"passed\":" << (row.passed ? "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 << "{\"quantity\":";
|
|
writeJsonString(stream, quantityName(metric.quantity));
|
|
stream << ",\"component\":";
|
|
writeJsonString(stream, metric.component);
|
|
stream << ",\"reference_scale\":" << metric.referenceScale
|
|
<< ",\"maximum_absolute_error\":"
|
|
<< metric.maximumAbsoluteError
|
|
<< ",\"maximum_normalized_error\":"
|
|
<< metric.maximumNormalizedError
|
|
<< ",\"rms_error\":" << metric.rmsError
|
|
<< ",\"norm_error\":" << metric.normError
|
|
<< ",\"worst_row\":" << metric.worstRow << '}';
|
|
}
|
|
stream << "],\"physics_evidence\":{\"free_residual_norm\":"
|
|
<< report.physicsEvidence.freeResidualNorm
|
|
<< ",\"applied_force\":";
|
|
writeArray(stream, report.physicsEvidence.appliedForce);
|
|
stream << ",\"reaction_force\":";
|
|
writeArray(stream, report.physicsEvidence.reactionForce);
|
|
stream << ",\"applied_moment_about_origin\":";
|
|
writeArray(stream, report.physicsEvidence.appliedMomentAboutOrigin);
|
|
stream << ",\"reaction_moment_about_origin\":";
|
|
writeArray(stream, report.physicsEvidence.reactionMomentAboutOrigin);
|
|
stream << ",\"endpoint_consistency_passed\":"
|
|
<< (report.physicsEvidence.endpointConsistencyPassed ? "true" : "false")
|
|
<< "},\"stress_comparison_applicable\":"
|
|
<< (report.stressComparisonApplicable ? "true" : "false")
|
|
<< ",\"stress_comparison_reason\":";
|
|
writeJsonString(stream, report.stressComparisonReason);
|
|
stream << ",\"passed\":" << (report.passed ? "true" : "false") << "}\n";
|
|
if (!stream) {
|
|
return Status::failure(
|
|
FailureCategory::output,
|
|
{{Severity::error,
|
|
"comparison-json-write-failure",
|
|
{},
|
|
"",
|
|
kModelId,
|
|
"The deterministic comparison JSON write failed."}});
|
|
}
|
|
return Status::ok();
|
|
}
|
|
|
|
} // namespace fesa::test
|