#include "reference_comparison.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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, kNodeCount>; using EndpointValues = std::array, 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{stream}, std::istreambuf_iterator{}}; } 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(contents.size())); if (!stream) { throw std::runtime_error{"Unable to finish build-local fixture write."}; } } std::vector ReadLines(const std::filesystem::path& path) { std::ifstream stream{path}; if (!stream) { throw std::runtime_error{"Unable to read fixture lines."}; } std::vector 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& 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 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 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, 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(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(node + 1U); definition.nodes.push_back({{kInstanceName, label, std::to_string(label)}, {static_cast(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(element + 1U); definition.elements.push_back( {{kInstanceName, label, std::to_string(label)}, {static_cast(element), static_cast(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(element), static_cast(endpoint), domain.Nodes()[node].source_id, {}, values.section_resultants[element][endpoint]}); } state.GaussResults().push_back( {static_cast(element), 1, {}, {}}); state.GaussResults().push_back( {static_cast(element), 2, {}, {}}); state.StressResults().push_back({static_cast(element), 1, 0U, 0.0, 0.0, 0.0, "fesa-default"}); state.StressResults().push_back({static_cast(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 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& 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(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 displacement_components = { "UX", "UY", "UZ", "URX", "URY", "URZ"}; const std::array 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 reaction_components = {"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}; const std::array 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 section_components = {"N", "T", "My", "Mz"}; const std::array 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(element + 1U)); EXPECT_EQ(side->endpoint_index, static_cast(endpoint)); EXPECT_EQ(side->source_node_label, static_cast(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{"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(kNodeCount * 3U)) * 1.0e-9 / metric->reference_scale, 1.0e-21); EXPECT_EQ(metric->worst_row, static_cast(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{0.0, 0.0, -1.0e6})); EXPECT_EQ(report.physics_evidence.reaction_force, (std::array{0.0, 0.0, 1.0e6})); EXPECT_EQ(report.physics_evidence.applied_moment_about_origin, (std::array{0.0, 1.0e7, 0.0})); EXPECT_EQ(report.physics_evidence.reaction_moment_about_origin, (std::array{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); }