feat(nonlinear-static-newton-raphson): step 11 - equilibrium-projection-and-reference-gate
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user