feat(nonlinear-static-newton-raphson): step 11 - equilibrium-projection-and-reference-gate

This commit is contained in:
KOKO\Mimi
2026-08-28 09:22:33 +09:00
parent 65b56b1755
commit 6dbf1b2bcf
30 changed files with 13122 additions and 728 deletions
+2
View File
@@ -94,6 +94,8 @@ add_executable(
reference/mitc4_reference_comparison.cpp
reference/mitc4_reference_comparison_test.cpp
reference/mitc4_reference_cases_test.cpp
reference/nonlinear_static_reference_comparison.cpp
reference/nonlinear_static_reference_cases_test.cpp
)
target_link_libraries(
@@ -3,14 +3,21 @@
#include <gtest/gtest.h>
#include <hdf5.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
#include "fesa/elements/finite_rotation.h"
#include "fesa/math/vector3.h"
namespace {
class TempDirectory {
@@ -83,6 +90,53 @@ All, 1, 6
)inp";
}
std::string NonzeroPrescribedNonlinearShellDeck() {
return R"inp(*Part, name=ShellPart
*Node
1, 0., 0., 0.
2, 1., 0., 0.
3, 1., 1., 0.
4, 0., 1., 0.
*Element, type=S4
1, 1, 2, 3, 4
*Elset, elset=ShellSet
1
*Shell Section, elset=ShellSet, material=Steel
0.1
*End Part
*Assembly, name=Assembly
*Instance, name=Shell-1, part=ShellPart
*End Instance
*Nset, nset=N1, instance=Shell-1
1
*Nset, nset=N2, instance=Shell-1
2
*Nset, nset=N3, instance=Shell-1
3
*Nset, nset=N4, instance=Shell-1
4
*End Assembly
*Material, name=Steel
*Elastic
1000., 0.25
*Boundary
N1, 1, 6
N2, 1, 1, 0.2
N2, 2, 3
N2, 4, 4, 0.3
N2, 5, 6
N3, 1, 6
N4, 1, 6
*Step, name=Step-1, nlgeom=YES
*Static
0.1, 1., 0.001, 0.1
*Cload
N2, 3, -3.
N2, 4, 2.
*End Step
)inp";
}
class Hdf5File {
public:
explicit Hdf5File(const std::filesystem::path& path)
@@ -129,6 +183,112 @@ std::string ReadStringAttribute(const hid_t object, const char* name) {
return result;
}
std::vector<double> ReadNodalDataset(const Hdf5File& file,
const std::string& path) {
const hid_t dataset = H5Dopen2(file.Get(), path.c_str(), H5P_DEFAULT);
if (dataset < 0) {
throw std::runtime_error{"Unable to open nonlinear nodal dataset."};
}
std::vector<double> values(24U);
const herr_t read = H5Dread(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, values.data());
(void)H5Dclose(dataset);
if (read < 0 || !std::all_of(values.begin(), values.end(),
[](const double value) {
return std::isfinite(value);
})) {
throw std::runtime_error{"Unable to read nonlinear nodal dataset."};
}
return values;
}
std::vector<std::uint8_t> ReadConstraintMask(const Hdf5File& file) {
const hid_t dataset =
H5Dopen2(file.Get(), "/model/nodal_constraint_mask", H5P_DEFAULT);
if (dataset < 0) {
throw std::runtime_error{"Unable to open nonlinear constraint mask."};
}
std::vector<std::uint8_t> values(24U);
const herr_t read = H5Dread(dataset, H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL,
H5P_DEFAULT, values.data());
(void)H5Dclose(dataset);
if (read < 0 || !std::all_of(values.begin(), values.end(),
[](const std::uint8_t value) {
return value <= 1U;
})) {
throw std::runtime_error{"Unable to read nonlinear constraint mask."};
}
return values;
}
fesa::Vector3 SpatialMoment(const fesa::Vector3& phi,
const fesa::Vector3& generalized) {
const fesa::Matrix transform = fesa::InverseTransposeLeftJacobianSO3(phi);
return {transform(0U, 0U) * generalized.X() +
transform(0U, 1U) * generalized.Y() +
transform(0U, 2U) * generalized.Z(),
transform(1U, 0U) * generalized.X() +
transform(1U, 1U) * generalized.Y() +
transform(1U, 2U) * generalized.Z(),
transform(2U, 0U) * generalized.X() +
transform(2U, 1U) * generalized.Y() +
transform(2U, 2U) * generalized.Z()};
}
std::array<double, 6> ReconstructGlobalBalance(const Hdf5File& file) {
const std::string root = "/steps/Step-1/frames/9/nodal/";
const auto displacement = ReadNodalDataset(file, root + "displacement");
const auto external =
ReadNodalDataset(file, root + "generalized_external_force");
const auto residual =
ReadNodalDataset(file, root + "generalized_residual");
const auto mask = ReadConstraintMask(file);
const std::array<fesa::Vector3, 4> initial_positions{
fesa::Vector3{0.0, 0.0, 0.0}, fesa::Vector3{1.0, 0.0, 0.0},
fesa::Vector3{1.0, 1.0, 0.0}, fesa::Vector3{0.0, 1.0, 0.0}};
fesa::Vector3 applied_force_resultant;
fesa::Vector3 applied_moment_resultant;
fesa::Vector3 reaction_force_resultant;
fesa::Vector3 reaction_moment_resultant;
for (std::size_t node = 0U; node < initial_positions.size(); ++node) {
const std::size_t offset = node * 6U;
const fesa::Vector3 current_position =
initial_positions[node] +
fesa::Vector3{displacement[offset], displacement[offset + 1U],
displacement[offset + 2U]};
const fesa::Vector3 applied_force{external[offset], external[offset + 1U],
external[offset + 2U]};
const fesa::Vector3 reaction_force{
mask[offset] == 1U ? residual[offset] : 0.0,
mask[offset + 1U] == 1U ? residual[offset + 1U] : 0.0,
mask[offset + 2U] == 1U ? residual[offset + 2U] : 0.0};
const fesa::Vector3 phi{displacement[offset + 3U],
displacement[offset + 4U],
displacement[offset + 5U]};
const fesa::Vector3 applied_moment = SpatialMoment(
phi, {external[offset + 3U], external[offset + 4U],
external[offset + 5U]});
const fesa::Vector3 reaction_moment = SpatialMoment(
phi, {mask[offset + 3U] == 1U ? residual[offset + 3U] : 0.0,
mask[offset + 4U] == 1U ? residual[offset + 4U] : 0.0,
mask[offset + 5U] == 1U ? residual[offset + 5U] : 0.0});
applied_force_resultant = applied_force_resultant + applied_force;
applied_moment_resultant =
applied_moment_resultant + current_position.Cross(applied_force) +
applied_moment;
reaction_force_resultant = reaction_force_resultant + reaction_force;
reaction_moment_resultant =
reaction_moment_resultant + current_position.Cross(reaction_force) +
reaction_moment;
}
const fesa::Vector3 force_balance =
applied_force_resultant + reaction_force_resultant;
const fesa::Vector3 moment_balance =
applied_moment_resultant + reaction_moment_resultant;
return {force_balance.X(), force_balance.Y(), force_balance.Z(),
moment_balance.X(), moment_balance.Y(), moment_balance.Z()};
}
// NSNR-PROC-004, NSNR-PROC-007, and NSNR-PROC-010.
TEST(NonlinearStaticProcedure,
AllConstrainedNlgeomRunWritesSchemaV1AfterAcceptedFrames) {
@@ -154,4 +314,33 @@ TEST(NonlinearStaticProcedure,
0);
}
// NSNR-PROC-019.
TEST(NonlinearStaticProcedure,
NonzeroPrescribedMotionWritesCurrentArmSpatialAppliedPlusReactionBalance) {
TempDirectory directory{"current-arm-balance"};
const auto input = directory.Path() / "shellnl-current-arm.inp";
const auto output = directory.Path() / "shellnl-current-arm_results.h5";
WriteText(input, NonzeroPrescribedNonlinearShellDeck());
testing::internal::CaptureStderr();
const int exit_code = fesa::FesaApplication{}.Run({input.string()});
const std::string standard_error = testing::internal::GetCapturedStderr();
ASSERT_EQ(exit_code, 0) << standard_error;
const Hdf5File file{output};
const hid_t dataset = H5Dopen2(
file.Get(), "/steps/Step-1/frames/9/global/equilibrium", H5P_DEFAULT);
ASSERT_GE(dataset, 0);
std::array<double, 6> actual{};
ASSERT_GE(H5Dread(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, actual.data()),
0);
(void)H5Dclose(dataset);
const auto expected = ReconstructGlobalBalance(file);
for (std::size_t component = 0U; component < actual.size(); ++component) {
EXPECT_NEAR(actual[component], expected[component], 1.0e-10)
<< "component=" << component;
}
}
} // namespace
@@ -0,0 +1,401 @@
#include "nonlinear_static_reference_comparison.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
#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 {
std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary};
if (!stream) {
throw std::runtime_error{"Unable to read reference artifact."};
}
return {std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}};
}
fesa::test::NonlinearReferenceCase MakeCase(const std::string& case_id) {
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
const bool fine = case_id == "shellnl-meshfine-s4-final";
const bool nonlinear = case_id == "shellnl-s4-final" || fine;
const std::filesystem::path directory =
root / "reference" / (fine ? "shellNL-meshfine"
: (nonlinear ? "shellNL" : "shell"));
const std::string stem = fine ? "shellNL-meshfine"
: (nonlinear ? "shellNL" : "shell");
return {case_id,
directory / (stem + ".inp"),
directory / (stem + " displacements.csv"),
directory / (stem + " reactions.csv"),
directory / (stem + " stresses.csv"),
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" /
"nonlinear-static-reference" / stem};
}
void ExpectSnapshotsEqual(
const std::vector<fesa::test::ArtifactSnapshot>& expected,
const std::vector<fesa::test::ArtifactSnapshot>& actual) {
ASSERT_EQ(actual.size(), expected.size());
for (std::size_t index = 0U; index < expected.size(); ++index) {
EXPECT_EQ(actual[index].path, expected[index].path);
EXPECT_EQ(actual[index].sha256, expected[index].sha256);
EXPECT_EQ(actual[index].byte_size, expected[index].byte_size);
EXPECT_EQ(actual[index].last_write_time, expected[index].last_write_time);
}
}
const fesa::test::NonlinearReferenceFamilyReport* FindFamily(
const fesa::test::NonlinearReferenceComparisonReport& report,
const std::string& identity) {
const auto found = std::find_if(
report.families.begin(), report.families.end(),
[&identity](const fesa::test::NonlinearReferenceFamilyReport& family) {
return family.family_identity == identity;
});
return found == report.families.end() ? nullptr : &*found;
}
void ExpectFamily(const fesa::test::NonlinearReferenceComparisonReport& report,
const std::string& identity, const std::size_t row_count,
const fesa::test::NonlinearReferenceDisposition disposition) {
const auto* family = FindFamily(report, identity);
ASSERT_NE(family, nullptr) << identity;
EXPECT_EQ(family->row_count, row_count);
EXPECT_EQ(family->disposition, disposition);
}
void ExpectBlockingFamiliesPassed(
const fesa::test::NonlinearReferenceComparisonReport& report) {
for (const auto& family : report.families) {
if (family.disposition !=
fesa::test::NonlinearReferenceDisposition::kBlocking) {
continue;
}
EXPECT_TRUE(family.tolerance.passed)
<< family.family_identity << " rows=" << family.row_count
<< " scale=" << family.tolerance.reference_scale
<< " max_abs=" << family.tolerance.maximum_absolute_error
<< " rms=" << family.tolerance.relative_rms
<< " diagnostic=" << family.tolerance.diagnostic_code;
}
}
void ExpectInternalFamiliesPassed(
const fesa::test::NonlinearReferenceComparisonReport& report) {
for (const auto& family : report.internal_equilibrium) {
EXPECT_TRUE(family.passed)
<< family.family_identity << " rows=" << family.row_count
<< " numerator=" << family.numerator << " scale=" << family.scale
<< " metric="
<< (family.normalized_metric.has_value()
? *family.normalized_metric
: -1.0)
<< " diagnostic=" << family.diagnostic_code;
}
}
} // namespace
// NSNR-REF-001 and NSNR-REF-018.
TEST(NonlinearStaticReference, AuditsEveryDeclaredArtifactBeforeExecution) {
const auto nonlinear = MakeCase("shellnl-s4-final");
const auto linear = MakeCase("shell-s4-linear-limit");
const auto nonlinear_artifacts = fesa::test::ArtifactCheck(nonlinear);
ASSERT_TRUE(nonlinear_artifacts.HasValue());
ASSERT_EQ(nonlinear_artifacts.Value().size(), 4U);
EXPECT_EQ(nonlinear_artifacts.Value()[0U].sha256,
"4BF3ECB0F7F1E8462C573E993DBC97AC5D920C91CF6F23AD4C6891D77FFBADA2");
EXPECT_EQ(nonlinear_artifacts.Value()[3U].sha256,
"5500E5114B1C44887528E30CEC054A1EB0F68BE3F4B8CE81FA59FC74A881483F");
const auto linear_artifacts = fesa::test::ArtifactCheck(linear);
ASSERT_TRUE(linear_artifacts.HasValue());
ASSERT_EQ(linear_artifacts.Value().size(), 4U);
EXPECT_EQ(linear_artifacts.Value()[0U].sha256,
"4005851E1AB22FD3A16AC17A8D5DA3E051233F69F37419079F3553AD134ECFCF");
EXPECT_EQ(linear_artifacts.Value()[3U].sha256,
"2B75A5B0DEB045A283C9BB7E9B1153B79F7537FF487214BF4A23316B59B660A7");
}
// NSNR-REF-002 through NSNR-REF-018.
TEST(NonlinearStaticReference,
CopiesRunsComparesAndPreservesTheReadOnlyReferenceInventory) {
for (const std::string case_id :
{"shellnl-s4-final", "shell-s4-linear-limit"}) {
const auto reference_case = MakeCase(case_id);
const auto before = fesa::test::ArtifactCheck(reference_case);
ASSERT_TRUE(before.HasValue());
const auto copied =
fesa::test::CopyInputToExecutionDirectory(reference_case);
ASSERT_TRUE(copied.HasValue());
EXPECT_NE(copied.Value().parent_path(),
reference_case.original_input_path.parent_path());
EXPECT_EQ(ReadBytes(copied.Value()),
ReadBytes(reference_case.original_input_path));
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
reference_case, copied.Value());
ASSERT_TRUE(generated.HasValue());
EXPECT_EQ(generated.Value().copied_input_path, copied.Value());
EXPECT_TRUE(std::filesystem::is_regular_file(
generated.Value().generated_results_hdf5));
EXPECT_TRUE(std::filesystem::is_regular_file(
generated.Value().generated_log_path));
EXPECT_EQ(generated.Value().generated_results_hdf5.filename(),
case_id == "shellnl-s4-final" ? "shellNL_results.h5"
: "shell_results.h5");
EXPECT_EQ(generated.Value().generated_log_path.filename(),
case_id == "shellnl-s4-final" ? "shellNL_log.txt"
: "shell_log.txt");
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
reference_case, generated.Value().generated_results_hdf5);
ASSERT_TRUE(comparison.HasValue());
ExpectBlockingFamiliesPassed(comparison.Value());
if (case_id == "shellnl-s4-final") {
ExpectInternalFamiliesPassed(comparison.Value());
}
EXPECT_TRUE(comparison.Value().blocking_passed);
EXPECT_EQ(comparison.Value().copied_input_path, copied.Value());
EXPECT_EQ(comparison.Value().generated_log_path,
generated.Value().generated_log_path);
ExpectFamily(comparison.Value(),
case_id == "shellnl-s4-final" ? "coarse.translation"
: "linear.translation",
147U, fesa::test::NonlinearReferenceDisposition::kBlocking);
ExpectFamily(comparison.Value(),
case_id == "shellnl-s4-final" ? "coarse.rotation"
: "linear.rotation",
147U,
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
if (case_id == "shellnl-s4-final") {
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force",
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment",
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
EXPECT_EQ(FindFamily(comparison.Value(), "RM3"), nullptr);
}
const auto after = fesa::test::ArtifactCheck(reference_case);
ASSERT_TRUE(after.HasValue());
ExpectSnapshotsEqual(before.Value(), after.Value());
}
}
// NSNR-REF-009 and NSNR-REF-017.
TEST(NonlinearStaticReference,
RejectsInvalidHdf5BeforeToleranceAndKeepsWarningOnlyClassification) {
const auto reference_case = MakeCase("shellnl-s4-final");
const auto copied = fesa::test::CopyInputToExecutionDirectory(reference_case);
ASSERT_TRUE(copied.HasValue());
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
reference_case, copied.Value());
ASSERT_TRUE(generated.HasValue());
const auto invalid_hdf5 = copied.Value().parent_path() / "invalid.h5";
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
stream << "not an HDF5 file";
stream.close();
EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case,
invalid_hdf5)
.HasValue());
fesa::test::NonlinearReferenceComparisonReport report{};
report.blocking_passed = true;
report.families.push_back(
{"nonlinear.rotation",
{"UR1", "UR2", "UR3"},
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
147U,
{"nonlinear.rotation", {"UR1", "UR2", "UR3"}, false, 1.0, 0.01,
0U, 0.02, 0.02, 0U, false, false, "relative-rms-exceeded", {}, 147U}});
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
}
// NSNR-REF-020.
TEST(NonlinearStaticReference, AuditsFinePrimaryAndCoarseCompanionArtifacts) {
const std::array<std::string, 3> case_ids{
"shellnl-meshfine-s4-final", "shellnl-s4-final",
"shell-s4-linear-limit"};
for (const auto& case_id : case_ids) {
const auto reference_case = MakeCase(case_id);
const auto before = fesa::test::ArtifactCheck(reference_case);
ASSERT_TRUE(before.HasValue()) << case_id;
ASSERT_EQ(before.Value().size(), 4U) << case_id;
const auto copied =
fesa::test::CopyInputToExecutionDirectory(reference_case);
ASSERT_TRUE(copied.HasValue()) << case_id;
EXPECT_NE(copied.Value().parent_path(),
reference_case.original_input_path.parent_path());
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
reference_case, copied.Value());
ASSERT_TRUE(generated.HasValue()) << case_id;
EXPECT_TRUE(std::filesystem::is_regular_file(
generated.Value().generated_results_hdf5));
const auto after = fesa::test::ArtifactCheck(reference_case);
ASSERT_TRUE(after.HasValue()) << case_id;
ExpectSnapshotsEqual(before.Value(), after.Value());
}
}
// NSNR-REF-021.
TEST(NonlinearStaticReference,
ReviewOnlyLocalReactionsCannotChangeBlockingVerdict) {
const auto reference_case = MakeCase("shellnl-s4-final");
const auto copied = fesa::test::CopyInputToExecutionDirectory(reference_case);
ASSERT_TRUE(copied.HasValue());
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
reference_case, copied.Value());
ASSERT_TRUE(generated.HasValue());
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
reference_case, generated.Value().generated_results_hdf5);
ASSERT_TRUE(comparison.HasValue());
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force", 48U,
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment",
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
EXPECT_TRUE(comparison.Value().external_blocking_passed);
}
// NSNR-REF-022.
TEST(NonlinearStaticReference,
BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
for (const std::string case_id :
{"shellnl-meshfine-s4-final", "shellnl-s4-final"}) {
const auto reference_case = MakeCase(case_id);
const auto copied =
fesa::test::CopyInputToExecutionDirectory(reference_case);
ASSERT_TRUE(copied.HasValue()) << case_id;
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
reference_case, copied.Value());
ASSERT_TRUE(generated.HasValue()) << case_id;
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
reference_case, generated.Value().generated_results_hdf5);
ASSERT_TRUE(comparison.HasValue()) << case_id;
const std::string prefix = case_id == "shellnl-meshfine-s4-final"
? "fine."
: "coarse.";
const std::array<std::string, 4> identities{
prefix + "global_force_balance", prefix + "global_moment_balance",
prefix + "accepted_free_force_residual",
prefix + "accepted_free_moment_residual"};
for (const auto& identity : identities) {
const auto found = std::find_if(
comparison.Value().internal_equilibrium.begin(),
comparison.Value().internal_equilibrium.end(),
[&identity](const fesa::test::NonlinearInternalEquilibriumReport& row) {
return row.family_identity == identity;
});
ASSERT_NE(found, comparison.Value().internal_equilibrium.end())
<< identity;
EXPECT_TRUE(found->passed)
<< identity << " numerator=" << found->numerator
<< " scale=" << found->scale << " metric="
<< (found->normalized_metric.has_value()
? *found->normalized_metric
: -1.0)
<< " diagnostic=" << found->diagnostic_code;
}
EXPECT_TRUE(comparison.Value().internal_blocking_passed);
const std::array<double, 3> zero_resultant{};
EXPECT_NE(comparison.Value().constrained_reaction_resultant.force,
zero_resultant);
}
}
// NSNR-REF-023.
TEST(NonlinearStaticReference,
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
fesa::test::NonlinearReferenceComparisonReport report{};
report.blocking_passed = true;
report.external_blocking_passed = true;
report.internal_blocking_passed = true;
report.precheck_findings = {
{"blocking", fesa::test::NonlinearReferenceDisposition::kBlocking,
"missing-row", {}},
{"warning", fesa::test::NonlinearReferenceDisposition::kWarningOnly,
"duplicate-row", {}},
{"review", fesa::test::NonlinearReferenceDisposition::kReviewOnly,
"nonfinite-row", {}}};
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
fesa::test::NonlinearReferenceClassification::kFailed);
EXPECT_EQ(report.precheck_findings[1U].diagnostic_code, "duplicate-row");
EXPECT_EQ(report.precheck_findings[2U].diagnostic_code, "nonfinite-row");
}
// NSNR-REF-024.
TEST(NonlinearStaticReference,
GlobalForceAndMomentBalanceUseApprovedInclusiveOneEighthGate) {
for (const std::string& identity :
{"fine.global_force_balance", "fine.global_moment_balance"}) {
const auto below = fesa::test::EvaluateGlobalBalance(
identity, 5.0e-9, 1.0, 0.0);
EXPECT_TRUE(below.passed) << identity;
ASSERT_TRUE(below.normalized_metric.has_value()) << identity;
EXPECT_EQ(*below.normalized_metric, 5.0e-9) << identity;
const auto boundary = fesa::test::EvaluateGlobalBalance(
identity, 1.0e-8, 1.0, 0.0);
EXPECT_TRUE(boundary.passed) << identity;
ASSERT_TRUE(boundary.normalized_metric.has_value()) << identity;
EXPECT_EQ(*boundary.normalized_metric, 1.0e-8) << identity;
const auto above = fesa::test::EvaluateGlobalBalance(
identity, std::nextafter(1.0e-8,
std::numeric_limits<double>::infinity()),
1.0, 0.0);
EXPECT_FALSE(above.passed) << identity;
ASSERT_TRUE(above.normalized_metric.has_value()) << identity;
EXPECT_GT(*above.normalized_metric, 1.0e-8) << identity;
}
}
// NSNR-REF-025.
TEST(NonlinearStaticReference,
GlobalForceAndMomentExactZeroScaleRequiresExactZeroBalance) {
for (const std::string& identity :
{"coarse.global_force_balance", "coarse.global_moment_balance"}) {
const auto exact_zero =
fesa::test::EvaluateGlobalBalance(identity, 0.0, 0.0, 0.0);
EXPECT_TRUE(exact_zero.passed) << identity;
EXPECT_EQ(exact_zero.scale, 0.0) << identity;
EXPECT_FALSE(exact_zero.normalized_metric.has_value()) << identity;
const auto nonzero =
fesa::test::EvaluateGlobalBalance(identity, 1.0e-300, 0.0, 0.0);
EXPECT_FALSE(nonzero.passed) << identity;
EXPECT_EQ(nonzero.scale, 0.0) << identity;
EXPECT_FALSE(nonzero.normalized_metric.has_value()) << identity;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,129 @@
#ifndef FESA_TESTS_REFERENCE_NONLINEAR_STATIC_REFERENCE_COMPARISON_H_
#define FESA_TESTS_REFERENCE_NONLINEAR_STATIC_REFERENCE_COMPARISON_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include "fesa/core/status.h"
#include "reference_tolerance_policy.h"
namespace fesa::test {
struct NonlinearReferenceCase {
std::string case_id;
std::filesystem::path original_input_path;
std::filesystem::path displacement_csv_path;
std::filesystem::path reaction_csv_path;
std::filesystem::path stress_inventory_path;
std::filesystem::path execution_directory;
};
struct ArtifactSnapshot {
std::filesystem::path path;
std::string sha256;
std::uintmax_t byte_size;
std::filesystem::file_time_type last_write_time;
};
enum class NonlinearReferenceDisposition {
kBlocking,
kWarningOnly,
kReviewOnly,
kExcluded,
};
struct NonlinearReferenceRowKey {
std::string case_id;
std::string accepted_state;
std::string instance_name;
std::int64_t source_node_label;
std::string component;
};
struct NonlinearReferenceFamilyReport {
std::string family_identity;
std::vector<std::string> components;
NonlinearReferenceDisposition disposition;
std::size_t row_count;
ReferenceToleranceFamilyDecision tolerance;
};
struct NonlinearReferencePrecheckFinding {
std::string family_identity;
NonlinearReferenceDisposition disposition;
std::string diagnostic_code;
std::vector<NonlinearReferenceRowKey> affected_keys;
};
struct NonlinearInternalEquilibriumReport {
std::string family_identity;
std::size_t row_count;
double numerator;
double scale;
std::optional<double> normalized_metric;
bool passed;
std::string diagnostic_code;
};
struct ConstrainedReactionResultantReport {
std::array<double, 3> force;
std::array<double, 3> moment;
};
NonlinearInternalEquilibriumReport EvaluateGlobalBalance(
const std::string& family_identity, double numerator,
double applied_scale, double reaction_scale);
struct GeneratedRunArtifacts {
std::filesystem::path copied_input_path;
std::filesystem::path generated_results_hdf5;
std::filesystem::path generated_log_path;
};
struct NonlinearReferenceComparisonReport {
std::string case_id;
std::filesystem::path original_input_path;
std::filesystem::path copied_input_path;
std::filesystem::path generated_results_hdf5;
std::filesystem::path generated_log_path;
std::vector<ArtifactSnapshot> pre_run_artifacts;
std::vector<ArtifactSnapshot> post_run_artifacts;
std::vector<NonlinearReferenceFamilyReport> families;
bool blocking_passed;
std::vector<NonlinearReferencePrecheckFinding> precheck_findings;
std::vector<NonlinearInternalEquilibriumReport> internal_equilibrium;
ConstrainedReactionResultantReport constrained_reaction_resultant;
bool external_blocking_passed;
bool internal_blocking_passed;
};
enum class NonlinearReferenceClassification {
kPassed,
kFailed,
kWarningOnlyDifferences,
};
Result<std::vector<ArtifactSnapshot>> ArtifactCheck(
const NonlinearReferenceCase& reference_case);
Result<std::filesystem::path> CopyInputToExecutionDirectory(
const NonlinearReferenceCase& reference_case);
Result<GeneratedRunArtifacts> RunFesaApplicationOnCopiedInput(
const NonlinearReferenceCase& reference_case,
const std::filesystem::path& copied_input_path);
Result<NonlinearReferenceComparisonReport> CompareHdf5ToReferenceCsv(
const NonlinearReferenceCase& reference_case,
const std::filesystem::path& hdf5_results_path);
NonlinearReferenceClassification ClassifyReferenceComparison(
const NonlinearReferenceComparisonReport& report);
NonlinearReferenceComparisonReport BuildReferenceComparisonReport(
const NonlinearReferenceCase& reference_case,
const std::filesystem::path& hdf5_results_path);
} // namespace fesa::test
#endif // FESA_TESTS_REFERENCE_NONLINEAR_STATIC_REFERENCE_COMPARISON_H_
@@ -80,6 +80,14 @@ std::string ValueAfterEquals(const std::string& field) {
return field.substr(field.find('=') + 1U);
}
fesa::Vector VectorFrom(const std::vector<double>& values) {
fesa::Vector result{values.size()};
for (std::size_t index = 0U; index < values.size(); ++index) {
result[index] = values[index];
}
return result;
}
fesa::NewtonChannelDecision MakeDecision(const std::string& identity) {
return {identity,
0.0012345678901234567,
@@ -324,4 +332,84 @@ TEST(NewtonIterationLog, LeavesZeroByteLogForNonNewtonControlledFailurePath) {
EXPECT_EQ(std::filesystem::file_size(path), 0U);
}
// NSNR-LOG-012
TEST(NewtonIterationLog,
SerializesEvaluatorProducedExternalScaleResidualDecisions) {
TempDirectory directory{"external-scale"};
const fesa::NewtonConvergenceInput zero_external{
{"residual_force", VectorFrom({1.0e-3}), VectorFrom({2.0}),
VectorFrom({0.0})},
{"residual_moment", VectorFrom({1.0e-3}), VectorFrom({3.0}),
VectorFrom({0.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
const auto zero_decisions = fesa::EvaluateNewtonConvergence(zero_external);
auto zero_record = CompleteRecord();
zero_record.force_residual = zero_decisions.force_residual;
zero_record.moment_residual = zero_decisions.moment_residual;
zero_record.translation_correction = zero_decisions.translation_correction;
zero_record.rotation_correction = zero_decisions.rotation_correction;
zero_record.combined_passed = zero_decisions.combined_passed;
const auto zero_fields =
WriteAndRead(directory.Path() / "zero_log.txt", zero_record);
ASSERT_EQ(zero_fields.size(), 39U);
EXPECT_EQ(FieldValue(zero_fields, "schema"), "schema=nsnr-log-v1");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_denominator_scale"),
"residual_force_denominator_scale=0");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_denominator_scale"),
"residual_moment_denominator_scale=0");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_branch"),
"residual_force_branch=zero-scale");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_branch"),
"residual_moment_branch=zero-scale");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_threshold"),
"residual_force_threshold=0.001");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_threshold"),
"residual_moment_threshold=0.001");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_threshold_unit"),
"residual_force_threshold_unit=force");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_threshold_unit"),
"residual_moment_threshold_unit=force*length");
const fesa::NewtonConvergenceInput nonzero_external{
{"residual_force", VectorFrom({2.0e-2}), VectorFrom({2.0}),
VectorFrom({1.0})},
{"residual_moment", VectorFrom({2.0e-2}), VectorFrom({1.0}),
VectorFrom({2.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
const auto relative_decisions =
fesa::EvaluateNewtonConvergence(nonzero_external);
auto relative_record = CompleteRecord();
relative_record.force_residual = relative_decisions.force_residual;
relative_record.moment_residual = relative_decisions.moment_residual;
relative_record.translation_correction =
relative_decisions.translation_correction;
relative_record.rotation_correction = relative_decisions.rotation_correction;
relative_record.combined_passed = relative_decisions.combined_passed;
const auto relative_fields =
WriteAndRead(directory.Path() / "relative_log.txt", relative_record);
ASSERT_EQ(relative_fields.size(), 39U);
EXPECT_EQ(FieldValue(relative_fields, "residual_force_denominator_scale"),
"residual_force_denominator_scale=2");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_denominator_scale"),
"residual_moment_denominator_scale=2");
EXPECT_EQ(FieldValue(relative_fields, "residual_force_branch"),
"residual_force_branch=relative");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_branch"),
"residual_moment_branch=relative");
EXPECT_EQ(FieldValue(relative_fields, "residual_force_threshold"),
"residual_force_threshold=0.01");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_threshold"),
"residual_moment_threshold=0.01");
EXPECT_EQ(FieldValue(relative_fields, "residual_force_threshold_unit"),
"residual_force_threshold_unit=1");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_threshold_unit"),
"residual_moment_threshold_unit=1");
}
} // namespace
@@ -143,6 +143,18 @@ fesa::NewtonConvergenceInput RelativeConvergenceInput() {
};
}
fesa::NewtonConvergenceInput ExactZeroExternalResidualInput(
const double force_numerator, const double moment_numerator) {
return {
{"residual_force", VectorFrom({force_numerator}), VectorFrom({2.0}),
VectorFrom({0.0})},
{"residual_moment", VectorFrom({moment_numerator}), VectorFrom({3.0}),
VectorFrom({0.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
}
// NSNR-CTRL-001, NSNR-CTRL-002, and NSNR-CTRL-017
TEST(NonlinearStaticControl, BuildsFixedFactorSequenceWithExactEndpoint) {
const auto fixture = MakeFixture();
@@ -362,4 +374,161 @@ TEST(NonlinearConvergence, AppliesTypedZeroScaleGatesToEmptyChannels) {
}
}
// NSNR-CTRL-018
TEST(NonlinearConvergence,
SelectsTypedZeroScaleFromExactZeroExternalResidualChannels) {
const auto accepted = fesa::EvaluateNewtonConvergence(
ExactZeroExternalResidualInput(fesa::kNewtonZeroScaleThreshold,
fesa::kNewtonZeroScaleThreshold));
EXPECT_TRUE(accepted.combined_passed);
const std::array<fesa::NewtonChannelDecision, 2U> decisions{
accepted.force_residual, accepted.moment_residual};
const std::array<fesa::NewtonChannelUnit, 2U> units{
fesa::NewtonChannelUnit::kForce,
fesa::NewtonChannelUnit::kForceLength};
for (std::size_t index = 0U; index < decisions.size(); ++index) {
EXPECT_DOUBLE_EQ(decisions[index].denominator_scale, 0.0);
EXPECT_EQ(decisions[index].branch,
fesa::NewtonConvergenceBranch::kZeroScale);
EXPECT_DOUBLE_EQ(decisions[index].threshold,
fesa::kNewtonZeroScaleThreshold);
EXPECT_EQ(decisions[index].threshold_unit, units[index]);
EXPECT_TRUE(decisions[index].passed);
}
const double just_over = std::nextafter(
fesa::kNewtonZeroScaleThreshold,
std::numeric_limits<double>::infinity());
const auto force_rejected =
fesa::EvaluateNewtonConvergence(ExactZeroExternalResidualInput(
just_over, fesa::kNewtonZeroScaleThreshold));
const auto moment_rejected =
fesa::EvaluateNewtonConvergence(ExactZeroExternalResidualInput(
fesa::kNewtonZeroScaleThreshold, just_over));
EXPECT_FALSE(force_rejected.force_residual.passed);
EXPECT_FALSE(force_rejected.combined_passed);
EXPECT_FALSE(moment_rejected.moment_residual.passed);
EXPECT_FALSE(moment_rejected.combined_passed);
}
// NSNR-CTRL-019
TEST(NonlinearConvergence,
UsesRelativeResidualScaleOnlyWhenMatchingExternalScaleIsNonzero) {
fesa::NewtonConvergenceInput input{
{"residual_force", VectorFrom({2.0e-2}), VectorFrom({2.0}),
VectorFrom({1.0})},
{"residual_moment", VectorFrom({2.0e-2}), VectorFrom({1.0}),
VectorFrom({2.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
const auto accepted = fesa::EvaluateNewtonConvergence(input);
EXPECT_TRUE(accepted.combined_passed);
for (const auto& decision : {accepted.force_residual,
accepted.moment_residual}) {
EXPECT_DOUBLE_EQ(decision.denominator_scale, 2.0);
EXPECT_EQ(decision.branch, fesa::NewtonConvergenceBranch::kRelative);
EXPECT_DOUBLE_EQ(decision.threshold, fesa::kNewtonRelativeThreshold);
EXPECT_EQ(decision.threshold_unit, fesa::NewtonChannelUnit::kDimensionless);
EXPECT_TRUE(decision.passed);
}
const double just_over = std::nextafter(2.0e-2,
std::numeric_limits<double>::infinity());
input.force_residual.free_residual = VectorFrom({just_over});
EXPECT_FALSE(fesa::EvaluateNewtonConvergence(input).force_residual.passed);
input.force_residual.free_residual = VectorFrom({2.0e-2});
input.moment_residual.free_residual = VectorFrom({just_over});
EXPECT_FALSE(fesa::EvaluateNewtonConvergence(input).moment_residual.passed);
input.moment_residual.free_residual = VectorFrom({2.0e-2});
input.force_residual.free_residual = VectorFrom({9.0e-4});
input.force_residual.free_internal = VectorFrom({5.0e-3});
input.force_residual.free_external = VectorFrom({1.0e-2});
const auto relative_rejected = fesa::EvaluateNewtonConvergence(input);
EXPECT_FALSE(relative_rejected.force_residual.passed);
EXPECT_FALSE(relative_rejected.combined_passed);
}
// NSNR-CTRL-020
TEST(NonlinearConvergence,
RejectsNonfiniteResidualInputsBeforeSelectingOrAcceptingAChannel) {
const std::array<double, 2U> invalid_values{
std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity()};
for (const double invalid : invalid_values) {
for (const bool is_moment : {false, true}) {
for (const std::size_t vector_index : {0U, 1U, 2U}) {
auto input = RelativeConvergenceInput();
fesa::NewtonResidualChannelInput* channel =
is_moment ? &input.moment_residual : &input.force_residual;
if (vector_index == 0U) {
channel->free_residual = VectorFrom({invalid});
} else if (vector_index == 1U) {
channel->free_internal = VectorFrom({invalid});
} else {
channel->free_external = VectorFrom({invalid});
}
const auto result = fesa::EvaluateNewtonConvergence(input);
const auto& decision =
is_moment ? result.moment_residual : result.force_residual;
EXPECT_FALSE(decision.passed);
EXPECT_FALSE(result.combined_passed);
}
}
}
auto input = RelativeConvergenceInput();
const double smallest_external = std::nextafter(
0.0, std::numeric_limits<double>::infinity());
input.force_residual.free_residual = VectorFrom({0.0});
input.force_residual.free_internal = VectorFrom({0.0});
input.force_residual.free_external = VectorFrom({smallest_external});
const auto relative = fesa::EvaluateNewtonConvergence(input);
EXPECT_TRUE(relative.force_residual.passed);
EXPECT_DOUBLE_EQ(relative.force_residual.denominator_scale,
smallest_external);
EXPECT_EQ(relative.force_residual.branch,
fesa::NewtonConvergenceBranch::kRelative);
EXPECT_DOUBLE_EQ(relative.force_residual.threshold,
fesa::kNewtonRelativeThreshold);
EXPECT_EQ(relative.force_residual.threshold_unit,
fesa::NewtonChannelUnit::kDimensionless);
}
// NSNR-CTRL-021
TEST(NonlinearConvergence, RequiresEveryResidualAndCorrectionChannelToPass) {
const auto accepted = fesa::EvaluateNewtonConvergence(RelativeConvergenceInput());
EXPECT_TRUE(accepted.combined_passed);
EXPECT_DOUBLE_EQ(accepted.translation_correction.denominator_scale, 1.0);
EXPECT_DOUBLE_EQ(accepted.rotation_correction.denominator_scale, 1.0);
EXPECT_DOUBLE_EQ(accepted.translation_correction.threshold,
fesa::kNewtonRelativeThreshold);
EXPECT_DOUBLE_EQ(accepted.rotation_correction.threshold,
fesa::kNewtonRelativeThreshold);
EXPECT_EQ(accepted.translation_correction.threshold_unit,
fesa::NewtonChannelUnit::kDimensionless);
EXPECT_EQ(accepted.rotation_correction.threshold_unit,
fesa::NewtonChannelUnit::kDimensionless);
for (const std::size_t failed_channel : {0U, 1U, 2U, 3U}) {
auto input = RelativeConvergenceInput();
const double just_over = std::nextafter(
1.0e-2, std::numeric_limits<double>::infinity());
if (failed_channel == 0U) {
input.force_residual.free_residual = VectorFrom({just_over});
} else if (failed_channel == 1U) {
input.moment_residual.free_residual = VectorFrom({just_over});
} else if (failed_channel == 2U) {
input.translation_correction.fresh_correction = VectorFrom({just_over});
} else {
input.rotation_correction.fresh_correction = VectorFrom({just_over});
}
const auto result = fesa::EvaluateNewtonConvergence(input);
EXPECT_FALSE(result.combined_passed);
}
}
} // namespace
@@ -534,6 +534,31 @@ TEST(NonlinearHdf5ResultsWriter, ProjectsRotationalReactionToSpatialMoment) {
EXPECT_GT(quadratic, 0.0);
}
// NSNR-HDF5-014.
TEST(NonlinearHdf5ResultsWriter, WritesSchemaV1GlobalBalanceErrorLocation) {
TempDirectory directory{"global-balance-location"};
auto fixture = MakeFixture(directory.Path() / "global-balance.inp");
const auto output = directory.Path() / "global-balance_results.h5";
fesa::NonlinearHdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output, *fixture.domain, fixture.state, {}).IsOk());
const auto file = OpenFile(output);
const auto equilibrium = OpenDataset(
file.Get(), "/steps/Step-1/frames/1/global/equilibrium");
EXPECT_EQ(DatasetDimensions(file.Get(),
"/steps/Step-1/frames/1/global/equilibrium"),
std::vector<hsize_t>({6U}));
EXPECT_EQ(ReadStringAttribute(equilibrium.Get(), "component_names"),
"FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3");
EXPECT_EQ(ReadStringAttribute(equilibrium.Get(), "location"),
"global-balance-error");
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT),
H5Gclose};
ASSERT_GE(metadata.Get(), 0);
EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 1U);
}
// NSNR-HDF5-012 and NSNR-HDF5-013.
TEST(NonlinearHdf5ResultsWriter, StoresSortedDiagnosticsThroughBackendNeutralBoundary) {
static_assert(std::is_base_of_v<fesa::NonlinearResultsWriter,