feat(nonlinear-static-newton-raphson): step 7 - newton-iteration-log-writer

This commit is contained in:
KOKO\Mimi
2026-08-24 14:34:19 +09:00
parent b0d53fb8f8
commit 902bc79e16
5 changed files with 746 additions and 0 deletions
@@ -0,0 +1,327 @@
#include "fesa/analysis/newton_iteration_log.h"
#include <algorithm>
#include <charconv>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#include <gtest/gtest.h>
namespace {
class TempDirectory {
public:
explicit TempDirectory(const std::string& label) {
const auto tick =
std::chrono::steady_clock::now().time_since_epoch().count();
path_ = std::filesystem::temp_directory_path() /
("fesa-newton-log-" + label + "-" + std::to_string(tick));
std::error_code error;
if (!std::filesystem::create_directory(path_, error) || error) {
throw std::runtime_error{"Unable to create Newton log test fixture."};
}
}
TempDirectory(const TempDirectory&) = delete;
TempDirectory& operator=(const TempDirectory&) = delete;
~TempDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
void ExpectLogOutputFailure(const fesa::Status& status) {
ASSERT_FALSE(status.IsOk());
ASSERT_EQ(status.Category(), fesa::FailureCategory::kOutput);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics().front().code, "log-output-failure");
}
std::vector<std::string> SplitTabs(const std::string& line) {
std::vector<std::string> fields;
std::size_t start = 0U;
while (start <= line.size()) {
const std::size_t end = line.find('\t', start);
if (end == std::string::npos) {
fields.push_back(line.substr(start));
return fields;
}
fields.push_back(line.substr(start, end - start));
start = end + 1U;
}
return fields;
}
const std::string& FieldValue(const std::vector<std::string>& fields,
const std::string_view label) {
const std::string prefix = std::string{label} + "=";
for (const std::string& field : fields) {
if (field.compare(0U, prefix.size(), prefix) == 0) {
return field;
}
}
throw std::runtime_error{"Newton log field is missing."};
}
std::string ValueAfterEquals(const std::string& field) {
return field.substr(field.find('=') + 1U);
}
fesa::NewtonChannelDecision MakeDecision(const std::string& identity) {
return {identity,
0.0012345678901234567,
1.2345678901234567,
fesa::NewtonConvergenceBranch::kRelative,
fesa::kNewtonRelativeThreshold,
fesa::NewtonChannelUnit::kDimensionless,
true};
}
fesa::NewtonIterationLogRecord CompleteRecord() {
return {"Step-1",
3U,
0.12345678901234567,
2U,
fesa::NewtonIterationStage::kConvergenceEvaluated,
{false, fesa::NewtonIterationStage::kFailed, "", ""},
"accept",
"accepted",
"none",
MakeDecision("residual_force"),
MakeDecision("residual_moment"),
MakeDecision("correction_translation"),
MakeDecision("correction_rotation"),
true};
}
std::vector<std::string> WriteAndRead(
const std::filesystem::path& path,
const fesa::NewtonIterationLogRecord& record) {
fesa::NewtonIterationLogWriter writer;
if (!writer.Open(path).IsOk() || !writer.Write(record).IsOk() ||
!writer.Flush().IsOk() || !writer.Close().IsOk()) {
throw std::runtime_error{"Unable to write Newton log test record."};
}
std::ifstream input{path, std::ios::binary};
if (!input.is_open()) {
throw std::runtime_error{"Unable to read Newton log test record."};
}
const std::string contents{std::istreambuf_iterator<char>{input},
std::istreambuf_iterator<char>{}};
if (contents.empty()) {
throw std::runtime_error{"Newton log test record is empty."};
}
EXPECT_EQ(contents.back(), '\n');
EXPECT_EQ(std::count(contents.begin(), contents.end(), '\n'), 1);
EXPECT_EQ(contents.find('\r'), std::string::npos);
EXPECT_FALSE(contents.size() >= 3U &&
static_cast<unsigned char>(contents[0U]) == 0xEFU &&
static_cast<unsigned char>(contents[1U]) == 0xBBU &&
static_cast<unsigned char>(contents[2U]) == 0xBFU);
return SplitTabs(contents.substr(0U, contents.size() - 1U));
}
// NSNR-LOG-001, NSNR-LOG-002, and NSNR-LOG-003
TEST(NewtonIterationLog, WritesOneLfTerminated39FieldRecordInApprovedOrder) {
TempDirectory directory{"complete"};
const auto fields =
WriteAndRead(directory.Path() / "model_log.txt", CompleteRecord());
const std::vector<std::string> labels{
"record", "schema", "increment", "target_factor", "iteration",
"last_completed_stage", "failing_stage", "residual_force_channel",
"residual_force_numerator_norm", "residual_force_denominator_scale",
"residual_force_branch", "residual_force_threshold",
"residual_force_threshold_unit", "residual_force_pass",
"residual_moment_channel", "residual_moment_numerator_norm",
"residual_moment_denominator_scale", "residual_moment_branch",
"residual_moment_threshold", "residual_moment_threshold_unit",
"residual_moment_pass", "correction_translation_channel",
"correction_translation_numerator_norm",
"correction_translation_denominator_scale",
"correction_translation_branch", "correction_translation_threshold",
"correction_translation_threshold_unit", "correction_translation_pass",
"correction_rotation_channel", "correction_rotation_numerator_norm",
"correction_rotation_denominator_scale", "correction_rotation_branch",
"correction_rotation_threshold", "correction_rotation_threshold_unit",
"correction_rotation_pass", "combined_pass", "transition", "outcome",
"diagnostic_code"};
ASSERT_EQ(fields.size(), labels.size());
for (std::size_t index = 0U; index < labels.size(); ++index) {
EXPECT_EQ(fields[index].substr(0U, fields[index].find('=')), labels[index]);
}
EXPECT_EQ(FieldValue(fields, "record"), "record=newton-iteration");
EXPECT_EQ(FieldValue(fields, "schema"), "schema=nsnr-log-v1");
}
// NSNR-LOG-004
TEST(NewtonIterationLog, FormatsFiniteNumbersForLocaleIndependentRoundTrip) {
TempDirectory directory{"numbers"};
const auto fields =
WriteAndRead(directory.Path() / "model_log.txt", CompleteRecord());
const std::string value = ValueAfterEquals(FieldValue(fields, "target_factor"));
double parsed = 0.0;
const auto result = std::from_chars(value.data(), value.data() + value.size(),
parsed, std::chars_format::general);
EXPECT_EQ(result.ec, std::errc{});
EXPECT_EQ(result.ptr, value.data() + value.size());
EXPECT_DOUBLE_EQ(parsed, 0.12345678901234567);
EXPECT_NE(value.find('.'), std::string::npos);
EXPECT_EQ(value.find(','), std::string::npos);
}
// NSNR-LOG-005 and NSNR-LOG-007
TEST(NewtonIterationLog, SerializesChannelDecisionsAndExplicitOutcomeFields) {
TempDirectory directory{"channels"};
const auto fields =
WriteAndRead(directory.Path() / "model_log.txt", CompleteRecord());
for (const std::string_view prefix :
{"residual_force", "residual_moment", "correction_translation",
"correction_rotation"}) {
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_channel"),
std::string{prefix} + "_channel=" + std::string{prefix});
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_numerator_norm"),
std::string{prefix} +
"_numerator_norm=0.0012345678901234567");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_denominator_scale"),
std::string{prefix} +
"_denominator_scale=1.2345678901234567");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_branch"),
std::string{prefix} + "_branch=relative");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_threshold"),
std::string{prefix} + "_threshold=0.01");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_threshold_unit"),
std::string{prefix} + "_threshold_unit=1");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_pass"),
std::string{prefix} + "_pass=1");
}
EXPECT_EQ(FieldValue(fields, "last_completed_stage"),
"last_completed_stage=convergence-gates");
EXPECT_EQ(FieldValue(fields, "failing_stage"), "failing_stage=none");
EXPECT_EQ(FieldValue(fields, "combined_pass"), "combined_pass=1");
EXPECT_EQ(FieldValue(fields, "transition"), "transition=accept");
EXPECT_EQ(FieldValue(fields, "outcome"), "outcome=accepted");
EXPECT_EQ(FieldValue(fields, "diagnostic_code"), "diagnostic_code=none");
}
// NSNR-LOG-006
TEST(NewtonIterationLog, SerializesEmptyChannelsAsTypedZeroScaleDecisions) {
TempDirectory directory{"zero-scale"};
auto record = CompleteRecord();
const std::vector<fesa::NewtonChannelUnit> units{
fesa::NewtonChannelUnit::kForce, fesa::NewtonChannelUnit::kForceLength,
fesa::NewtonChannelUnit::kLength, fesa::NewtonChannelUnit::kDimensionless};
fesa::NewtonChannelDecision* decisions[]{
&record.force_residual, &record.moment_residual,
&record.translation_correction, &record.rotation_correction};
for (std::size_t index = 0U; index < std::size(decisions); ++index) {
decisions[index]->numerator_norm = 0.0;
decisions[index]->denominator_scale = 0.0;
decisions[index]->branch = fesa::NewtonConvergenceBranch::kZeroScale;
decisions[index]->threshold = fesa::kNewtonZeroScaleThreshold;
decisions[index]->threshold_unit = units[index];
decisions[index]->passed = true;
}
const auto fields = WriteAndRead(directory.Path() / "model_log.txt", record);
for (const std::string_view prefix :
{"residual_force", "residual_moment", "correction_translation",
"correction_rotation"}) {
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_numerator_norm"),
std::string{prefix} + "_numerator_norm=0");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_denominator_scale"),
std::string{prefix} + "_denominator_scale=0");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_branch"),
std::string{prefix} + "_branch=zero-scale");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_threshold"),
std::string{prefix} + "_threshold=0.001");
}
EXPECT_EQ(FieldValue(fields, "residual_force_threshold_unit"),
"residual_force_threshold_unit=force");
EXPECT_EQ(FieldValue(fields, "residual_moment_threshold_unit"),
"residual_moment_threshold_unit=force*length");
EXPECT_EQ(FieldValue(fields, "correction_translation_threshold_unit"),
"correction_translation_threshold_unit=length");
EXPECT_EQ(FieldValue(fields, "correction_rotation_threshold_unit"),
"correction_rotation_threshold_unit=1");
}
// NSNR-LOG-008 and NSNR-LOG-009
TEST(NewtonIterationLog, EscapesStringsAndOmitsUnevaluatedFailureFields) {
TempDirectory directory{"failure"};
auto record = CompleteRecord();
record.last_completed_stage = fesa::NewtonIterationStage::kInternalAssembled;
record.failure = {true,
fesa::NewtonIterationStage::kExternalAssembled,
"load\\failure",
std::string{"external\tload\nmissing\r"} + '\0'};
record.transition = "fail\ttransition";
record.outcome = "failed\nrecord";
record.diagnostic_code = std::string{"external\\load\r"} + '\0';
const auto fields = WriteAndRead(directory.Path() / "model_log.txt", record);
const std::string not_evaluated =
"not-evaluated(external\\tload\\nmissing\\r\\0)";
for (std::size_t index = 7U; index < 36U; ++index) {
EXPECT_EQ(ValueAfterEquals(fields[index]), not_evaluated);
}
EXPECT_EQ(FieldValue(fields, "failing_stage"),
"failing_stage=load-boundary");
EXPECT_EQ(FieldValue(fields, "transition"),
"transition=fail\\ttransition");
EXPECT_EQ(FieldValue(fields, "outcome"), "outcome=failed\\nrecord");
EXPECT_EQ(FieldValue(fields, "diagnostic_code"),
"diagnostic_code=external\\\\load\\r\\0");
}
// NSNR-LOG-010
TEST(NewtonIterationLog, ReportsWriteFlushAndCloseStateFailures) {
TempDirectory directory{"failures"};
const auto path = directory.Path() / "model_log.txt";
fesa::NewtonIterationLogWriter writer;
ExpectLogOutputFailure(writer.Write(CompleteRecord()));
ExpectLogOutputFailure(writer.Flush());
ExpectLogOutputFailure(writer.Close());
fesa::NewtonIterationLogWriter invalid_open_writer;
ExpectLogOutputFailure(
invalid_open_writer.Open(directory.Path() / "missing" / "log.txt"));
fesa::NewtonIterationLogWriter closed_writer;
ASSERT_TRUE(closed_writer.Open(path).IsOk());
ASSERT_TRUE(closed_writer.Close().IsOk());
ExpectLogOutputFailure(closed_writer.Write(CompleteRecord()));
ExpectLogOutputFailure(closed_writer.Flush());
ExpectLogOutputFailure(closed_writer.Close());
}
// NSNR-LOG-011
TEST(NewtonIterationLog, LeavesZeroByteLogForNonNewtonControlledFailurePath) {
TempDirectory directory{"zero-byte"};
const auto path = directory.Path() / "model_log.txt";
fesa::NewtonIterationLogWriter writer;
ASSERT_TRUE(writer.Open(path).IsOk());
ASSERT_TRUE(writer.Close().IsOk());
EXPECT_TRUE(std::filesystem::exists(path));
EXPECT_EQ(std::filesystem::file_size(path), 0U);
}
} // namespace