806 lines
28 KiB
C++
806 lines
28 KiB
C++
#include "mitc4_reference_comparison.hpp"
|
|
|
|
#include <hdf5.h>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#ifndef FESA_TEST_BINARY_DIR
|
|
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kInstanceName = "PART-1-1";
|
|
constexpr const char* kInternalFormulation = "FESA-MITC4";
|
|
constexpr const char* kIntegrationRule =
|
|
"2x2x2-gauss; mitc4-edge-midpoint-shear";
|
|
constexpr const char* kDisplacementPath =
|
|
"/steps/Step-1/frames/0/nodal/displacement";
|
|
constexpr std::array<const char*, 6> kComponents{
|
|
"U1", "U2", "U3", "UR1", "UR2", "UR3"};
|
|
|
|
class Hdf5Handle {
|
|
public:
|
|
using Closer = herr_t (*)(hid_t);
|
|
|
|
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_;
|
|
Closer closer_;
|
|
};
|
|
|
|
hid_t requireId(const hid_t value, const char* message) {
|
|
if (value < 0) {
|
|
throw std::runtime_error{message};
|
|
}
|
|
return value;
|
|
}
|
|
|
|
void requireHdf(const herr_t value, const char* message) {
|
|
if (value < 0) {
|
|
throw std::runtime_error{message};
|
|
}
|
|
}
|
|
|
|
Hdf5Handle makeUtf8StringType() {
|
|
Hdf5Handle type{
|
|
requireId(H5Tcopy(H5T_C_S1), "Unable to copy string type."), H5Tclose};
|
|
requireHdf(
|
|
H5Tset_size(type.get(), H5T_VARIABLE),
|
|
"Unable to create variable string type.");
|
|
requireHdf(
|
|
H5Tset_cset(type.get(), H5T_CSET_UTF8),
|
|
"Unable to create UTF-8 string type.");
|
|
return type;
|
|
}
|
|
|
|
void writeStringAttribute(
|
|
const hid_t object, const char* name, const std::string& value) {
|
|
auto type = makeUtf8StringType();
|
|
Hdf5Handle space{
|
|
requireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."),
|
|
H5Sclose};
|
|
Hdf5Handle attribute{
|
|
requireId(
|
|
H5Acreate2(
|
|
object, name, type.get(), space.get(), H5P_DEFAULT,
|
|
H5P_DEFAULT),
|
|
"Unable to create string attribute."),
|
|
H5Aclose};
|
|
const char* raw = value.c_str();
|
|
requireHdf(
|
|
H5Awrite(attribute.get(), type.get(), &raw),
|
|
"Unable to write string attribute.");
|
|
}
|
|
|
|
void writeUint64Attribute(
|
|
const hid_t object, const char* name, const std::uint64_t value) {
|
|
Hdf5Handle space{
|
|
requireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."),
|
|
H5Sclose};
|
|
Hdf5Handle attribute{
|
|
requireId(
|
|
H5Acreate2(
|
|
object, name, H5T_STD_U64LE, space.get(), H5P_DEFAULT,
|
|
H5P_DEFAULT),
|
|
"Unable to create integer attribute."),
|
|
H5Aclose};
|
|
requireHdf(
|
|
H5Awrite(attribute.get(), H5T_NATIVE_UINT64, &value),
|
|
"Unable to write integer attribute.");
|
|
}
|
|
|
|
Hdf5Handle createGroup(const hid_t parent, const char* path) {
|
|
return {
|
|
requireId(
|
|
H5Gcreate2(
|
|
parent, path, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
|
"Unable to create HDF5 group."),
|
|
H5Gclose};
|
|
}
|
|
|
|
struct ComparisonValueRow {
|
|
std::string instanceName{kInstanceName};
|
|
std::int64_t sourceNodeLabel{};
|
|
std::array<double, 6> values{};
|
|
};
|
|
|
|
std::vector<ComparisonValueRow> defaultRows() {
|
|
return {
|
|
{kInstanceName, 1, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}},
|
|
{kInstanceName, 2, {2.0, -4.0, 8.0, 0.1, -0.2, 0.3}}};
|
|
}
|
|
|
|
struct Hdf5Options {
|
|
std::string sourceElementType{"S4"};
|
|
std::string internalFormulation{kInternalFormulation};
|
|
std::string integrationRule{kIntegrationRule};
|
|
std::string displacementComponents{"UX,UY,UZ,URX,URY,URZ"};
|
|
};
|
|
|
|
struct NodeWriteRow {
|
|
std::uint64_t internalNodeId;
|
|
const char* instanceName;
|
|
const char* sourceLabel;
|
|
double coordinates[3];
|
|
};
|
|
|
|
struct ElementWriteRow {
|
|
std::uint64_t internalElementId;
|
|
const char* instanceName;
|
|
const char* sourceLabel;
|
|
const char* sourceElementType;
|
|
const char* internalFormulation;
|
|
std::uint64_t nodeInternalIds[4];
|
|
std::uint64_t shellSectionInternalId;
|
|
std::uint64_t materialInternalId;
|
|
};
|
|
|
|
void writeNodes(
|
|
const hid_t file, const std::vector<ComparisonValueRow>& values) {
|
|
std::vector<std::string> labels;
|
|
labels.reserve(values.size());
|
|
for (const auto& row : values) {
|
|
labels.push_back(std::to_string(row.sourceNodeLabel));
|
|
}
|
|
std::vector<NodeWriteRow> rows;
|
|
rows.reserve(values.size());
|
|
for (std::size_t index = 0U; index < values.size(); ++index) {
|
|
rows.push_back({
|
|
static_cast<std::uint64_t>(index),
|
|
values[index].instanceName.c_str(),
|
|
labels[index].c_str(),
|
|
{static_cast<double>(index), 0.0, 0.0}});
|
|
}
|
|
|
|
auto stringType = makeUtf8StringType();
|
|
const hsize_t coordinateDimensions[] = {3U};
|
|
Hdf5Handle fileCoordinates{
|
|
requireId(
|
|
H5Tarray_create2(H5T_IEEE_F64LE, 1, coordinateDimensions),
|
|
"Unable to create coordinate file type."),
|
|
H5Tclose};
|
|
Hdf5Handle memoryCoordinates{
|
|
requireId(
|
|
H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions),
|
|
"Unable to create coordinate memory type."),
|
|
H5Tclose};
|
|
Hdf5Handle fileType{
|
|
requireId(
|
|
H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)),
|
|
"Unable to create node file type."),
|
|
H5Tclose};
|
|
Hdf5Handle memoryType{
|
|
requireId(
|
|
H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)),
|
|
"Unable to create node memory type."),
|
|
H5Tclose};
|
|
const auto insert = [&](const hid_t type,
|
|
const hid_t integerType,
|
|
const hid_t coordinateType) {
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "internal_node_id",
|
|
HOFFSET(NodeWriteRow, internalNodeId), integerType),
|
|
"Unable to define node ID.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "instance_name", HOFFSET(NodeWriteRow, instanceName),
|
|
stringType.get()),
|
|
"Unable to define node instance.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "source_label", HOFFSET(NodeWriteRow, sourceLabel),
|
|
stringType.get()),
|
|
"Unable to define node label.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "coordinates", HOFFSET(NodeWriteRow, coordinates),
|
|
coordinateType),
|
|
"Unable to define node coordinates.");
|
|
};
|
|
insert(fileType.get(), H5T_STD_U64LE, fileCoordinates.get());
|
|
insert(memoryType.get(), H5T_NATIVE_UINT64, memoryCoordinates.get());
|
|
const hsize_t dimensions[] = {static_cast<hsize_t>(rows.size())};
|
|
Hdf5Handle space{
|
|
requireId(
|
|
H5Screate_simple(1, dimensions, nullptr),
|
|
"Unable to create node space."),
|
|
H5Sclose};
|
|
Hdf5Handle dataset{
|
|
requireId(
|
|
H5Dcreate2(
|
|
file, "/model/nodes", fileType.get(), space.get(),
|
|
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
|
"Unable to create node dataset."),
|
|
H5Dclose};
|
|
requireHdf(
|
|
H5Dwrite(
|
|
dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
|
rows.data()),
|
|
"Unable to write node dataset.");
|
|
writeStringAttribute(dataset.get(), "coordinate_system", "global-cartesian");
|
|
writeStringAttribute(dataset.get(), "units_label", "length");
|
|
}
|
|
|
|
void writeElements(const hid_t file, const Hdf5Options& options) {
|
|
ElementWriteRow row{
|
|
0U,
|
|
kInstanceName,
|
|
"1",
|
|
options.sourceElementType.c_str(),
|
|
options.internalFormulation.c_str(),
|
|
{0U, 1U, 0U, 1U},
|
|
0U,
|
|
0U};
|
|
auto stringType = makeUtf8StringType();
|
|
const hsize_t nodeDimensions[] = {4U};
|
|
Hdf5Handle fileNodes{
|
|
requireId(
|
|
H5Tarray_create2(H5T_STD_U64LE, 1, nodeDimensions),
|
|
"Unable to create element node file type."),
|
|
H5Tclose};
|
|
Hdf5Handle memoryNodes{
|
|
requireId(
|
|
H5Tarray_create2(H5T_NATIVE_UINT64, 1, nodeDimensions),
|
|
"Unable to create element node memory type."),
|
|
H5Tclose};
|
|
Hdf5Handle fileType{
|
|
requireId(
|
|
H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)),
|
|
"Unable to create element file type."),
|
|
H5Tclose};
|
|
Hdf5Handle memoryType{
|
|
requireId(
|
|
H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)),
|
|
"Unable to create element memory type."),
|
|
H5Tclose};
|
|
const auto insert = [&](const hid_t type,
|
|
const hid_t integerType,
|
|
const hid_t nodesType) {
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "internal_element_id",
|
|
HOFFSET(ElementWriteRow, internalElementId), integerType),
|
|
"Unable to define element ID.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "instance_name", HOFFSET(ElementWriteRow, instanceName),
|
|
stringType.get()),
|
|
"Unable to define element instance.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "source_label", HOFFSET(ElementWriteRow, sourceLabel),
|
|
stringType.get()),
|
|
"Unable to define element label.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "source_element_type",
|
|
HOFFSET(ElementWriteRow, sourceElementType), stringType.get()),
|
|
"Unable to define source element type.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "internal_formulation",
|
|
HOFFSET(ElementWriteRow, internalFormulation), stringType.get()),
|
|
"Unable to define internal formulation.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "node_internal_ids",
|
|
HOFFSET(ElementWriteRow, nodeInternalIds), nodesType),
|
|
"Unable to define element connectivity.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "shell_section_internal_id",
|
|
HOFFSET(ElementWriteRow, shellSectionInternalId), integerType),
|
|
"Unable to define section ID.");
|
|
requireHdf(
|
|
H5Tinsert(
|
|
type, "material_internal_id",
|
|
HOFFSET(ElementWriteRow, materialInternalId), integerType),
|
|
"Unable to define material ID.");
|
|
};
|
|
insert(fileType.get(), H5T_STD_U64LE, fileNodes.get());
|
|
insert(memoryType.get(), H5T_NATIVE_UINT64, memoryNodes.get());
|
|
const hsize_t dimensions[] = {1U};
|
|
Hdf5Handle space{
|
|
requireId(
|
|
H5Screate_simple(1, dimensions, nullptr),
|
|
"Unable to create element space."),
|
|
H5Sclose};
|
|
Hdf5Handle dataset{
|
|
requireId(
|
|
H5Dcreate2(
|
|
file, "/model/elements", fileType.get(), space.get(),
|
|
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
|
"Unable to create element dataset."),
|
|
H5Dclose};
|
|
requireHdf(
|
|
H5Dwrite(
|
|
dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
|
&row),
|
|
"Unable to write element dataset.");
|
|
writeStringAttribute(dataset.get(), "formulation", kInternalFormulation);
|
|
}
|
|
|
|
void writeDisplacement(
|
|
const hid_t file,
|
|
const std::vector<ComparisonValueRow>& values,
|
|
const Hdf5Options& options) {
|
|
std::vector<double> flattened;
|
|
flattened.reserve(values.size() * kComponents.size());
|
|
for (const auto& row : values) {
|
|
flattened.insert(flattened.end(), row.values.begin(), row.values.end());
|
|
}
|
|
const hsize_t dimensions[] = {
|
|
static_cast<hsize_t>(values.size()),
|
|
static_cast<hsize_t>(kComponents.size())};
|
|
Hdf5Handle space{
|
|
requireId(
|
|
H5Screate_simple(2, dimensions, nullptr),
|
|
"Unable to create displacement space."),
|
|
H5Sclose};
|
|
Hdf5Handle dataset{
|
|
requireId(
|
|
H5Dcreate2(
|
|
file, kDisplacementPath, H5T_IEEE_F64LE, space.get(),
|
|
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
|
"Unable to create displacement dataset."),
|
|
H5Dclose};
|
|
requireHdf(
|
|
H5Dwrite(
|
|
dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
|
H5P_DEFAULT, flattened.data()),
|
|
"Unable to write displacement dataset.");
|
|
writeStringAttribute(
|
|
dataset.get(), "component_names", options.displacementComponents);
|
|
writeStringAttribute(
|
|
dataset.get(), "component_unit_dimensions",
|
|
"length,length,length,radian,radian,radian");
|
|
writeStringAttribute(dataset.get(), "coordinate_system", "global-cartesian");
|
|
writeStringAttribute(dataset.get(), "location", "nodal");
|
|
writeStringAttribute(dataset.get(), "step_name", "Step-1");
|
|
writeUint64Attribute(dataset.get(), "frame_index", 0U);
|
|
}
|
|
|
|
void writeHdf5(
|
|
const std::filesystem::path& path,
|
|
const std::filesystem::path& inputPath,
|
|
const std::vector<ComparisonValueRow>& values,
|
|
const Hdf5Options& options = {}) {
|
|
Hdf5Handle file{
|
|
requireId(
|
|
H5Fcreate(
|
|
path.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT),
|
|
"Unable to create comparison HDF5 fixture."),
|
|
H5Fclose};
|
|
auto metadata = createGroup(file.get(), "/metadata");
|
|
auto model = createGroup(file.get(), "/model");
|
|
auto steps = createGroup(file.get(), "/steps");
|
|
auto step = createGroup(file.get(), "/steps/Step-1");
|
|
auto frames = createGroup(file.get(), "/steps/Step-1/frames");
|
|
auto frame = createGroup(file.get(), "/steps/Step-1/frames/0");
|
|
auto nodal = createGroup(file.get(), "/steps/Step-1/frames/0/nodal");
|
|
(void)model;
|
|
(void)steps;
|
|
(void)step;
|
|
(void)frames;
|
|
(void)frame;
|
|
(void)nodal;
|
|
|
|
writeUint64Attribute(metadata.get(), "schema_version", 0U);
|
|
writeStringAttribute(metadata.get(), "feature_id", "linear-static-mitc4-shell");
|
|
writeStringAttribute(
|
|
metadata.get(), "source_input_identity",
|
|
"path=" + std::filesystem::absolute(inputPath)
|
|
.lexically_normal()
|
|
.generic_u8string() +
|
|
";content_identity=test");
|
|
writeStringAttribute(
|
|
metadata.get(), "internal_formulation", options.internalFormulation);
|
|
writeStringAttribute(
|
|
metadata.get(), "integration_rule", options.integrationRule);
|
|
writeStringAttribute(metadata.get(), "step_name", "Step-1");
|
|
writeUint64Attribute(metadata.get(), "frame_index", 0U);
|
|
writeNodes(file.get(), values);
|
|
writeElements(file.get(), options);
|
|
writeDisplacement(file.get(), values, options);
|
|
}
|
|
|
|
void writeCsv(
|
|
const std::filesystem::path& path,
|
|
const std::vector<ComparisonValueRow>& rows,
|
|
const std::string& header =
|
|
"Part Instance Name,Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3") {
|
|
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
|
|
if (!stream) {
|
|
throw std::runtime_error{"Unable to create comparison CSV fixture."};
|
|
}
|
|
stream << header << '\n' << std::setprecision(17);
|
|
for (const auto& row : rows) {
|
|
stream << row.instanceName << ',' << row.sourceNodeLabel;
|
|
for (const double value : row.values) {
|
|
stream << ',' << value;
|
|
}
|
|
stream << '\n';
|
|
}
|
|
}
|
|
|
|
class ContractFixture {
|
|
public:
|
|
explicit ContractFixture(const std::string& name)
|
|
: root_{std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
|
"reference" / ("mitc4-comparator-" + name)},
|
|
input_{root_ / "case.inp"},
|
|
csv_{root_ / "displacements.csv"},
|
|
results_{root_ / "results.h5"} {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root_, error);
|
|
error.clear();
|
|
if (!std::filesystem::create_directories(root_, error) || error) {
|
|
throw std::runtime_error{"Unable to create comparison fixture directory."};
|
|
}
|
|
std::ofstream inputStream{input_, std::ios::binary | std::ios::trunc};
|
|
inputStream << "*Element, type=S4\n1,1,2,1,2\n";
|
|
inputStream.close();
|
|
writeCsv(csv_, defaultRows());
|
|
writeHdf5(results_, input_, defaultRows());
|
|
}
|
|
|
|
~ContractFixture() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root_, error);
|
|
}
|
|
|
|
fesa::test::Mitc4ReferenceCase referenceCase() const {
|
|
return {"shell-contract", "S4", input_, csv_, results_};
|
|
}
|
|
|
|
const std::filesystem::path& root() const noexcept { return root_; }
|
|
const std::filesystem::path& input() const noexcept { return input_; }
|
|
const std::filesystem::path& csv() const noexcept { return csv_; }
|
|
const std::filesystem::path& results() const noexcept { return results_; }
|
|
|
|
private:
|
|
std::filesystem::path root_;
|
|
std::filesystem::path input_;
|
|
std::filesystem::path csv_;
|
|
std::filesystem::path results_;
|
|
};
|
|
|
|
void expectFailureCode(
|
|
const fesa::Result<fesa::test::Mitc4ComparisonReport>& result,
|
|
const std::string& code) {
|
|
ASSERT_FALSE(result.HasValue());
|
|
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
|
EXPECT_EQ(result.GetStatus().Diagnostics().front().code, code);
|
|
}
|
|
|
|
const fesa::test::Mitc4RowDecision* findRow(
|
|
const fesa::test::Mitc4ComparisonReport& report,
|
|
const std::int64_t label,
|
|
const std::string& component) {
|
|
const auto found = std::find_if(
|
|
report.rows.begin(), report.rows.end(),
|
|
[&](const fesa::test::Mitc4RowDecision& row) {
|
|
return row.sourceNodeLabel == label && row.component == component;
|
|
});
|
|
return found == report.rows.end() ? nullptr : &*found;
|
|
}
|
|
|
|
const fesa::test::Mitc4ComponentMetrics* findMetric(
|
|
const fesa::test::Mitc4ComparisonReport& report,
|
|
const std::string& component) {
|
|
const auto found = std::find_if(
|
|
report.metrics.begin(), report.metrics.end(),
|
|
[&](const fesa::test::Mitc4ComponentMetrics& metric) {
|
|
return metric.component == component;
|
|
});
|
|
return found == report.metrics.end() ? nullptr : &*found;
|
|
}
|
|
|
|
std::string readBytes(const std::filesystem::path& path) {
|
|
std::ifstream stream{path, std::ios::binary};
|
|
return {std::istreambuf_iterator<char>{stream},
|
|
std::istreambuf_iterator<char>{}};
|
|
}
|
|
|
|
// MITC4-REF-001
|
|
TEST(Mitc4ReferenceComparison, MapsTrimmedHeaderAndSixComponentsBySourceIdentity) {
|
|
ContractFixture fixture{"mapping"};
|
|
auto csvRows = defaultRows();
|
|
std::reverse(csvRows.begin(), csvRows.end());
|
|
for (auto& row : csvRows) {
|
|
row.instanceName = "part-1-1";
|
|
}
|
|
writeCsv(
|
|
fixture.csv(), csvRows,
|
|
" Part Instance Name , Node Label , U-U1 , U-U2 , U-U3 , "
|
|
"UR-UR1 , UR-UR2 , UR-UR3 ");
|
|
|
|
auto result = fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
ASSERT_TRUE(report.passed);
|
|
ASSERT_EQ(report.rows.size(), 12U);
|
|
EXPECT_EQ(report.rows[0U].sourceNodeLabel, 1);
|
|
EXPECT_EQ(report.rows[0U].component, "U1");
|
|
EXPECT_EQ(report.rows[5U].component, "UR3");
|
|
EXPECT_EQ(report.rows[6U].sourceNodeLabel, 2);
|
|
EXPECT_TRUE(std::all_of(
|
|
report.rows.begin(), report.rows.end(),
|
|
[](const fesa::test::Mitc4RowDecision& row) {
|
|
return row.caseId == "shell-contract" &&
|
|
row.instanceName == kInstanceName && row.withinTolerance;
|
|
}));
|
|
}
|
|
|
|
// MITC4-REF-002
|
|
TEST(Mitc4ReferenceComparison, RejectsInvalidInventoryBeforeNumericComparison) {
|
|
{
|
|
ContractFixture fixture{"missing-input"};
|
|
ASSERT_TRUE(std::filesystem::remove(fixture.input()));
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"needs-reference-artifacts");
|
|
}
|
|
{
|
|
ContractFixture fixture{"header"};
|
|
writeCsv(
|
|
fixture.csv(), defaultRows(),
|
|
"Part Instance Name,Node Label,U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3");
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"missing-row"};
|
|
auto rows = defaultRows();
|
|
rows.pop_back();
|
|
writeCsv(fixture.csv(), rows);
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"extra-row"};
|
|
auto rows = defaultRows();
|
|
rows.push_back({kInstanceName, 3, {}});
|
|
writeCsv(fixture.csv(), rows);
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"duplicate-row"};
|
|
auto rows = defaultRows();
|
|
rows.push_back(rows.front());
|
|
writeCsv(fixture.csv(), rows);
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"nonfinite-csv"};
|
|
auto rows = defaultRows();
|
|
rows[0U].values[0U] = std::numeric_limits<double>::quiet_NaN();
|
|
writeCsv(fixture.csv(), rows);
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"nonfinite-hdf5"};
|
|
auto rows = defaultRows();
|
|
rows[0U].values[0U] = std::numeric_limits<double>::infinity();
|
|
writeHdf5(fixture.results(), fixture.input(), rows);
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"source-identity"};
|
|
auto rows = defaultRows();
|
|
rows[0U].instanceName = "WRONG-INSTANCE";
|
|
writeCsv(fixture.csv(), rows);
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"hdf5-schema"};
|
|
Hdf5Options options;
|
|
options.displacementComponents = "U1,U2,U3,UR1,UR2,UR3";
|
|
writeHdf5(fixture.results(), fixture.input(), defaultRows(), options);
|
|
expectFailureCode(
|
|
fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase()),
|
|
"schema-mismatch");
|
|
}
|
|
}
|
|
|
|
// MITC4-REF-003
|
|
TEST(Mitc4ReferenceComparison, AppliesFixedAbsoluteToleranceWithoutScaleClampOrRowDenominator) {
|
|
ContractFixture fixture{"tolerance"};
|
|
auto reference = defaultRows();
|
|
reference[0U].values[0U] = 0.0;
|
|
reference[1U].values[0U] = 2.0;
|
|
reference[0U].values[1U] = 0.0;
|
|
reference[1U].values[1U] = 0.0;
|
|
writeCsv(fixture.csv(), reference);
|
|
auto fesaValues = reference;
|
|
fesaValues[0U].values[0U] = 0.9999e-5;
|
|
fesaValues[1U].values[0U] += 1.0001e-5;
|
|
fesaValues[0U].values[1U] = 0.9999e-5;
|
|
fesaValues[1U].values[1U] = 1.0001e-5;
|
|
writeHdf5(fixture.results(), fixture.input(), fesaValues);
|
|
|
|
auto result = fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
EXPECT_FALSE(report.passed);
|
|
const auto* u1Zero = findRow(report, 1, "U1");
|
|
const auto* u1Scaled = findRow(report, 2, "U1");
|
|
const auto* u2Near = findRow(report, 1, "U2");
|
|
const auto* u2Over = findRow(report, 2, "U2");
|
|
ASSERT_NE(u1Zero, nullptr);
|
|
ASSERT_NE(u1Scaled, nullptr);
|
|
ASSERT_NE(u2Near, nullptr);
|
|
ASSERT_NE(u2Over, nullptr);
|
|
EXPECT_DOUBLE_EQ(u1Zero->tolerance, 1.0e-5);
|
|
EXPECT_TRUE(u1Zero->withinTolerance);
|
|
EXPECT_NEAR(u1Zero->normalizedError, 0.9999, 1.0e-12);
|
|
EXPECT_FALSE(u1Scaled->withinTolerance);
|
|
EXPECT_NEAR(u1Scaled->normalizedError, 1.0001, 2.0e-11);
|
|
EXPECT_DOUBLE_EQ(u2Near->tolerance, 1.0e-5);
|
|
EXPECT_TRUE(u2Near->withinTolerance);
|
|
EXPECT_NEAR(u2Near->normalizedError, 0.9999, 1.0e-12);
|
|
EXPECT_FALSE(u2Over->withinTolerance);
|
|
EXPECT_NEAR(u2Over->normalizedError, 1.0001, 1.0e-12);
|
|
for (const auto& row : report.rows) {
|
|
EXPECT_DOUBLE_EQ(row.tolerance, 1.0e-5);
|
|
}
|
|
for (const auto& metric : report.metrics) {
|
|
EXPECT_DOUBLE_EQ(metric.tolerance, 1.0e-5);
|
|
}
|
|
const auto* u1Metric = findMetric(report, "U1");
|
|
ASSERT_NE(u1Metric, nullptr);
|
|
EXPECT_DOUBLE_EQ(u1Metric->referenceScale, 2.0);
|
|
}
|
|
|
|
// MITC4-REF-004
|
|
TEST(Mitc4ReferenceComparison, RotationExceedanceWarnsWithoutBlockingTranslationVerdict) {
|
|
ContractFixture fixture{"warning"};
|
|
auto fesaValues = defaultRows();
|
|
fesaValues[0U].values[3U] = 1.0;
|
|
writeHdf5(fixture.results(), fixture.input(), fesaValues);
|
|
|
|
auto result = fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
EXPECT_TRUE(report.passed);
|
|
ASSERT_EQ(report.warnings.size(), 1U);
|
|
EXPECT_EQ(report.warnings[0U].code, "rotation-reference-exceedance");
|
|
const auto* row = findRow(report, 1, "UR1");
|
|
ASSERT_NE(row, nullptr);
|
|
EXPECT_FALSE(row->blocking);
|
|
EXPECT_FALSE(row->withinTolerance);
|
|
EXPECT_EQ(report.warnings[0U].row,
|
|
static_cast<std::size_t>(row - report.rows.data()));
|
|
EXPECT_NE(report.warnings[0U].message.find("shell-contract"),
|
|
std::string::npos);
|
|
EXPECT_NE(report.warnings[0U].message.find("UR1"), std::string::npos);
|
|
}
|
|
|
|
// MITC4-REF-005
|
|
TEST(Mitc4ReferenceComparison, ReportsMetricsVectorsWorstRowAndJsonDeterministically) {
|
|
ContractFixture fixture{"report"};
|
|
auto fesaValues = defaultRows();
|
|
fesaValues[0U].values[0U] = 0.5e-9;
|
|
fesaValues[0U].values[1U] = -0.25e-9;
|
|
fesaValues[0U].values[3U] = 0.75e-9;
|
|
writeHdf5(fixture.results(), fixture.input(), fesaValues);
|
|
|
|
auto result = fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
ASSERT_TRUE(report.passed);
|
|
ASSERT_EQ(report.metrics.size(), 6U);
|
|
ASSERT_EQ(report.vectorMetrics.size(), 2U);
|
|
EXPECT_NEAR(
|
|
report.vectorMetrics[0U].displacementNormError,
|
|
std::sqrt(0.3125) * 1.0e-9,
|
|
1.0e-21);
|
|
EXPECT_DOUBLE_EQ(
|
|
report.vectorMetrics[0U].rotationNormError, 0.75e-9);
|
|
ASSERT_LT(report.worstRow, report.rows.size());
|
|
EXPECT_EQ(report.rows[report.worstRow].component, "UR1");
|
|
for (const auto& metric : report.metrics) {
|
|
EXPECT_TRUE(std::isfinite(metric.referenceScale));
|
|
EXPECT_TRUE(std::isfinite(metric.maximumAbsoluteError));
|
|
EXPECT_TRUE(std::isfinite(metric.maximumNormalizedError));
|
|
EXPECT_TRUE(std::isfinite(metric.rmsError));
|
|
EXPECT_TRUE(std::isfinite(metric.vectorNormError));
|
|
EXPECT_LT(metric.worstRow, report.rows.size());
|
|
}
|
|
|
|
const auto jsonA = fixture.root() / "comparison-a.json";
|
|
const auto jsonB = fixture.root() / "comparison-b.json";
|
|
ASSERT_TRUE(
|
|
fesa::test::Mitc4ReferenceComparison::writeDeterministicJson(
|
|
report, jsonA)
|
|
.IsOk());
|
|
ASSERT_TRUE(
|
|
fesa::test::Mitc4ReferenceComparison::writeDeterministicJson(
|
|
report, jsonB)
|
|
.IsOk());
|
|
const std::string first = readBytes(jsonA);
|
|
EXPECT_EQ(first, readBytes(jsonB));
|
|
for (const char* key : {
|
|
"\"rows\"", "\"metrics\"", "\"vector_metrics\"",
|
|
"\"warnings\"", "\"worst_row\"", "\"passed\""}) {
|
|
EXPECT_NE(first.find(key), std::string::npos) << key;
|
|
}
|
|
}
|
|
|
|
// MITC4-REF-006
|
|
TEST(Mitc4ReferenceComparison, RequiresOnlyDeclaredInputCsvAndHdf5) {
|
|
ContractFixture fixture{"minimal-artifacts"};
|
|
std::vector<std::string> names;
|
|
for (const auto& entry : std::filesystem::directory_iterator{fixture.root()}) {
|
|
names.push_back(entry.path().filename().string());
|
|
}
|
|
std::sort(names.begin(), names.end());
|
|
EXPECT_EQ(
|
|
names,
|
|
(std::vector<std::string>{"case.inp", "displacements.csv", "results.h5"}));
|
|
auto result = fesa::test::Mitc4ReferenceComparison::compare(
|
|
fixture.referenceCase());
|
|
ASSERT_TRUE(result.HasValue());
|
|
EXPECT_TRUE(result.Value().passed);
|
|
}
|
|
|
|
} // namespace
|