Files
FESADev/tests/unit/analysis/hht_dynamic_procedure_test.cpp

1021 lines
48 KiB
C++

#include "fesa/analysis/hht_dynamic_procedure.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <optional>
#include <sstream>
#include <map>
#include <string>
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/analysis/analysis_model.h"
#include "analysis/hht_dynamic_internal.h"
#include "analysis/hht_reduced_integrator.h"
#include "analysis/hht_reduced_procedure.h"
#include "analysis/equilibrium_algorithm.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
namespace {
fesa::Status InjectedTangentFailure() {
return fesa::Status::Failure(
fesa::FailureCategory::kSolver,
{{fesa::Severity::kError, "injected-tangent-backend", {}, "DYNAMIC",
{}, "Injected tangent backend failure."}});
}
class FailingTangent final : public fesa::TangentSolver {
public:
fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override {
return InjectedTangentFailure();
}
fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override {
return InjectedTangentFailure();
}
};
std::string FailureText(const fesa::Status& status) {
std::string result;
for (const auto& diagnostic : status.Diagnostics()) {
if (!result.empty()) result += " | ";
result += diagnostic.code + ": " + diagnostic.message;
}
return result;
}
fesa::ModelDefinition MakeDefinition(bool fixed = true);
fesa::ModelDefinition MakeRigidTranslationDefinition() {
auto definition = MakeDefinition(false);
const std::filesystem::path source{"dynamic-state.inp"};
definition.nodes.insert(
definition.nodes.end(),
{{{"Shell-B", 50, "0050"}, {2.0, 0.0, 0.0}, {source, 14U}},
{{"Shell-B", 60, "0060"}, {3.0, 0.0, 0.0}, {source, 15U}},
{{"Shell-B", 70, "0070"}, {3.0, 1.0, 0.0}, {source, 16U}},
{{"Shell-B", 80, "0080"}, {2.0, 1.0, 0.0}, {source, 17U}}});
definition.shell_elements.push_back(
{{"Shell-B", 19, "0019"}, fesa::ShellSourceElementType::kS4,
{4U, 5U, 6U, 7U}, 0U, 0U, {source, 18U}});
for (std::size_t node = 4U; node < 8U; ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node), {0.0, 0.0, 1.0},
{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}});
}
definition.node_sets[0U] =
{"All-A", std::string{"Shell-A"}, {0U, 1U, 2U, 3U}, {source, 9U}};
definition.node_sets.push_back(
{"All-B", std::string{"Shell-B"}, {4U, 5U, 6U, 7U}, {source, 19U}});
definition.dynamic_step->loads = {{"All-A", 1, 1.0, {source, 20U}},
{"All-B", 1, 1.0, {source, 21U}}};
return definition;
}
std::array<double, 3> RotateGlobal(const std::array<double, 3>& value) {
constexpr double kCosZ = 0.7648421872844885;
constexpr double kSinZ = 0.6442176872376910;
constexpr double kCosX = 0.9210609940028851;
constexpr double kSinX = 0.3894183423086505;
return {kCosZ * value[0U] - kSinZ * kCosX * value[1U] +
kSinZ * kSinX * value[2U],
kSinZ * value[0U] + kCosZ * kCosX * value[1U] -
kCosZ * kSinX * value[2U],
kSinX * value[1U] + kCosX * value[2U]};
}
fesa::ModelDefinition MakeRotatedRigidTranslationDefinition() {
auto definition = MakeRigidTranslationDefinition();
for (auto& node : definition.nodes) node.coordinates = RotateGlobal(node.coordinates);
for (auto& frame : definition.shell_node_initial_frames) {
frame.director = RotateGlobal(frame.director);
frame.tangent_a = RotateGlobal(frame.tangent_a);
frame.tangent_b = RotateGlobal(frame.tangent_b);
}
const auto load = RotateGlobal({1.0, 0.0, 0.0});
const std::filesystem::path source{"dynamic-state.inp"};
definition.dynamic_step->loads = {{"All-A", 1, load[0U], {source, 22U}},
{"All-A", 2, load[1U], {source, 23U}},
{"All-A", 3, load[2U], {source, 24U}},
{"All-B", 1, load[0U], {source, 25U}},
{"All-B", 2, load[1U], {source, 26U}},
{"All-B", 3, load[2U], {source, 27U}}};
return definition;
}
fesa::ModelDefinition MakeFiniteRotationDefinition() {
auto definition = MakeDefinition(false);
definition.dynamic_step->initial_increment = 0.01;
definition.dynamic_step->time_period = 0.02;
definition.dynamic_step->increment_count = 2U;
definition.dynamic_step->loads = {
{"Loaded", 3, 1.0, {"dynamic-state.inp", 23U}}};
return definition;
}
fesa::ModelDefinition MakeRotatedFiniteRotationDefinition() {
auto definition = MakeFiniteRotationDefinition();
for (auto& node : definition.nodes) node.coordinates = RotateGlobal(node.coordinates);
for (auto& frame : definition.shell_node_initial_frames) {
frame.director = RotateGlobal(frame.director);
frame.tangent_a = RotateGlobal(frame.tangent_a);
frame.tangent_b = RotateGlobal(frame.tangent_b);
}
const auto load = RotateGlobal({0.0, 0.0, 1.0});
const std::filesystem::path source{"dynamic-state.inp"};
definition.dynamic_step->loads = {{"Loaded", 1, load[0U], {source, 24U}},
{"Loaded", 2, load[1U], {source, 25U}},
{"Loaded", 3, load[2U], {source, 26U}}};
return definition;
}
struct RigidTranslationRun {
fesa::DynamicExecutionResult execution;
std::string log;
};
void ExpectSameSourceIds(const std::vector<fesa::SourceEntityId>& left,
const std::vector<fesa::SourceEntityId>& right) {
ASSERT_EQ(left.size(), right.size());
for (std::size_t i = 0U; i < left.size(); ++i) {
EXPECT_EQ(left[i].instance_name, right[i].instance_name);
EXPECT_EQ(left[i].source_label, right[i].source_label);
EXPECT_EQ(left[i].source_label_text, right[i].source_label_text);
}
}
fesa::Result<RigidTranslationRun> RunRigidTranslation(
const fesa::ModelDefinition& definition, const fesa::ParallelFor& parallel,
const std::string& label) {
auto domain = fesa::Domain::Create(definition);
if (!domain.HasValue()) return fesa::Result<RigidTranslationRun>::Failure(
domain.GetStatus());
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path = std::filesystem::temp_directory_path() /
("fesa-hht-rigid-" + label + ".log");
auto status = log.Open(path);
if (!status.IsOk()) return fesa::Result<RigidTranslationRun>::Failure(status);
auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value(), log);
status = log.Close();
std::string log_text;
{
std::ifstream input{path, std::ios::binary};
log_text.assign(std::istreambuf_iterator<char>{input}, {});
}
std::filesystem::remove(path);
if (!status.IsOk()) return fesa::Result<RigidTranslationRun>::Failure(status);
if (!result.HasValue()) return fesa::Result<RigidTranslationRun>::Failure(
result.GetStatus());
return fesa::Result<RigidTranslationRun>::Success(
{std::move(result.Value()), log_text});
}
void ExpectSameDynamicHistory(const RigidTranslationRun& left,
const RigidTranslationRun& right) {
EXPECT_EQ(left.log, right.log);
const auto& left_frames = left.execution.state.AcceptedFrames();
const auto& right_frames = right.execution.state.AcceptedFrames();
ASSERT_EQ(left_frames.size(), right_frames.size());
for (std::size_t frame = 0U; frame < left_frames.size(); ++frame) {
const auto& lhs = left_frames[frame];
const auto& rhs = right_frames[frame];
EXPECT_EQ(lhs.time, rhs.time);
EXPECT_EQ(lhs.iteration_count, rhs.iteration_count);
EXPECT_EQ(lhs.nodal_internal_order, rhs.nodal_internal_order);
ExpectSameSourceIds(lhs.nodal_source_ids, rhs.nodal_source_ids);
EXPECT_EQ(lhs.nodal_triad, rhs.nodal_triad);
EXPECT_EQ(lhs.nodal_director, rhs.nodal_director);
for (const auto* values : {&lhs.generalized_coordinates, &lhs.velocity,
&lhs.acceleration, &lhs.algorithmic_acceleration,
&lhs.inertia_force, &lhs.internal_force,
&lhs.external_force, &lhs.residual, &lhs.reaction}) {
const auto* other = values == &lhs.generalized_coordinates
? &rhs.generalized_coordinates
: values == &lhs.velocity ? &rhs.velocity
: values == &lhs.acceleration ? &rhs.acceleration
: values == &lhs.algorithmic_acceleration
? &rhs.algorithmic_acceleration
: values == &lhs.inertia_force ? &rhs.inertia_force
: values == &lhs.internal_force ? &rhs.internal_force
: values == &lhs.external_force ? &rhs.external_force
: values == &lhs.residual ? &rhs.residual
: &rhs.reaction;
ASSERT_EQ(values->Size(), other->Size());
for (std::size_t i = 0U; i < values->Size(); ++i)
EXPECT_DOUBLE_EQ((*values)[i], (*other)[i]);
}
EXPECT_DOUBLE_EQ(lhs.kinetic_energy, rhs.kinetic_energy);
EXPECT_DOUBLE_EQ(lhs.physical_strain_energy, rhs.physical_strain_energy);
EXPECT_DOUBLE_EQ(lhs.drilling_energy, rhs.drilling_energy);
EXPECT_DOUBLE_EQ(lhs.external_work, rhs.external_work);
EXPECT_DOUBLE_EQ(lhs.energy_defect, rhs.energy_defect);
ASSERT_TRUE(lhs.mitc4_drilling);
ASSERT_TRUE(rhs.mitc4_drilling);
EXPECT_EQ(lhs.mitc4_drilling->mode, rhs.mitc4_drilling->mode);
EXPECT_EQ(lhs.mitc4_drilling->drilling_moment, rhs.mitc4_drilling->drilling_moment);
EXPECT_EQ(lhs.mitc4_drilling->equilibrium_with_drilling,
rhs.mitc4_drilling->equilibrium_with_drilling);
}
}
void ExpectRotatedDofVector(const fesa::Vector& source, const fesa::Vector& target,
const double scale) {
ASSERT_EQ(source.Size(), target.Size());
ASSERT_EQ(source.Size() % 6U, 0U);
for (std::size_t node = 0U; node < source.Size() / 6U; ++node)
for (const std::size_t offset : {0U, 3U}) {
const auto rotated = RotateGlobal(
{source[6U * node + offset], source[6U * node + offset + 1U],
source[6U * node + offset + 2U]});
for (std::size_t component = 0U; component < 3U; ++component)
EXPECT_LE(std::abs(rotated[component] -
target[6U * node + offset + component]) /
scale,
1e-10);
}
}
void ExpectCovariantDynamicHistory(const RigidTranslationRun& original,
const RigidTranslationRun& rotated,
const double time_scale,
const double reference_energy) {
constexpr double kCoordinateScale = 1.0;
const double velocity_scale = kCoordinateScale / time_scale;
const double acceleration_scale = kCoordinateScale / (time_scale * time_scale);
const auto& original_frames = original.execution.state.AcceptedFrames();
const auto& rotated_frames = rotated.execution.state.AcceptedFrames();
ASSERT_EQ(original_frames.size(), rotated_frames.size());
for (std::size_t frame = 0U; frame < original_frames.size(); ++frame) {
const auto& source = original_frames[frame];
const auto& target = rotated_frames[frame];
ExpectSameSourceIds(source.nodal_source_ids, target.nodal_source_ids);
EXPECT_LE(std::abs(source.kinetic_energy - target.kinetic_energy) /
reference_energy,
1e-10);
EXPECT_LE(std::abs(source.physical_strain_energy -
target.physical_strain_energy) /
reference_energy,
1e-10);
EXPECT_LE(std::abs(source.drilling_energy - target.drilling_energy) /
reference_energy,
1e-10);
ExpectRotatedDofVector(source.generalized_coordinates,
target.generalized_coordinates, kCoordinateScale);
ExpectRotatedDofVector(source.velocity, target.velocity, velocity_scale);
ExpectRotatedDofVector(source.acceleration, target.acceleration,
acceleration_scale);
ExpectRotatedDofVector(source.algorithmic_acceleration,
target.algorithmic_acceleration, acceleration_scale);
for (const auto* values : {&source.inertia_force, &source.internal_force,
&source.external_force, &source.residual,
&source.reaction}) {
const auto* other = values == &source.inertia_force ? &target.inertia_force
: values == &source.internal_force ? &target.internal_force
: values == &source.external_force ? &target.external_force
: values == &source.residual ? &target.residual
: &target.reaction;
ExpectRotatedDofVector(*values, *other, 1.0);
}
for (std::size_t node = 0U; node < source.nodal_triad.size(); ++node)
for (std::size_t row = 0U; row < 3U; ++row) {
const auto triad = RotateGlobal(source.nodal_triad[node][row]);
const auto director = RotateGlobal(source.nodal_director[node]);
for (std::size_t component = 0U; component < 3U; ++component) {
EXPECT_LE(std::abs(triad[component] -
target.nodal_triad[node][row][component]),
1e-10);
EXPECT_LE(std::abs(director[component] -
target.nodal_director[node][component]),
1e-10);
}
}
}
}
fesa::ModelDefinition MakeDefinition(const bool fixed) {
const std::filesystem::path source{"dynamic-state.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "hht-output-unit-fixture";
definition.nodes = {{{"Shell-A", 10, "0010"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}};
definition.densities = {{0U, 2.0, {source, 6U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}};
definition.shell_elements = {{{"Shell-A", 9, "0009"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 8U}}};
for (std::size_t node = 0U; node < 4U; ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.node_sets = {{"All", std::nullopt, {0U, 1U, 2U, 3U}, {source, 9U}},
{"Loaded", std::nullopt, {0U}, {source, 10U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Transient Load";
step.source_step_name_canonical = "TRANSIENT LOAD";
step.initial_increment = 0.25;
step.time_period = 0.5;
step.increment_count = 2U;
step.maximum_increments = 3U;
if (fixed) {
step.boundaries = {{"All", 1, 6, 0.0, {source, 11U}}};
step.loads = {{"Loaded", 3, 4.0, {source, 12U}}};
}
definition.dynamic_step = step;
return definition;
}
TEST(HhtDynamicProcedure, ClampedStepLoadHasPhysicalReactionAndZeroMotion) {
auto domain = fesa::Domain::Create(MakeDefinition());
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path =
std::filesystem::temp_directory_path() / "fesa-hht-clamped.log";
ASSERT_TRUE(log.Open(path).IsOk());
const fesa::HhtDynamicProcedureExecutor executor(parallel, mass, tangent);
auto result = executor.Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
std::filesystem::remove(path);
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics().front().message);
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
for (const auto& frame : result.Value().state.AcceptedFrames()) {
EXPECT_EQ(frame.time_integration_formulation,
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
EXPECT_EQ(frame.iteration_count, 0U);
ASSERT_TRUE(frame.mitc4_drilling);
EXPECT_EQ(frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
EXPECT_EQ(frame.mitc4_drilling->drilling_moment, (std::array<double, 3>{}));
EXPECT_EQ(frame.mitc4_drilling->equilibrium_with_drilling, frame.global_equilibrium);
EXPECT_DOUBLE_EQ(frame.reaction[2U], -4.0);
EXPECT_DOUBLE_EQ(frame.external_force[2U], 4.0);
EXPECT_DOUBLE_EQ(frame.kinetic_energy, 0.0);
EXPECT_DOUBLE_EQ(frame.energy_defect, 0.0);
for (std::size_t i = 0; i < 24U; ++i) {
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], 0.0);
EXPECT_DOUBLE_EQ(frame.acceleration[i], 0.0);
}
}
}
TEST(HhtDynamicProcedure, UnloadedFreeChannelsRemainExactlyAtRest) {
for (const int fixed_first_dof : {0, 1, 4}) {
SCOPED_TRACE(fixed_first_dof);
auto definition = MakeDefinition(false);
if (fixed_first_dof != 0) {
definition.dynamic_step->boundaries = {{"All",
fixed_first_dof,
fixed_first_dof + 2,
0.0,
{"dynamic-state.inp", 11U}}};
}
auto domain = fesa::Domain::Create(std::move(definition));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path =
std::filesystem::temp_directory_path() / "fesa-hht-rest.log";
ASSERT_TRUE(log.Open(path).IsOk());
auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
std::filesystem::remove(path);
ASSERT_TRUE(result.HasValue())
<< (result.HasValue()
? ""
: result.GetStatus().Diagnostics().front().message);
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
for (const auto& frame : result.Value().state.AcceptedFrames()) {
EXPECT_EQ(frame.time_integration_formulation,
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
EXPECT_EQ(frame.iteration_count, 0U);
for (const auto* values :
{&frame.generalized_coordinates, &frame.velocity,
&frame.acceleration, &frame.algorithmic_acceleration,
&frame.reaction, &frame.residual}) {
for (std::size_t i = 0; i < values->Size(); ++i)
EXPECT_DOUBLE_EQ((*values)[i], 0.0);
}
EXPECT_DOUBLE_EQ(frame.kinetic_energy, 0.0);
EXPECT_DOUBLE_EQ(frame.physical_strain_energy, 0.0);
EXPECT_DOUBLE_EQ(frame.drilling_energy, 0.0);
EXPECT_DOUBLE_EQ(frame.energy_defect, 0.0);
for (const double metric : frame.free_residual_metrics)
EXPECT_DOUBLE_EQ(metric, 0.0);
}
}
}
TEST(HhtDynamicProcedure, LargeDirectStepStartsFromAcceptedGeometry) {
auto definition = MakeDefinition(false);
definition.materials[0U].youngs_modulus = 1e9;
definition.dynamic_step->increment_count = 1U;
definition.dynamic_step->time_period =
definition.dynamic_step->initial_increment;
definition.node_sets[0U].node_indices = {0U, 3U};
definition.node_sets[1U].node_indices = {2U};
definition.dynamic_step->boundaries = {
{"All", 1, 6, 0.0, {"dynamic-state.inp", 11U}}};
definition.dynamic_step->loads = {
{"Loaded", 3, -40000.0, {"dynamic-state.inp", 12U}}};
auto domain = fesa::Domain::Create(std::move(definition));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path =
std::filesystem::temp_directory_path() / "fesa-hht-large-direct.log";
ASSERT_TRUE(log.Open(path).IsOk());
auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics().front().message);
EXPECT_GT(
std::abs(result.Value().state.AcceptedFrames()[0U].acceleration[14U]),
1e5);
EXPECT_LT(result.Value().state.CommittedState().generalized_coordinates[14U],
0.0);
for (double metric :
result.Value().state.CommittedState().free_residual_metrics)
EXPECT_LE(metric, 1e-10);
EXPECT_GT(result.Value().state.CommittedState().iteration_count, 0U);
const auto& frames = result.Value().state.AcceptedFrames();
const double alpha = domain.Value().Dynamic()->alpha;
for (std::size_t frame_index = 1U; frame_index < frames.size(); ++frame_index) {
const auto& frame = frames[frame_index];
EXPECT_EQ(frame.time_integration_formulation,
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
for (std::size_t dof = 0U; dof < frame.acceleration.Size(); ++dof) {
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[dof],
(1.0 + alpha) * frame.acceleration[dof] -
alpha * frames[frame_index - 1U].acceleration[dof]);
}
}
std::filesystem::remove(path);
}
TEST(HhtDynamicProcedure, PreservesBackendCauseAndAddsInitialFailureContext) {
auto definition = MakeDefinition(false);
definition.dynamic_step->dynamic_location = {"dynamic-state.inp", 13U};
auto domain = fesa::Domain::Create(std::move(definition));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
FailingTangent tangent;
fesa::NewtonIterationLogWriter log;
const auto path =
std::filesystem::temp_directory_path() / "fesa-hht-context.log";
ASSERT_TRUE(log.Open(path).IsOk());
const auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
std::filesystem::remove(path);
ASSERT_FALSE(result.HasValue());
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
EXPECT_EQ(result.GetStatus().Diagnostics().front().code,
"injected-tangent-backend");
const auto found = std::find_if(
result.GetStatus().Diagnostics().begin(), result.GetStatus().Diagnostics().end(),
[](const fesa::Diagnostic& diagnostic) {
return diagnostic.code == "dynamic-procedure-context";
});
ASSERT_NE(found, result.GetStatus().Diagnostics().end());
EXPECT_EQ(found->location.file, std::filesystem::path{"dynamic-state.inp"});
EXPECT_EQ(found->location.line, 13U);
EXPECT_EQ(found->entity_identity, "Transient Load");
EXPECT_NE(found->message.find("stage=initial"), std::string::npos);
EXPECT_NE(found->message.find("increment=0"), std::string::npos);
EXPECT_NE(found->message.find("time=0"), std::string::npos);
}
TEST(HhtDynamicProcedure,
UniformTranslationHasNoShellStrainAndIsDeterministicAcrossPartitions) {
const auto definition = MakeRigidTranslationDefinition();
const auto rotated_definition = MakeRotatedRigidTranslationDefinition();
const auto finite_rotation_definition = MakeFiniteRotationDefinition();
const auto rotated_finite_rotation_definition =
MakeRotatedFiniteRotationDefinition();
const fesa::SerialParallelFor serial;
const fesa::TbbParallelFor tbb;
auto first = RunRigidTranslation(definition, serial, "serial-first");
auto partitioned = RunRigidTranslation(definition, tbb, "tbb");
auto second = RunRigidTranslation(definition, serial, "serial-second");
auto rotated = RunRigidTranslation(rotated_definition, serial, "rotated");
auto finite_rotation = RunRigidTranslation(finite_rotation_definition, serial,
"finite-rotation");
auto rotated_finite_rotation = RunRigidTranslation(
rotated_finite_rotation_definition, serial, "rotated-finite-rotation");
ASSERT_TRUE(first.HasValue())
<< (first.HasValue() ? "" : FailureText(first.GetStatus()));
ASSERT_TRUE(partitioned.HasValue())
<< (partitioned.HasValue() ? "" : FailureText(partitioned.GetStatus()));
ASSERT_TRUE(second.HasValue())
<< (second.HasValue() ? "" : FailureText(second.GetStatus()));
ASSERT_TRUE(rotated.HasValue())
<< (rotated.HasValue() ? "" : FailureText(rotated.GetStatus()));
ASSERT_TRUE(finite_rotation.HasValue())
<< (finite_rotation.HasValue() ? "" : FailureText(finite_rotation.GetStatus()));
ASSERT_TRUE(rotated_finite_rotation.HasValue())
<< (rotated_finite_rotation.HasValue()
? ""
: FailureText(rotated_finite_rotation.GetStatus()));
ExpectSameDynamicHistory(first.Value(), partitioned.Value());
ExpectSameDynamicHistory(first.Value(), second.Value());
ExpectCovariantDynamicHistory(first.Value(), rotated.Value(), 0.5,
2.0 * 1000.0 * 0.1);
ExpectCovariantDynamicHistory(finite_rotation.Value(),
rotated_finite_rotation.Value(), 0.02,
1000.0 * 0.1);
const auto& frames = first.Value().execution.state.AcceptedFrames();
ASSERT_EQ(frames.size(), 3U);
constexpr double kReferenceEnergy = 2.0 * 1000.0 * 0.1;
constexpr double kCoordinateScale = 1.0;
for (const auto& frame : frames) {
EXPECT_LE(std::abs(frame.physical_strain_energy) / kReferenceEnergy, 1e-10);
EXPECT_LE(std::abs(frame.drilling_energy) / kReferenceEnergy, 1e-10);
for (std::size_t node = 1U; node < 8U; ++node)
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node] -
frame.generalized_coordinates[0U]) /
kCoordinateScale,
1e-12);
for (std::size_t node = 0U; node < 8U; ++node) {
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + 1U]) /
kCoordinateScale,
1e-12);
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + 2U]) /
kCoordinateScale,
1e-12);
for (std::size_t rotation = 3U; rotation < 6U; ++rotation)
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + rotation]),
1e-12);
}
}
EXPECT_GT(frames.back().generalized_coordinates[0U], 0.0);
double maximum_rotation = 0.0;
for (const auto& frame : finite_rotation.Value().execution.state.AcceptedFrames())
for (std::size_t node = 0U; node < 4U; ++node)
for (std::size_t component = 3U; component < 6U; ++component)
maximum_rotation = (std::max)(
maximum_rotation, std::abs(frame.generalized_coordinates[6U * node + component]));
EXPECT_GT(maximum_rotation, 0.0);
}
} // namespace
namespace {
std::map<std::string, std::string> LogFields(const std::string& line) {
std::map<std::string, std::string> fields;
std::istringstream input(line);
std::string token;
while (std::getline(input, token, '\t')) {
const auto equal = token.find('=');
if (equal != std::string::npos) fields.emplace(token.substr(0, equal), token.substr(equal + 1));
}
return fields;
}
}
namespace {
fesa::ModelDefinition SymmetricExtension() {
auto definition = MakeDefinition(false);
definition.dynamic_step->initial_increment = 0.1;
definition.dynamic_step->time_period = 0.1;
definition.dynamic_step->increment_count = 1U;
definition.node_sets.push_back({"Left", std::nullopt, {0U, 3U}, {"dynamic-state.inp", 30U}});
definition.node_sets.push_back({"Right", std::nullopt, {1U, 2U}, {"dynamic-state.inp", 31U}});
definition.dynamic_step->boundaries = {
{"All", 2, 6, 0.0, {"dynamic-state.inp", 32U}}};
definition.dynamic_step->loads = {
{"Left", 1, -1.0, {"dynamic-state.inp", 33U}},
{"Right", 1, 1.0, {"dynamic-state.inp", 34U}}};
return definition;
}
}
TEST(HhtDynamicProcedure, FieldPolicyActuallyTerminatesSymmetricExtensionBeforeStrict) {
auto definition = SymmetricExtension();
fesa::SerialParallelFor parallel;
auto run = RunRigidTranslation(definition, parallel, "field-policy-extension");
ASSERT_TRUE(run.HasValue()) << (run.HasValue() ? "" : FailureText(run.GetStatus()));
const auto& frame = run.Value().execution.state.CommittedState();
ASSERT_GT(frame.iteration_count, 0U);
ASSERT_LE(frame.iteration_count, 100U);
const auto& text = run.Value().log;
const auto accepted = text.rfind("record=dynamic-newton-iteration");
ASSERT_NE(accepted, std::string::npos);
const auto line = text.substr(accepted);
EXPECT_NE(line.find("\tschema=hht-log-v2\t"), std::string::npos);
EXPECT_NE(line.find("\tpolicy_id=abaqus2025-field-on-fesa-v1\t"), std::string::npos);
EXPECT_NE(line.find("\tstep=Transient Load\t"), std::string::npos);
EXPECT_NE(line.find("\tfield_pass=1\taccepted=1\t"), std::string::npos);
EXPECT_NE(line.find("\tstrict_correction_pass=0"), std::string::npos);
for (const auto value : frame.global_equilibrium) EXPECT_LE(std::abs(value), 1e-8);
auto fields = LogFields(line.substr(0, line.find('\n')));
double rho = 0, increment = 0, flux = 4.0;
for (std::size_t node = 0; node < 4; ++node) {
rho = (std::max)(rho, std::abs(frame.residual[6 * node]));
increment = (std::max)(increment, std::abs(frame.generalized_coordinates[6 * node]));
for (std::size_t c = 0; c < 3; ++c)
flux += std::abs(frame.internal_force[6 * node + c] + frame.inertia_force[6 * node + c]);
}
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_residual_numerator")), rho);
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_increment_norm")), increment);
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_spatial_flux")), flux / 24.0);
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_time_average_flux")), flux / 24.0);
EXPECT_LE(rho, .005 * std::stod(fields.at("force_residual_scale")));
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_residual_threshold")), .005);
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_correction_threshold")), .01);
EXPECT_EQ(fields.at("force_residual_pass"), "1");
EXPECT_EQ(fields.at("force_correction_pass"), "1");
const double c = std::stod(fields.at("force_correction_numerator"));
const auto& estimated = fields.at("force_estimated_correction");
EXPECT_TRUE(c <= .01 * increment ||
(estimated != "not-evaluated" && std::stod(estimated) <= .01 * increment));
EXPECT_EQ(fields.at("force_branch"), "normal");
EXPECT_EQ(fields.at("moment_branch"), "not-evaluated");
// Symmetric monotone extension has q=(-D,+D,+D,-D). This independent
// endpoint oracle includes the selected iteration-zero start, not the lift.
std::istringstream records(text);
std::string row;
double previous_D = 0;
bool first = true;
while (std::getline(records, row)) {
const auto values = LogFields(row);
if (values.at("schema") != "hht-log-v2") continue;
const double D = std::stod(values.at("force_increment_norm"));
if (first) {
EXPECT_EQ(values.at("force_correction_numerator"), "not-evaluated");
EXPECT_EQ(values.at("force_estimated_correction"), "not-evaluated");
EXPECT_EQ(values.at("field_pass"), "0");
} else {
EXPECT_NEAR(std::stod(values.at("force_correction_numerator")),
std::abs(D - previous_D), 1e-16);
}
previous_D = D;
first = false;
}
}
TEST(HhtDynamicProcedure, FieldPassDoesNotBypassPhysicalAcceptanceAndLogsActualCause) {
auto definition = MakeDefinition(false);
definition.materials[0].youngs_modulus = 1e9;
definition.dynamic_step->increment_count = 1;
definition.dynamic_step->time_period = definition.dynamic_step->initial_increment;
definition.node_sets[0].node_indices = {0U, 3U};
definition.node_sets[1].node_indices = {2U};
definition.dynamic_step->boundaries = {{"All", 1, 6, 0, {"dynamic-state.inp", 11U}}};
definition.dynamic_step->loads = {{"Loaded", 3, -40000, {"dynamic-state.inp", 12U}}};
auto domain = fesa::Domain::Create(definition);
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path = std::filesystem::temp_directory_path() / "fesa-field-post-gate.log";
ASSERT_TRUE(log.Open(path).IsOk());
auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent).Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
std::ifstream input(path, std::ios::binary);
const std::string text{std::istreambuf_iterator<char>{input}, {}};
input.close();
std::filesystem::remove(path);
ASSERT_FALSE(result.HasValue());
const auto row = text.substr(text.rfind("record="));
const auto fields = LogFields(row.substr(0, row.find('\n')));
EXPECT_EQ(fields.at("field_pass"), "1");
EXPECT_EQ(fields.at("accepted"), "0");
EXPECT_EQ(fields.at("outcome"), "failed");
const auto cause = std::find_if(result.GetStatus().Diagnostics().begin(),
result.GetStatus().Diagnostics().end(), [](const auto& d) {
return d.code != "dynamic-procedure-context";
});
ASSERT_NE(cause, result.GetStatus().Diagnostics().end());
EXPECT_EQ(fields.at("failure_code"), cause->code) << FailureText(result.GetStatus());
EXPECT_EQ(text.find("\taccepted=1"), std::string::npos);
}
TEST(HhtDynamicProcedure, AcceptedFluxHistoryCountsEachIncrementExactlyOnce) {
auto definition = SymmetricExtension();
definition.dynamic_step->time_period = .2;
definition.dynamic_step->increment_count = 2;
fesa::SerialParallelFor parallel;
auto run = RunRigidTranslation(definition, parallel, "field-history");
ASSERT_TRUE(run.HasValue()) << (run.HasValue() ? "" : FailureText(run.GetStatus()));
const auto& frames = run.Value().execution.state.AcceptedFrames();
ASSERT_EQ(frames.size(), 3U);
double sum = 0, maximum_sum = 0;
std::size_t n = 0;
std::istringstream input(run.Value().log);
std::string row;
while (std::getline(input, row)) {
const auto fields = LogFields(row);
if (fields.at("schema") != "hht-log-v2" || fields.at("accepted") != "1") continue;
++n;
const auto& frame = frames[n];
std::vector<double> flux;
for (std::size_t node = 0; node < 4; ++node)
for (std::size_t c = 0; c < 3; ++c)
flux.push_back(std::abs(frame.internal_force[6 * node + c] + frame.inertia_force[6 * node + c]));
for (int node = 0; node < 4; ++node) {
flux.push_back(1); flux.push_back(0); flux.push_back(0);
}
const double m = *std::max_element(flux.begin(), flux.end());
const double H = n == 1 ? 0 : maximum_sum / static_cast<double>(n - 1);
double selected_sum = 0;
std::size_t count = 0;
for (double x : flux)
if (m < .1 * H || x >= 1e-5 * H) { selected_sum += x; ++count; }
ASSERT_GT(count, 0U);
const double spatial = selected_sum / static_cast<double>(count);
sum += spatial; maximum_sum += m;
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_spatial_flux")), spatial);
EXPECT_DOUBLE_EQ(std::stod(fields.at("force_time_average_flux")), sum / static_cast<double>(n));
EXPECT_EQ(std::stoul(fields.at("iteration")), frame.iteration_count);
}
EXPECT_EQ(n, 2U);
}
TEST(HhtDynamicProcedure, CandidateRigidTranslationMatchesIndependentPhysicalOracle) {
// Two disconnected unit squares: rho=2, thickness=.1, area=1 each.
// Each nodal lump has mass .05 and receives F_x=1, hence a_x=20.
// This exact rigid motion verifies endpoint semantics, not temporal order.
constexpr double kNumericalTolerance = 1e-3;
constexpr double kAcceleration = 20.0;
constexpr double kTotalMass = 0.4;
constexpr double kTotalForce = 8.0;
constexpr double kLength = 1.0;
constexpr double kTime = 0.5;
constexpr double kEnergy = 20.0;
fesa::SerialParallelFor parallel;
for (const bool constrain_transverse : {false, true}) {
SCOPED_TRACE(constrain_transverse);
auto definition = MakeRigidTranslationDefinition();
if (constrain_transverse) {
definition.dynamic_step->boundaries = {
{"All-A", 2, 6, 0.0, {"dynamic-state.inp", 30U}},
{"All-B", 2, 6, 0.0, {"dynamic-state.inp", 31U}}};
}
auto run = RunRigidTranslation(definition, parallel,
constrain_transverse ? "analytic-bc" : "analytic-free");
ASSERT_TRUE(run.HasValue()) << (run.HasValue() ? "" : FailureText(run.GetStatus()));
const auto& frames = run.Value().execution.state.AcceptedFrames();
ASSERT_EQ(frames.size(), 3U);
for (std::size_t index = 0U; index < frames.size(); ++index) {
const auto& frame = frames[index];
EXPECT_DOUBLE_EQ(frame.time, static_cast<double>(index) * 0.25);
const double q = 0.5 * kAcceleration * frame.time * frame.time;
const double v = kAcceleration * frame.time;
for (std::size_t node = 0U; node < 8U; ++node) {
const auto dof = 6U * node;
EXPECT_LE(std::abs(frame.generalized_coordinates[dof] - q) / kLength, kNumericalTolerance);
EXPECT_LE(std::abs(frame.velocity[dof] - v) * kTime / kLength, kNumericalTolerance);
EXPECT_LE(std::abs(frame.acceleration[dof] - kAcceleration) *
kTime * kTime / kLength, kNumericalTolerance);
EXPECT_LE(std::abs(frame.algorithmic_acceleration[dof] - kAcceleration) *
kTime * kTime / kLength, kNumericalTolerance);
EXPECT_DOUBLE_EQ(frame.external_force[dof], 1.0);
EXPECT_LE(std::abs(frame.inertia_force[dof] - 1.0), kNumericalTolerance);
for (std::size_t component = 1U; component < 6U; ++component) {
const double scale = component < 3U ? kLength : 1.0;
EXPECT_LE(std::abs(frame.generalized_coordinates[dof + component]) / scale, kNumericalTolerance);
EXPECT_LE(std::abs(frame.velocity[dof + component]) * kTime / scale, kNumericalTolerance);
EXPECT_LE(std::abs(frame.acceleration[dof + component]) * kTime * kTime / scale, kNumericalTolerance);
EXPECT_LE(std::abs(frame.algorithmic_acceleration[dof + component]) * kTime * kTime /
scale, kNumericalTolerance);
if (constrain_transverse) {
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[dof + component], 0.0);
EXPECT_DOUBLE_EQ(frame.velocity[dof + component], 0.0);
EXPECT_DOUBLE_EQ(frame.acceleration[dof + component], 0.0);
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[dof + component], 0.0);
}
}
}
EXPECT_LE(std::abs(frame.kinetic_energy - 0.5 * kTotalMass * v * v) /
kEnergy, kNumericalTolerance);
EXPECT_LE(std::abs(frame.external_work - kTotalForce * q) / kEnergy, kNumericalTolerance);
EXPECT_LE(std::abs(frame.physical_strain_energy) / kEnergy, kNumericalTolerance);
EXPECT_LE(std::abs(frame.drilling_energy) / kEnergy, kNumericalTolerance);
EXPECT_LE(std::abs(frame.energy_defect) / kEnergy, kNumericalTolerance);
for (const auto value : frame.global_equilibrium)
EXPECT_LE(std::abs(value), 1e-8);
}
std::size_t accepted = 0U;
std::istringstream input(run.Value().log);
std::string row;
while (std::getline(input, row)) {
const auto fields = LogFields(row);
if (fields.at("schema") != "hht-log-v2") continue;
EXPECT_EQ(fields.at("policy_id"), "abaqus2025-field-on-fesa-v1");
if (fields.at("accepted") == "1") {
EXPECT_EQ(fields.at("field_pass"), "1");
++accepted;
}
}
EXPECT_EQ(accepted, 2U);
}
}
TEST(HhtDynamicProcedure, LoadFluxPreservesSourceCancellationAndExplicitZeroEntries) {
auto definition = MakeDefinition(true);
definition.dynamic_step->loads = {
{"Loaded", 1, 3, {"dynamic-state.inp", 30U}},
{"Loaded", 1, -3, {"dynamic-state.inp", 31U}},
{"Loaded", 2, 0, {"dynamic-state.inp", 32U}}};
auto domain = fesa::Domain::Create(definition);
ASSERT_TRUE(domain.HasValue());
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
auto dofs = fesa::DofManager::Create(model.Value());
ASSERT_TRUE(dofs.HasValue());
std::array<std::vector<fesa::hht_field_convergence::FluxOccurrence>, 2> flux;
const auto loads = fesa::hht_internal::AssembleLoads(domain.Value(), dofs.Value(), &flux);
ASSERT_TRUE(loads.HasValue());
EXPECT_DOUBLE_EQ(loads.Value()[0][0], 0);
EXPECT_DOUBLE_EQ(loads.Value()[1][0], 6);
ASSERT_EQ(flux[0].size(), 9U);
ASSERT_EQ(flux[1].size(), 9U);
const std::array<double, 9> expected{3, 0, 0, -3, 0, 0, 0, 0, 0};
for (std::size_t i = 0; i < expected.size(); ++i) {
EXPECT_DOUBLE_EQ(flux[0][i].internal, expected[i]);
EXPECT_DOUBLE_EQ(flux[0][i].inertia, 0);
EXPECT_DOUBLE_EQ(flux[1][i].internal, 0);
}
}
namespace {
// All-fixed fixture has no private coordinates. This adapter exercises the
// real production acceptance/output transaction without a test solver path.
class EmptyCoordinateIntegrator final : public fesa::hht_reduced_internal::ShellHhtIntegrator {
public:
fesa::hht_reduced_internal::ReducedShellPredictor Predict(
const fesa::hht_reduced_internal::ReducedHistory& history, double,
const fesa::HhtParameters&) const override {
EXPECT_EQ(history.p.Size(), 0U);
return {};
}
void EvaluateRates(const fesa::hht_reduced_internal::ReducedShellRateRequest&,
fesa::Vector& v, fesa::Vector& a) const override {
EXPECT_EQ(v.Size(), 0U); EXPECT_EQ(a.Size(), 0U);
}
fesa::Result<fesa::hht_reduced_internal::ReconstructedRates> ReconstructPhysicalRates(
const fesa::hht_reduced_internal::FixedChart& chart,
const fesa::hht_reduced_internal::EmbeddingRoot& root,
const fesa::Vector& v, const fesa::Vector& a,
fesa::TangentSolver& solver) const override {
return fesa::hht_reduced_internal::ReconstructPhysicalRates(chart, root, v, a, solver);
}
};
class CloseAtCandidate final : public fesa::EquilibriumAlgorithm {
public:
explicit CloseAtCandidate(fesa::NewtonIterationLogWriter& log) : log_(log) {}
fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last,
const Evaluation& evaluate) override {
EXPECT_EQ(first, 0U); EXPECT_EQ(last, 100U);
auto value = evaluate(first);
if (!value.HasValue()) return value.GetStatus();
EXPECT_TRUE(value.Value());
return log_.Close();
}
private:
fesa::NewtonIterationLogWriter& log_;
};
}
TEST(HhtDynamicProcedure, FailedAcceptanceOutputPublishesNoCandidateAndFreshRetryIsUnchanged) {
auto domain = fesa::Domain::Create(MakeDefinition());
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path = std::filesystem::temp_directory_path() / "fesa-field-output-fail.log";
ASSERT_TRUE(log.Open(path).IsOk());
EmptyCoordinateIntegrator integrator;
CloseAtCandidate algorithm(log);
const auto result = fesa::ExecuteReducedHht(domain.Value(), log, parallel, mass, tangent, integrator, algorithm);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kOutput);
std::ifstream input(path, std::ios::binary);
const std::string text{std::istreambuf_iterator<char>{input}, {}};
input.close(); std::filesystem::remove(path);
EXPECT_EQ(text.find("accepted=1"), std::string::npos);
auto first = RunRigidTranslation(MakeDefinition(), parallel, "field-retry");
auto second = RunRigidTranslation(MakeDefinition(), parallel, "field-retry-repeat");
ASSERT_TRUE(first.HasValue()); ASSERT_TRUE(second.HasValue());
ExpectSameDynamicHistory(first.Value(), second.Value());
}
namespace {
class FailNextSelectedTrial final : public fesa::TangentSolver {
public:
fesa::Status FactorizeFresh(const fesa::SparseMatrix& matrix) override {
if (outer_solved_) return fesa::Status::Failure(fesa::FailureCategory::kSolver,
{{fesa::Severity::kError, "injected-next-trial", {}, "DYNAMIC", {}, "Fail before the next selected field evaluation."}});
large_ = matrix.Rows() > 4;
return solver_.FactorizeFresh(matrix);
}
fesa::Status SolveCorrection(const fesa::Vector& rhs, fesa::Vector& solution) const override {
auto status = solver_.SolveCorrection(rhs, solution);
if (status.IsOk() && large_) outer_solved_ = true;
return status;
}
private:
fesa::MklPardisoTangentSolver solver_;
bool large_ = false;
mutable bool outer_solved_ = false;
};
}
TEST(HhtDynamicProcedure, FailureBeforeNextEvaluationDoesNotReusePreviousFieldEvidence) {
auto domain = fesa::Domain::Create(MakeFiniteRotationDefinition());
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
FailNextSelectedTrial tangent;
fesa::NewtonIterationLogWriter log;
const auto path = std::filesystem::temp_directory_path() / "fesa-field-next-fail.log";
ASSERT_TRUE(log.Open(path).IsOk());
const auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent).Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
std::ifstream input(path, std::ios::binary);
const std::string text{std::istreambuf_iterator<char>{input}, {}};
input.close(); std::filesystem::remove(path);
ASSERT_FALSE(result.HasValue());
const auto row = text.substr(text.rfind("record="));
const auto fields = LogFields(row.substr(0, row.find('\n')));
EXPECT_EQ(fields.at("failure_code"), "injected-next-trial") << FailureText(result.GetStatus());
EXPECT_EQ(fields.at("iteration"), "1");
EXPECT_EQ(fields.at("force_residual_numerator"), "not-evaluated");
EXPECT_EQ(fields.at("moment_residual_scale"), "not-evaluated");
EXPECT_EQ(fields.at("field_pass"), "not-evaluated");
EXPECT_EQ(fields.at("accepted"), "0");
}
namespace {
class EvaluateEndpoint100 final : public fesa::EquilibriumAlgorithm {
public:
fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last,
const Evaluation& evaluate) override {
EXPECT_EQ(first, 0U); EXPECT_EQ(last, 100U);
auto value = evaluate(last);
if (!value.HasValue()) return value.GetStatus();
EXPECT_TRUE(value.Value());
return fesa::Status::Ok();
}
};
}
TEST(HhtDynamicProcedure, FieldAcceptanceEvaluatesProductionEndpoint100BeforeCorrectionBudget) {
auto domain = fesa::Domain::Create(MakeDefinition());
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path = std::filesystem::temp_directory_path() / "fesa-field-endpoint100.log";
ASSERT_TRUE(log.Open(path).IsOk());
EmptyCoordinateIntegrator integrator;
EvaluateEndpoint100 algorithm;
// Tests the real endpoint callback at the inclusive boundary. No physical
// trajectory is forced through 100 corrections; all fixed coordinates stay zero.
const auto result = fesa::ExecuteReducedHht(domain.Value(), log, parallel, mass, tangent, integrator, algorithm);
ASSERT_TRUE(log.Close().IsOk());
std::ifstream input(path, std::ios::binary);
const std::string text{std::istreambuf_iterator<char>{input}, {}};
input.close(); std::filesystem::remove(path);
ASSERT_TRUE(result.HasValue()) << (result.HasValue() ? "" : FailureText(result.GetStatus()));
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
EXPECT_EQ(result.Value().state.AcceptedFrames()[1].iteration_count, 100U);
EXPECT_EQ(result.Value().state.AcceptedFrames()[2].iteration_count, 100U);
const auto row = text.substr(text.rfind("record="));
const auto fields = LogFields(row.substr(0, row.find('\n')));
EXPECT_EQ(fields.at("iteration"), "100");
EXPECT_EQ(fields.at("field_pass"), "1");
EXPECT_EQ(fields.at("accepted"), "1");
}