Connect HHT field convergence to production acceptance and v2 logs
This commit is contained in:
@@ -669,4 +669,39 @@ TEST(HhtDynamicAlgebra, RejectsAdmissibleNullRankChangeBeforeAcceptingFrame) {
|
||||
EXPECT_EQ(frame.hidden_constraint[0], 23.0);
|
||||
}
|
||||
|
||||
TEST(HhtEndpointFields, MeasuresActualFullChartAcrossNonlinearRootAndPartialBoundary) {
|
||||
const auto fixture = MakeFixture(1U, true);
|
||||
auto trial = EmptyTrial(fixture), old = EmptyTrial(fixture);
|
||||
trial.residual = fesa::Vector{24U};
|
||||
fesa::Vector previous{24U};
|
||||
// Independent nonlinear graph q_phiZ=p^2: p .1 -> .2. The
|
||||
// previous tangent lift is .02, while the actual endpoint change is .03.
|
||||
previous[0] = .1; previous[5] = .01;
|
||||
trial.generalized_coordinates[0] = .2;
|
||||
trial.generalized_coordinates[5] = .04;
|
||||
trial.residual[0] = 3; trial.residual[5] = -5;
|
||||
// Large constrained values and spatial support moments are not free
|
||||
// generalized endpoint evidence and cannot change either scalar maximum.
|
||||
trial.generalized_coordinates[3] = 1000;
|
||||
trial.residual[3] = 2000;
|
||||
trial.reaction = fesa::Vector{24U}; trial.reaction[5] = 3000;
|
||||
const auto result = fesa::hht_internal::MeasureEndpointFields(fixture.dofs, trial, old, previous);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
EXPECT_TRUE(result.Value()[0].active); EXPECT_TRUE(result.Value()[1].active);
|
||||
EXPECT_DOUBLE_EQ(result.Value()[0].residual, 3);
|
||||
EXPECT_DOUBLE_EQ(result.Value()[1].residual, 5);
|
||||
EXPECT_DOUBLE_EQ(result.Value()[0].increment, .2);
|
||||
EXPECT_DOUBLE_EQ(result.Value()[1].increment, .04);
|
||||
EXPECT_DOUBLE_EQ(*result.Value()[0].correction, .2 - .1);
|
||||
EXPECT_DOUBLE_EQ(*result.Value()[1].correction, .04 - .01);
|
||||
EXPECT_NE(*result.Value()[1].correction, 2 * .1 * (.2 - .1));
|
||||
const auto initial = fesa::hht_internal::MeasureEndpointFields(fixture.dofs, trial, old, std::nullopt);
|
||||
ASSERT_TRUE(initial.HasValue());
|
||||
EXPECT_FALSE(initial.Value()[0].correction.has_value());
|
||||
EXPECT_FALSE(initial.Value()[1].correction.has_value());
|
||||
previous[0] = -std::numeric_limits<double>::max();
|
||||
trial.generalized_coordinates[0] = std::numeric_limits<double>::max();
|
||||
EXPECT_FALSE(fesa::hht_internal::MeasureEndpointFields(fixture.dofs, trial, old, previous).HasValue());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -562,4 +563,149 @@ TEST(DynamicNewtonIterationLog,
|
||||
ExpectLogOutputFailure(unopened.Write(CompleteDynamicRecord()));
|
||||
}
|
||||
|
||||
TEST(DynamicFieldIterationLog, KeepsOrderedUnitsMissingEvidenceAndEscapedSourceName) {
|
||||
TempDirectory directory{"field-v2"};
|
||||
const auto path = directory.Path() / "log.txt";
|
||||
auto record = CompleteDynamicRecord();
|
||||
record.policy_id = "abaqus2025-field-on-fesa-v1";
|
||||
record.step_name = "\xED\x95\x9C\xEA\xB8\x80=Step\\tail\tline\nend\r";
|
||||
record.stage = "evaluation";
|
||||
record.outcome = "candidate";
|
||||
record.fields[0].residual_numerator = 1.2345678901234567;
|
||||
record.fields[0].increment_norm = 0;
|
||||
fesa::NewtonIterationLogWriter writer;
|
||||
ASSERT_TRUE(writer.Open(path).IsOk());
|
||||
ASSERT_TRUE(writer.Write(record).IsOk());
|
||||
ASSERT_TRUE(writer.Close().IsOk());
|
||||
const auto text = ReadLogBytes(path);
|
||||
EXPECT_EQ(std::count(text.begin(), text.end(), '\n'), 1);
|
||||
EXPECT_EQ(text.find('\r'), std::string::npos);
|
||||
const auto fields = SplitTabs(text.substr(0, text.size() - 1));
|
||||
const std::vector<std::string> base{"record", "schema", "policy_id", "step", "increment", "time", "iteration", "stage", "outcome", "failure_code", "field_pass", "accepted"};
|
||||
for (std::size_t i = 0; i < base.size(); ++i)
|
||||
EXPECT_EQ(fields[i].substr(0, fields[i].find('=')), base[i]);
|
||||
const std::vector<std::string> suffixes{
|
||||
"residual_numerator", "residual_scale", "spatial_flux", "time_average_flux",
|
||||
"correction_numerator", "increment_norm", "estimated_correction",
|
||||
"residual_threshold", "correction_threshold", "residual_unit", "correction_unit",
|
||||
"branch", "residual_pass", "correction_pass", "field_pass"};
|
||||
for (std::size_t c = 0; c < 2; ++c)
|
||||
for (std::size_t i = 0; i < suffixes.size(); ++i)
|
||||
EXPECT_EQ(fields[base.size() + c * suffixes.size() + i].substr(
|
||||
0, fields[base.size() + c * suffixes.size() + i].find('=')),
|
||||
(c == 0 ? "force_" : "moment_") + suffixes[i]);
|
||||
EXPECT_EQ(FieldValue(fields, "step"), "step=\xED\x95\x9C\xEA\xB8\x80=Step\\\\tail\\tline\\nend\\r");
|
||||
EXPECT_EQ(FieldValue(fields, "force_correction_numerator"), "force_correction_numerator=not-evaluated");
|
||||
EXPECT_EQ(FieldValue(fields, "force_increment_norm"), "force_increment_norm=0");
|
||||
EXPECT_EQ(FieldValue(fields, "strict_residual_pass"), "strict_residual_pass=not-evaluated");
|
||||
EXPECT_EQ(FieldValue(fields, "moment_correction_unit"), "moment_correction_unit=rad");
|
||||
EXPECT_DOUBLE_EQ(std::stod(ValueAfterEquals(FieldValue(fields, "force_residual_numerator"))),
|
||||
*record.fields[0].residual_numerator);
|
||||
}
|
||||
|
||||
TEST(DynamicFieldIterationLog, InvalidFieldEvidenceFailsWithoutWritingPartialRecord) {
|
||||
TempDirectory directory{"field-v2-invalid"};
|
||||
const auto path = directory.Path() / "log.txt";
|
||||
for (int kind = 0; kind < 4; ++kind) {
|
||||
SCOPED_TRACE(kind);
|
||||
fesa::NewtonIterationLogWriter writer;
|
||||
ASSERT_TRUE(writer.Open(path).IsOk());
|
||||
auto record = CompleteDynamicRecord();
|
||||
record.policy_id = "abaqus2025-field-on-fesa-v1";
|
||||
ASSERT_TRUE(writer.Write(record).IsOk());
|
||||
ASSERT_TRUE(writer.Flush().IsOk());
|
||||
const auto prefix = ReadLogBytes(path);
|
||||
if (kind == 0) record.fields[0].branch = "unknown";
|
||||
if (kind == 1) record.fields[0].estimated_correction = std::numeric_limits<double>::infinity();
|
||||
if (kind == 2) record.fields[1].residual_scale = -1;
|
||||
if (kind == 3) record.policy_id = "unknown";
|
||||
ExpectLogOutputFailure(writer.Write(record));
|
||||
ExpectLogOutputFailure(writer.Flush());
|
||||
EXPECT_EQ(ReadLogBytes(path), prefix);
|
||||
}
|
||||
}
|
||||
|
||||
// Independent strict consumer: production has a writer, no public log reader.
|
||||
bool ReadFieldV2(const std::string& bytes) {
|
||||
if (bytes.empty() || bytes.back() != '\n' || bytes.find('\r') != std::string::npos ||
|
||||
std::count(bytes.begin(), bytes.end(), '\n') != 1) return false;
|
||||
std::map<std::string, std::string> values;
|
||||
for (const auto& field : SplitTabs(bytes.substr(0, bytes.size() - 1))) {
|
||||
const auto equal = field.find('=');
|
||||
if (equal == std::string::npos || !values.emplace(field.substr(0, equal), field.substr(equal + 1)).second)
|
||||
return false;
|
||||
}
|
||||
for (const auto* key : {"record", "schema", "policy_id", "step", "increment", "time", "iteration", "stage", "outcome", "failure_code", "field_pass", "accepted"})
|
||||
if (!values.count(key)) return false;
|
||||
if (values["record"] != "dynamic-newton-iteration" || values["schema"] != "hht-log-v2" ||
|
||||
values["policy_id"] != "abaqus2025-field-on-fesa-v1") return false;
|
||||
const auto numeric = [](const std::string& text, bool missing) {
|
||||
if (missing && text == "not-evaluated") return true;
|
||||
double value = 0;
|
||||
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() && std::isfinite(value) && value >= 0;
|
||||
};
|
||||
const auto boolean = [](const std::string& text, bool missing) {
|
||||
return text == "0" || text == "1" || (missing && text == "not-evaluated");
|
||||
};
|
||||
for (const auto* key : {"increment", "iteration"}) {
|
||||
std::uint64_t value = 0;
|
||||
const auto& text = values[key];
|
||||
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size()) return false;
|
||||
}
|
||||
if (!numeric(values["time"], false) || !boolean(values["field_pass"], true) || !boolean(values["accepted"], false)) return false;
|
||||
for (const auto* prefix : {"force_", "moment_"}) {
|
||||
for (const auto* suffix : {"residual_numerator", "residual_scale", "spatial_flux", "time_average_flux", "correction_numerator", "increment_norm", "estimated_correction", "residual_threshold", "correction_threshold"}) {
|
||||
const auto found = values.find(std::string(prefix) + suffix);
|
||||
if (found == values.end() || !numeric(found->second, true)) return false;
|
||||
}
|
||||
for (const auto* suffix : {"residual_pass", "correction_pass", "field_pass"}) {
|
||||
const auto found = values.find(std::string(prefix) + suffix);
|
||||
if (found == values.end() || !boolean(found->second, true)) return false;
|
||||
}
|
||||
const auto branch = values.find(std::string(prefix) + "branch");
|
||||
if (branch == values.end() || (branch->second != "normal" && branch->second != "zero-flux" &&
|
||||
branch->second != "linear" && branch->second != "nonquadratic" && branch->second != "not-evaluated")) return false;
|
||||
if (values[std::string(prefix) + "residual_unit"] != (std::string(prefix) == "force_" ? "force" : "moment") ||
|
||||
values[std::string(prefix) + "correction_unit"] != (std::string(prefix) == "force_" ? "length" : "rad")) return false;
|
||||
}
|
||||
for (const auto& entry : values)
|
||||
for (std::size_t i = 0; i < entry.second.size(); ++i)
|
||||
if (entry.second[i] == '\\') {
|
||||
if (++i == entry.second.size() || (entry.second[i] != '\\' && entry.second[i] != 't' && entry.second[i] != 'n' && entry.second[i] != 'r')) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(DynamicFieldIterationLog, IndependentReaderRejectsMalformedVersionedRecords) {
|
||||
TempDirectory directory{"field-reader"};
|
||||
const auto path = directory.Path() / "log.txt";
|
||||
fesa::NewtonIterationLogWriter writer;
|
||||
ASSERT_TRUE(writer.Open(path).IsOk());
|
||||
auto record = CompleteDynamicRecord();
|
||||
record.policy_id = "abaqus2025-field-on-fesa-v1";
|
||||
ASSERT_TRUE(writer.Write(record).IsOk());
|
||||
ASSERT_TRUE(writer.Close().IsOk());
|
||||
const auto valid = ReadLogBytes(path);
|
||||
ASSERT_TRUE(ReadFieldV2(valid));
|
||||
const auto replaced = [&](const std::string& from, const std::string& to) {
|
||||
auto text = valid;
|
||||
const auto start = text.find(from);
|
||||
EXPECT_NE(start, std::string::npos);
|
||||
text.replace(start, from.size(), to);
|
||||
return text;
|
||||
};
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("schema=hht-log-v2", "schema=hht-log-v1")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tincrement=2", "\tincrement=-1")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\ttime=0.25", "\ttime=nan")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\ttime=0.25", "\ttime=inf")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("force_residual_numerator=not-evaluated", "force_residual_numerator=1junk")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("force_branch=not-evaluated", "force_branch=unknown")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tfield_pass=not-evaluated", "\tfield_pass=true")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tstep=", "\tstep=bad\\x")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\taccepted=0", "\taccepted=0\taccepted=0")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tforce_increment_norm=not-evaluated", "")));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user