Connect HHT field convergence to production acceptance and v2 logs
This commit is contained in:
@@ -8,9 +8,16 @@
|
||||
#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"
|
||||
@@ -569,3 +576,355 @@ TEST(HhtDynamicProcedure,
|
||||
}
|
||||
|
||||
} // 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, 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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user