719 lines
30 KiB
C++
719 lines
30 KiB
C++
#include "reference_comparison.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "fesa/analysis/analysis_model.h"
|
|
#include "fesa/analysis/analysis_state.h"
|
|
#include "fesa/fem/dof_manager.h"
|
|
#include "fesa/io/abaqus/input_reader.h"
|
|
#include "fesa/io/hdf5/hdf5_results_writer.h"
|
|
#include "fesa/model/domain.h"
|
|
|
|
#ifndef FESA_TEST_SOURCE_DIR
|
|
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
|
#endif
|
|
|
|
#ifndef FESA_TEST_BINARY_DIR
|
|
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
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* kInstanceName = "PART-1_1-1";
|
|
constexpr std::size_t kNodeCount = 11U;
|
|
constexpr std::size_t kElementCount = 10U;
|
|
constexpr std::size_t kExpectedRowCount = 212U;
|
|
constexpr std::size_t kExpectedMetricCount = 6U;
|
|
|
|
using NodalValues = std::array<std::array<double, 6>, kNodeCount>;
|
|
using EndpointValues =
|
|
std::array<std::array<std::array<double, 4>, 2>, kElementCount>;
|
|
|
|
struct ComparisonValues {
|
|
NodalValues displacement{};
|
|
NodalValues reaction{};
|
|
EndpointValues section_resultants{};
|
|
};
|
|
|
|
const std::filesystem::path& SourceRoot() {
|
|
static const std::filesystem::path kRoot{FESA_TEST_SOURCE_DIR};
|
|
return kRoot;
|
|
}
|
|
|
|
const std::filesystem::path& BinaryRoot() {
|
|
static const std::filesystem::path kRoot{FESA_TEST_BINARY_DIR};
|
|
return kRoot;
|
|
}
|
|
|
|
std::string ReadBytes(const std::filesystem::path& path) {
|
|
std::ifstream stream{path, std::ios::binary};
|
|
if (!stream) {
|
|
throw std::runtime_error{"Unable to read fixture: " + path.string()};
|
|
}
|
|
return {std::istreambuf_iterator<char>{stream},
|
|
std::istreambuf_iterator<char>{}};
|
|
}
|
|
|
|
void WriteBytes(const std::filesystem::path& path,
|
|
const std::string& contents) {
|
|
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
|
|
if (!stream) {
|
|
throw std::runtime_error{"Unable to write build-local fixture: " +
|
|
path.string()};
|
|
}
|
|
stream.write(contents.data(), static_cast<std::streamsize>(contents.size()));
|
|
if (!stream) {
|
|
throw std::runtime_error{"Unable to finish build-local fixture write."};
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> ReadLines(const std::filesystem::path& path) {
|
|
std::ifstream stream{path};
|
|
if (!stream) {
|
|
throw std::runtime_error{"Unable to read fixture lines."};
|
|
}
|
|
std::vector<std::string> lines;
|
|
for (std::string line; std::getline(stream, line);) {
|
|
if (!line.empty() && line.back() == '\r') {
|
|
line.pop_back();
|
|
}
|
|
lines.push_back(std::move(line));
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
void WriteLines(const std::filesystem::path& path,
|
|
const std::vector<std::string>& lines) {
|
|
std::ofstream stream{path, std::ios::trunc};
|
|
if (!stream) {
|
|
throw std::runtime_error{"Unable to write build-local fixture lines."};
|
|
}
|
|
for (const auto& line : lines) {
|
|
stream << line << '\n';
|
|
}
|
|
if (!stream) {
|
|
throw std::runtime_error{"Unable to finish build-local line write."};
|
|
}
|
|
}
|
|
|
|
void ReplaceFirst(std::string& contents, const std::string& from,
|
|
const std::string& to) {
|
|
const std::size_t position = contents.find(from);
|
|
if (position == std::string::npos) {
|
|
throw std::runtime_error{"Fixture token was not found: " + from};
|
|
}
|
|
contents.replace(position, from.size(), to);
|
|
}
|
|
|
|
ComparisonValues ReferenceValues() {
|
|
ComparisonValues values{};
|
|
const std::array<double, kNodeCount> uz = {
|
|
-1.0e-30, -2.761905780e-4, -1.066667140e-3, -2.314286540e-3,
|
|
-3.961906300e-3, -5.952383390e-3, -8.228574880e-3, -1.073333810e-2,
|
|
-1.340952890e-2, -1.620000600e-2, -1.904762720e-2};
|
|
const std::array<double, kNodeCount> ury = {
|
|
1.0e-29, 5.428573350e-4, 1.028571860e-3, 1.457143460e-3,
|
|
1.828572130e-3, 2.142857990e-3, 2.400001050e-3, 2.600000940e-3,
|
|
2.742858140e-3, 2.828572640e-3, 2.857143990e-3};
|
|
std::array<std::array<double, 4>, kNodeCount> stations{};
|
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
|
values.displacement[node][2U] = uz[node];
|
|
values.displacement[node][4U] = ury[node];
|
|
stations[node][2U] = node < kElementCount
|
|
? 1.0e7 - 1.0e6 * static_cast<double>(node)
|
|
: -1.56e-2;
|
|
}
|
|
values.reaction[0U][2U] = 1.0e6;
|
|
values.reaction[0U][4U] = -1.0e7;
|
|
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
|
values.section_resultants[element][0U] = stations[element];
|
|
values.section_resultants[element][1U] = stations[element + 1U];
|
|
}
|
|
return values;
|
|
}
|
|
|
|
fesa::ModelDefinition MakeDefinition(const std::filesystem::path& input,
|
|
std::string source_content_identity) {
|
|
fesa::ModelDefinition definition{};
|
|
definition.source_path = input;
|
|
definition.source_content_identity = std::move(source_content_identity);
|
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
|
const auto label = static_cast<std::int64_t>(node + 1U);
|
|
definition.nodes.push_back({{kInstanceName, label, std::to_string(label)},
|
|
{static_cast<double>(node), 0.0, 0.0},
|
|
{input, node + 1U}});
|
|
}
|
|
definition.materials.push_back({"Material-1", 2.1e11, 0.3, {input, 20U}});
|
|
definition.sections.push_back({"Section-1",
|
|
1.0,
|
|
0.0833333,
|
|
0.0,
|
|
0.0833333,
|
|
0.140833,
|
|
{0.0, 1.0, 0.0},
|
|
{},
|
|
{input, 30U}});
|
|
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
|
const auto label = static_cast<std::int64_t>(element + 1U);
|
|
definition.elements.push_back(
|
|
{{kInstanceName, label, std::to_string(label)},
|
|
{static_cast<fesa::EntityIndex>(element),
|
|
static_cast<fesa::EntityIndex>(element + 1U)},
|
|
0U,
|
|
0U,
|
|
{input, 40U + element}});
|
|
}
|
|
definition.steps.push_back(
|
|
{"Step-1", {}, {}, 1.0, 1.0, 1.0e-5, 1.0, {input, 60U}});
|
|
return definition;
|
|
}
|
|
|
|
void WriteResultsFixture(const std::filesystem::path& output,
|
|
const std::filesystem::path& input,
|
|
const ComparisonValues& values) {
|
|
auto parsed_input = fesa::AbaqusInputReader{}.Read(input);
|
|
if (!parsed_input.HasValue()) {
|
|
throw std::runtime_error{"Reference fixture input identity read failed."};
|
|
}
|
|
auto domain_result = fesa::Domain::Create(
|
|
MakeDefinition(input, parsed_input.Value().source_content_identity));
|
|
if (!domain_result.HasValue()) {
|
|
throw std::runtime_error{"Reference fixture Domain construction failed."};
|
|
}
|
|
fesa::Domain domain = std::move(domain_result.Value());
|
|
auto model_result = fesa::AnalysisModel::Create(domain);
|
|
if (!model_result.HasValue()) {
|
|
throw std::runtime_error{
|
|
"Reference fixture AnalysisModel construction failed."};
|
|
}
|
|
fesa::AnalysisModel model = std::move(model_result.Value());
|
|
auto dofs_result = fesa::DofManager::Create(model);
|
|
if (!dofs_result.HasValue()) {
|
|
throw std::runtime_error{
|
|
"Reference fixture DofManager construction failed."};
|
|
}
|
|
fesa::DofManager dofs = std::move(dofs_result.Value());
|
|
fesa::AnalysisState state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U});
|
|
|
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
|
for (std::size_t component = 0U; component < 6U; ++component) {
|
|
const std::size_t index = node * 6U + component;
|
|
state.Displacement()[index] = values.displacement[node][component];
|
|
state.Reaction()[index] = values.reaction[node][component];
|
|
state.Residual()[index] = values.reaction[node][component];
|
|
}
|
|
}
|
|
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
|
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
|
|
const std::size_t node = element + endpoint;
|
|
state.EndpointResults().push_back(
|
|
{static_cast<fesa::EntityIndex>(element),
|
|
static_cast<int>(endpoint),
|
|
domain.Nodes()[node].source_id,
|
|
{},
|
|
values.section_resultants[element][endpoint]});
|
|
}
|
|
state.GaussResults().push_back(
|
|
{static_cast<fesa::EntityIndex>(element), 1, {}, {}});
|
|
state.GaussResults().push_back(
|
|
{static_cast<fesa::EntityIndex>(element), 2, {}, {}});
|
|
state.StressResults().push_back({static_cast<fesa::EntityIndex>(element), 1,
|
|
0U, 0.0, 0.0, 0.0, "fesa-default"});
|
|
state.StressResults().push_back({static_cast<fesa::EntityIndex>(element), 2,
|
|
0U, 0.0, 0.0, 0.0, "fesa-default"});
|
|
}
|
|
|
|
fesa::Hdf5ResultsWriter writer;
|
|
const fesa::Status status = writer.Write(output, domain, state, {});
|
|
if (!status.IsOk()) {
|
|
throw std::runtime_error{"Reference fixture HDF5 write failed."};
|
|
}
|
|
}
|
|
|
|
class ContractFixture {
|
|
public:
|
|
ContractFixture(std::string label, const ComparisonValues& values) {
|
|
static std::atomic<std::uint64_t> sequence{0U};
|
|
root_ = BinaryRoot() / "reference" / "contract-fixtures" /
|
|
(std::move(label) + "-" + std::to_string(sequence.fetch_add(1U)));
|
|
legacy_ = root_ / "cantilever beam";
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root_, error);
|
|
error.clear();
|
|
if (!std::filesystem::create_directories(legacy_, error) || error) {
|
|
throw std::runtime_error{"Unable to create contract fixture directory."};
|
|
}
|
|
const auto approved = SourceRoot() / "reference" / "cantilever beam";
|
|
for (const char* name :
|
|
{kInputName, kDisplacementName, kReactionName, kSectionName}) {
|
|
std::filesystem::copy_file(
|
|
approved / name, legacy_ / name,
|
|
std::filesystem::copy_options::overwrite_existing);
|
|
}
|
|
results_ = root_ / "results.h5";
|
|
WriteResultsFixture(results_, legacy_ / kInputName, values);
|
|
}
|
|
|
|
ContractFixture(const ContractFixture&) = delete;
|
|
ContractFixture& operator=(const ContractFixture&) = delete;
|
|
|
|
~ContractFixture() {
|
|
std::error_code ignored;
|
|
std::filesystem::remove_all(root_, ignored);
|
|
}
|
|
|
|
const std::filesystem::path& Root() const noexcept { return root_; }
|
|
const std::filesystem::path& Legacy() const noexcept { return legacy_; }
|
|
const std::filesystem::path& Results() const noexcept { return results_; }
|
|
|
|
private:
|
|
std::filesystem::path root_;
|
|
std::filesystem::path legacy_;
|
|
std::filesystem::path results_;
|
|
};
|
|
|
|
void ExpectFailureCode(const fesa::Result<fesa::test::ComparisonReport>& result,
|
|
const std::string& expected_code) {
|
|
ASSERT_FALSE(result.HasValue());
|
|
ASSERT_FALSE(result.GetStatus().IsOk());
|
|
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
|
EXPECT_EQ(result.GetStatus().Diagnostics().front().code, expected_code);
|
|
}
|
|
|
|
const fesa::test::RowDecision* FindRow(
|
|
const fesa::test::ComparisonReport& report,
|
|
const fesa::test::ComparisonQuantity quantity,
|
|
const std::int64_t source_node_label, const std::string& component) {
|
|
const auto found = std::find_if(report.rows.begin(), report.rows.end(),
|
|
[&](const fesa::test::RowDecision& row) {
|
|
return row.reference.quantity == quantity &&
|
|
row.reference.source_node_label ==
|
|
source_node_label &&
|
|
row.reference.component == component;
|
|
});
|
|
return found == report.rows.end() ? nullptr : &*found;
|
|
}
|
|
|
|
const fesa::test::ComponentMetrics* FindMetric(
|
|
const fesa::test::ComparisonReport& report,
|
|
const std::string& family_identity) {
|
|
const auto found =
|
|
std::find_if(report.metrics.begin(), report.metrics.end(),
|
|
[&](const fesa::test::ComponentMetrics& metric) {
|
|
return metric.family_identity == family_identity;
|
|
});
|
|
return found == report.metrics.end() ? nullptr : &*found;
|
|
}
|
|
|
|
const fesa::test::RowDecision* FindSectionRow(
|
|
const fesa::test::ComparisonReport& report,
|
|
const std::int64_t source_element_label, const int endpoint_index,
|
|
const std::string& component) {
|
|
const auto found = std::find_if(
|
|
report.rows.begin(), report.rows.end(),
|
|
[&](const fesa::test::RowDecision& row) {
|
|
return row.reference.quantity ==
|
|
fesa::test::ComparisonQuantity::kSectionResultant &&
|
|
row.reference.source_element_label == source_element_label &&
|
|
row.reference.endpoint_index == endpoint_index &&
|
|
row.reference.component == component;
|
|
});
|
|
return found == report.rows.end() ? nullptr : &*found;
|
|
}
|
|
|
|
void ExpectExactRowInventory(const fesa::test::ComparisonReport& report) {
|
|
ASSERT_EQ(report.rows.size(), kExpectedRowCount);
|
|
std::size_t row_index = 0U;
|
|
const auto expect_row = [&](const fesa::test::ComparisonQuantity quantity,
|
|
const std::size_t node,
|
|
const std::string& component,
|
|
const std::string& unit,
|
|
const std::string& coordinate_system,
|
|
const std::string& dataset_path) {
|
|
ASSERT_LT(row_index, report.rows.size());
|
|
const auto& row = report.rows[row_index++];
|
|
for (const auto* side : {&row.fesa, &row.reference}) {
|
|
EXPECT_EQ(side->model_id, "cantilever-beam-b33");
|
|
EXPECT_EQ(side->step_name, "Step-1");
|
|
EXPECT_EQ(side->frame_index, 0U);
|
|
EXPECT_EQ(side->instance_name, kInstanceName);
|
|
EXPECT_EQ(side->source_node_label, static_cast<std::int64_t>(node + 1U));
|
|
EXPECT_EQ(side->quantity, quantity);
|
|
EXPECT_EQ(side->component, component);
|
|
EXPECT_EQ(side->unit_dimension, unit);
|
|
EXPECT_EQ(side->coordinate_system, coordinate_system);
|
|
EXPECT_EQ(side->hdf5_dataset_path, dataset_path);
|
|
}
|
|
};
|
|
|
|
const std::array<std::string, 6> displacement_components = {
|
|
"UX", "UY", "UZ", "URX", "URY", "URZ"};
|
|
const std::array<std::string, 6> displacement_units = {
|
|
"length", "length", "length", "radian", "radian", "radian"};
|
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
|
for (std::size_t component = 0U; component < displacement_components.size();
|
|
++component) {
|
|
expect_row(fesa::test::ComparisonQuantity::kDisplacement, node,
|
|
displacement_components[component],
|
|
displacement_units[component], "global-cartesian",
|
|
"/steps/Step-1/frames/0/nodal/displacement");
|
|
}
|
|
}
|
|
|
|
const std::array<std::string, 6> reaction_components = {"RF1", "RF2", "RF3",
|
|
"RM1", "RM2", "RM3"};
|
|
const std::array<std::string, 6> reaction_units = {
|
|
"force", "force", "force",
|
|
"force*length", "force*length", "force*length"};
|
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
|
for (std::size_t component = 0U; component < reaction_components.size();
|
|
++component) {
|
|
expect_row(fesa::test::ComparisonQuantity::kReaction, node,
|
|
reaction_components[component], reaction_units[component],
|
|
"global-cartesian", "/steps/Step-1/frames/0/nodal/reaction");
|
|
}
|
|
}
|
|
|
|
const std::array<std::string, 4> section_components = {"N", "T", "My", "Mz"};
|
|
const std::array<std::string, 4> section_units = {
|
|
"force", "force*length", "force*length", "force*length"};
|
|
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
|
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
|
|
for (std::size_t component = 0U; component < section_components.size();
|
|
++component) {
|
|
ASSERT_LT(row_index, report.rows.size());
|
|
const auto& row = report.rows[row_index++];
|
|
for (const auto* side : {&row.fesa, &row.reference}) {
|
|
EXPECT_EQ(side->source_element_label,
|
|
static_cast<std::int64_t>(element + 1U));
|
|
EXPECT_EQ(side->endpoint_index, static_cast<int>(endpoint));
|
|
EXPECT_EQ(side->source_node_label,
|
|
static_cast<std::int64_t>(element + endpoint + 1U));
|
|
EXPECT_EQ(side->component, section_components[component]);
|
|
EXPECT_EQ(side->unit_dimension, section_units[component]);
|
|
EXPECT_EQ(side->coordinate_system, "beam-local");
|
|
EXPECT_EQ(side->hdf5_dataset_path,
|
|
"/steps/Step-1/frames/0/element/section_resultant");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EXPECT_EQ(row_index, report.rows.size());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(ReferenceComparisonContract,
|
|
PrecheckRejectsMissingSchemaDuplicateAndNonfiniteRows) {
|
|
auto mismatched_values = ReferenceValues();
|
|
mismatched_values.displacement[0U][0U] = 1.0;
|
|
|
|
{
|
|
ContractFixture fixture{"missing-file", mismatched_values};
|
|
ASSERT_TRUE(std::filesystem::remove(fixture.Legacy() / kDisplacementName));
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"needs-reference-artifacts");
|
|
}
|
|
{
|
|
ContractFixture fixture{"b31", mismatched_values};
|
|
auto input = ReadBytes(fixture.Legacy() / kInputName);
|
|
ReplaceFirst(input, "type=B33", "type=B31");
|
|
WriteBytes(fixture.Legacy() / kInputName, input);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"needs-reference-artifacts");
|
|
}
|
|
{
|
|
ContractFixture fixture{"header", mismatched_values};
|
|
auto csv = ReadBytes(fixture.Legacy() / kDisplacementName);
|
|
ReplaceFirst(csv, "U-U1", "U1");
|
|
WriteBytes(fixture.Legacy() / kDisplacementName, csv);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"missing-row", mismatched_values};
|
|
auto lines = ReadLines(fixture.Legacy() / kDisplacementName);
|
|
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
|
lines.pop_back();
|
|
WriteLines(fixture.Legacy() / kDisplacementName, lines);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"extra-row", mismatched_values};
|
|
auto lines = ReadLines(fixture.Legacy() / kDisplacementName);
|
|
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
|
std::string extra = lines.back();
|
|
ReplaceFirst(extra, ",PART-1_1-1,11,", ",PART-1_1-1,12,");
|
|
lines.push_back(std::move(extra));
|
|
WriteLines(fixture.Legacy() / kDisplacementName, lines);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"duplicate-row", mismatched_values};
|
|
auto lines = ReadLines(fixture.Legacy() / kReactionName);
|
|
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
|
lines.push_back(lines[1U]);
|
|
WriteLines(fixture.Legacy() / kReactionName, lines);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"nonfinite-row", mismatched_values};
|
|
auto csv = ReadBytes(fixture.Legacy() / kReactionName);
|
|
ReplaceFirst(csv, "0.000000000E+00", "NaN");
|
|
WriteBytes(fixture.Legacy() / kReactionName, csv);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"identity", mismatched_values};
|
|
auto csv = ReadBytes(fixture.Legacy() / kSectionName);
|
|
ReplaceFirst(csv, "PART-1_1-1", "WRONG-INSTANCE");
|
|
WriteBytes(fixture.Legacy() / kSectionName, csv);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"section-missing-endpoint", mismatched_values};
|
|
auto lines = ReadLines(fixture.Legacy() / kSectionName);
|
|
ASSERT_EQ(lines.size(), kElementCount * 2U + 1U);
|
|
lines.pop_back();
|
|
WriteLines(fixture.Legacy() / kSectionName, lines);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"section-duplicate-endpoint", mismatched_values};
|
|
auto lines = ReadLines(fixture.Legacy() / kSectionName);
|
|
ASSERT_EQ(lines.size(), kElementCount * 2U + 1U);
|
|
lines.push_back(lines[1U]);
|
|
WriteLines(fixture.Legacy() / kSectionName, lines);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
{
|
|
ContractFixture fixture{"section-mismatched-endpoint", mismatched_values};
|
|
auto lines = ReadLines(fixture.Legacy() / kSectionName);
|
|
ASSERT_GT(lines.size(), 2U);
|
|
ReplaceFirst(lines[1U], ",1,1,", ",1,3,");
|
|
WriteLines(fixture.Legacy() / kSectionName, lines);
|
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
|
fixture.Results(), fixture.Legacy()),
|
|
"schema-mismatch");
|
|
}
|
|
}
|
|
|
|
TEST(ReferenceComparisonContract,
|
|
AppliesSharedFamilyScaleAndNearZeroBranchesWithoutAnAbsoluteGate) {
|
|
auto values = ReferenceValues();
|
|
const double translation_scale = 1.90476272e-2;
|
|
const double near_zero_band = 0.01 * translation_scale;
|
|
values.displacement[1U][0U] = 0.999 * near_zero_band;
|
|
values.displacement[2U][0U] = 1.001 * near_zero_band;
|
|
values.section_resultants[0U][0U][2U] = 1.0e7 + 4.999e5;
|
|
values.section_resultants[9U][1U][2U] = 0.0;
|
|
ContractFixture fixture{"tolerance", values};
|
|
|
|
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
|
|
fixture.Legacy());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
EXPECT_FALSE(report.passed);
|
|
EXPECT_EQ(report.rows.size(), kExpectedRowCount);
|
|
EXPECT_EQ(report.metrics.size(), kExpectedMetricCount);
|
|
|
|
const auto* zero =
|
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 1, "UX");
|
|
const auto* near_zero =
|
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 2, "UX");
|
|
const auto* deliberate_failure =
|
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 3, "UX");
|
|
ASSERT_NE(zero, nullptr);
|
|
ASSERT_NE(near_zero, nullptr);
|
|
ASSERT_NE(deliberate_failure, nullptr);
|
|
EXPECT_DOUBLE_EQ(zero->reference.value, 0.0);
|
|
EXPECT_DOUBLE_EQ(zero->fesa.value, 0.0);
|
|
EXPECT_DOUBLE_EQ(zero->tolerance, near_zero_band);
|
|
EXPECT_TRUE(zero->passed);
|
|
EXPECT_TRUE(near_zero->passed);
|
|
EXPECT_FALSE(deliberate_failure->passed);
|
|
EXPECT_EQ(near_zero->tolerance_branch, "near-zero");
|
|
EXPECT_FALSE(near_zero->relative_error_applicable);
|
|
|
|
const auto* my_metric = FindMetric(report, "section-moment");
|
|
ASSERT_NE(my_metric, nullptr);
|
|
EXPECT_DOUBLE_EQ(my_metric->reference_scale, 1.0e7);
|
|
EXPECT_EQ(my_metric->components, (std::vector<std::string>{"T", "My", "Mz"}));
|
|
const auto* scaled = FindSectionRow(report, 1, 0, "My");
|
|
const auto* residue = FindSectionRow(report, 10, 1, "My");
|
|
ASSERT_NE(scaled, nullptr);
|
|
ASSERT_NE(residue, nullptr);
|
|
EXPECT_DOUBLE_EQ(scaled->tolerance, 5.0e5);
|
|
EXPECT_DOUBLE_EQ(scaled->absolute_error, 4.999e5);
|
|
EXPECT_TRUE(scaled->passed);
|
|
EXPECT_DOUBLE_EQ(residue->reference.value, -1.5625e-2);
|
|
EXPECT_DOUBLE_EQ(residue->fesa.value, 0.0);
|
|
EXPECT_DOUBLE_EQ(residue->absolute_error, 1.5625e-2);
|
|
EXPECT_DOUBLE_EQ(residue->tolerance, 1.0e5);
|
|
EXPECT_TRUE(residue->passed);
|
|
}
|
|
|
|
TEST(ReferenceComparisonContract,
|
|
ReportsEveryRowAndAggregateMetricDeterministically) {
|
|
auto values = ReferenceValues();
|
|
values.displacement[0U][0U] = 0.5e-9;
|
|
values.displacement[1U][0U] = -1.0e-9;
|
|
ContractFixture fixture{"metrics", values};
|
|
|
|
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
|
|
fixture.Legacy());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
ASSERT_TRUE(report.passed);
|
|
ExpectExactRowInventory(report);
|
|
ASSERT_EQ(report.metrics.size(), kExpectedMetricCount);
|
|
EXPECT_TRUE(std::all_of(
|
|
report.rows.begin(), report.rows.end(),
|
|
[](const fesa::test::RowDecision& row) { return row.passed; }));
|
|
|
|
const auto* metric = FindMetric(report, "displacement-translation");
|
|
const auto* worst =
|
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 2, "UX");
|
|
ASSERT_NE(metric, nullptr);
|
|
ASSERT_NE(worst, nullptr);
|
|
EXPECT_EQ(metric->row_count, kNodeCount * 3U);
|
|
EXPECT_DOUBLE_EQ(metric->reference_scale, 1.90476272e-2);
|
|
EXPECT_DOUBLE_EQ(metric->maximum_absolute_error, 1.0e-9);
|
|
EXPECT_NEAR(metric->relative_rms,
|
|
std::sqrt(1.25 / static_cast<double>(kNodeCount * 3U)) * 1.0e-9 /
|
|
metric->reference_scale,
|
|
1.0e-21);
|
|
EXPECT_EQ(metric->worst_row,
|
|
static_cast<std::size_t>(worst - report.rows.data()));
|
|
|
|
EXPECT_FALSE(report.stress_comparison_applicable);
|
|
EXPECT_NE(report.stress_comparison_reason.find("N/A"), std::string::npos);
|
|
EXPECT_NE(report.stress_comparison_reason.find("HDF5"), std::string::npos);
|
|
EXPECT_DOUBLE_EQ(report.physics_evidence.free_residual_norm, 0.0);
|
|
EXPECT_EQ(report.physics_evidence.applied_force,
|
|
(std::array<double, 3>{0.0, 0.0, -1.0e6}));
|
|
EXPECT_EQ(report.physics_evidence.reaction_force,
|
|
(std::array<double, 3>{0.0, 0.0, 1.0e6}));
|
|
EXPECT_EQ(report.physics_evidence.applied_moment_about_origin,
|
|
(std::array<double, 3>{0.0, 1.0e7, 0.0}));
|
|
EXPECT_EQ(report.physics_evidence.reaction_moment_about_origin,
|
|
(std::array<double, 3>{0.0, -1.0e7, 0.0}));
|
|
EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed);
|
|
|
|
const auto json_a = fixture.Root() / "comparison-a.json";
|
|
const auto json_b = fixture.Root() / "comparison-b.json";
|
|
ASSERT_TRUE(
|
|
fesa::test::ReferenceComparison::WriteDeterministicJson(report, json_a)
|
|
.IsOk());
|
|
ASSERT_TRUE(
|
|
fesa::test::ReferenceComparison::WriteDeterministicJson(report, json_b)
|
|
.IsOk());
|
|
const std::string first = ReadBytes(json_a);
|
|
EXPECT_EQ(first, ReadBytes(json_b));
|
|
for (const char* required :
|
|
{"\"rows\"", "\"metrics\"", "\"family_identity\"", "\"components\"",
|
|
"\"near_zero_band\"", "\"relative_rms\"", "\"row_count\"",
|
|
"\"tolerance_branch\"", "\"relative_error_applicable\"",
|
|
"\"stress_comparison_applicable\":false",
|
|
"\"stress_comparison_reason\"", "\"physics_evidence\"",
|
|
"\"free_residual_norm\"", "\"applied_force\"", "\"reaction_force\"",
|
|
"\"applied_moment_about_origin\"", "\"reaction_moment_about_origin\"",
|
|
"\"endpoint_consistency_passed\""}) {
|
|
EXPECT_NE(first.find(required), std::string::npos) << required;
|
|
}
|
|
}
|
|
|
|
TEST(ReferenceComparisonContract,
|
|
PreservesSectionEndpointIdentityWithoutStationNormalization) {
|
|
auto values = ReferenceValues();
|
|
values.section_resultants[0U][1U][2U] = 9.0e6 - 5.0;
|
|
ContractFixture fixture{"stations", values};
|
|
|
|
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
|
|
fixture.Legacy());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
ASSERT_TRUE(report.passed);
|
|
EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed);
|
|
|
|
const auto* interior = FindSectionRow(report, 1, 1, "My");
|
|
ASSERT_NE(interior, nullptr);
|
|
EXPECT_DOUBLE_EQ(interior->fesa.value, 9.0e6 - 5.0);
|
|
EXPECT_DOUBLE_EQ(interior->reference.value, 9.0e6);
|
|
EXPECT_DOUBLE_EQ(interior->absolute_error, 5.0);
|
|
const auto* adjacent = FindSectionRow(report, 2, 0, "My");
|
|
ASSERT_NE(adjacent, nullptr);
|
|
EXPECT_DOUBLE_EQ(adjacent->fesa.value, 9.0e6);
|
|
EXPECT_DOUBLE_EQ(adjacent->reference.value, 9.0e6);
|
|
}
|
|
|
|
TEST(ReferenceComparisonContract,
|
|
MapsEachSectionCsvComponentToItsDirectEndpointResultant) {
|
|
auto values = ReferenceValues();
|
|
values.section_resultants[0U][0U] = {11.0, 22.0, 33.0, 44.0};
|
|
ContractFixture fixture{"section-component-mapping", values};
|
|
auto lines = ReadLines(fixture.Legacy() / kSectionName);
|
|
ASSERT_EQ(lines.size(), kElementCount * 2U + 1U);
|
|
lines[1U] = "Increment 1: Step Time = 1.000,PART-1_1-1,1,1,11,33,44,22";
|
|
WriteLines(fixture.Legacy() / kSectionName, lines);
|
|
|
|
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
|
|
fixture.Legacy());
|
|
ASSERT_TRUE(result.HasValue());
|
|
const auto& report = result.Value();
|
|
ASSERT_TRUE(report.passed);
|
|
|
|
const auto* n = FindSectionRow(report, 1, 0, "N");
|
|
const auto* t = FindSectionRow(report, 1, 0, "T");
|
|
const auto* my = FindSectionRow(report, 1, 0, "My");
|
|
const auto* mz = FindSectionRow(report, 1, 0, "Mz");
|
|
ASSERT_NE(n, nullptr);
|
|
ASSERT_NE(t, nullptr);
|
|
ASSERT_NE(my, nullptr);
|
|
ASSERT_NE(mz, nullptr);
|
|
EXPECT_DOUBLE_EQ(n->reference.value, 11.0);
|
|
EXPECT_DOUBLE_EQ(t->reference.value, 22.0);
|
|
EXPECT_DOUBLE_EQ(my->reference.value, 33.0);
|
|
EXPECT_DOUBLE_EQ(mz->reference.value, 44.0);
|
|
EXPECT_DOUBLE_EQ(n->fesa.value, 11.0);
|
|
EXPECT_DOUBLE_EQ(t->fesa.value, 22.0);
|
|
EXPECT_DOUBLE_EQ(my->fesa.value, 33.0);
|
|
EXPECT_DOUBLE_EQ(mz->fesa.value, 44.0);
|
|
}
|