feat(nonlinear-static-newton-raphson): step 7 - newton-iteration-log-writer
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
#ifndef FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
|
||||
#define FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/analysis/nonlinear_static_control.h"
|
||||
#include "fesa/core/status.h"
|
||||
|
||||
namespace fesa {
|
||||
|
||||
/// @brief Identifies the latest Newton procedure stage reached by a record.
|
||||
enum class NewtonIterationStage {
|
||||
kEntered,
|
||||
kKinematicsEvaluated,
|
||||
kInternalAssembled,
|
||||
kExternalAssembled,
|
||||
kSolvedCorrection,
|
||||
kConvergenceEvaluated,
|
||||
kAccepted,
|
||||
kFailed,
|
||||
};
|
||||
|
||||
/// @brief Carries the controlled failure that ended one Newton iteration.
|
||||
struct NewtonIterationFailure {
|
||||
bool present;
|
||||
NewtonIterationStage stage;
|
||||
std::string diagnostic_code;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
/// @brief Carries one fixed-schema Newton iteration diagnostic record.
|
||||
struct NewtonIterationLogRecord {
|
||||
std::string step_name;
|
||||
std::uint32_t increment_index;
|
||||
double load_factor;
|
||||
std::uint32_t iteration;
|
||||
NewtonIterationStage last_completed_stage;
|
||||
NewtonIterationFailure failure;
|
||||
std::string transition;
|
||||
std::string outcome;
|
||||
std::string diagnostic_code;
|
||||
NewtonChannelDecision force_residual;
|
||||
NewtonChannelDecision moment_residual;
|
||||
NewtonChannelDecision translation_correction;
|
||||
NewtonChannelDecision rotation_correction;
|
||||
bool combined_passed;
|
||||
};
|
||||
|
||||
/// @brief Writes deterministic, line-oriented Newton iteration diagnostics.
|
||||
/// @note A write, flush, or close failure permanently prevents a successful
|
||||
/// log lifecycle for this writer instance.
|
||||
class NewtonIterationLogWriter {
|
||||
public:
|
||||
/// @brief Opens and truncates the diagnostic log before Newton execution.
|
||||
/// @param log_path Derived application-owned diagnostic-log path.
|
||||
/// @return Success when the writer owns an empty binary log, otherwise an
|
||||
/// output failure with code `log-output-failure`.
|
||||
Status Open(const std::filesystem::path& log_path);
|
||||
|
||||
/// @brief Writes one complete LF-terminated Newton iteration record.
|
||||
/// @param record Explicit stage, outcome, and typed convergence decision.
|
||||
/// @return Success after writing a complete record, otherwise an output
|
||||
/// failure with code `log-output-failure`.
|
||||
Status Write(const NewtonIterationLogRecord& record);
|
||||
|
||||
/// @brief Flushes every complete record written so far to the log.
|
||||
/// @return Success after the stream flush, otherwise an output failure with
|
||||
/// code `log-output-failure`.
|
||||
Status Flush();
|
||||
|
||||
/// @brief Closes the log before authoritative result finalization.
|
||||
/// @return Success after close, otherwise an output failure with code
|
||||
/// `log-output-failure`.
|
||||
Status Close();
|
||||
|
||||
private:
|
||||
enum class State { kReady, kOpen, kClosed, kFailed };
|
||||
|
||||
std::ofstream stream_;
|
||||
std::filesystem::path log_path_;
|
||||
State state_{State::kReady};
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
#endif // FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
|
||||
@@ -4,6 +4,7 @@ add_library(
|
||||
analysis/analysis_model.cpp
|
||||
analysis/analysis_state.cpp
|
||||
analysis/linear_static_analysis.cpp
|
||||
analysis/newton_iteration_log.cpp
|
||||
analysis/nonlinear_static_control.cpp
|
||||
app/fesa_application.cpp
|
||||
app/output_paths.cpp
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
#include "fesa/analysis/newton_iteration_log.h"
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
struct ChannelSpec {
|
||||
std::string_view label_prefix;
|
||||
std::string_view identity;
|
||||
};
|
||||
|
||||
Status LogOutputFailure(const std::filesystem::path& log_path,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kOutput,
|
||||
{{Severity::kError,
|
||||
"log-output-failure",
|
||||
{{}, 0U},
|
||||
"",
|
||||
log_path.generic_u8string(),
|
||||
message}});
|
||||
}
|
||||
|
||||
std::string EscapeText(const std::string_view value) {
|
||||
constexpr char kHexDigits[] = "0123456789ABCDEF";
|
||||
std::string escaped;
|
||||
escaped.reserve(value.size());
|
||||
for (const unsigned char byte : value) {
|
||||
switch (byte) {
|
||||
case '\\':
|
||||
escaped += "\\\\";
|
||||
break;
|
||||
case '\t':
|
||||
escaped += "\\t";
|
||||
break;
|
||||
case '\n':
|
||||
escaped += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escaped += "\\r";
|
||||
break;
|
||||
case '\0':
|
||||
escaped += "\\0";
|
||||
break;
|
||||
default:
|
||||
if (byte < 0x20U || byte == 0x7FU) {
|
||||
escaped += "\\x";
|
||||
escaped += kHexDigits[byte >> 4U];
|
||||
escaped += kHexDigits[byte & 0x0FU];
|
||||
} else {
|
||||
escaped += static_cast<char>(byte);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
Result<std::string> FormatFiniteNumber(const double value,
|
||||
const std::filesystem::path& log_path) {
|
||||
if (!std::isfinite(value)) {
|
||||
return Result<std::string>::Failure(
|
||||
LogOutputFailure(log_path, "A Newton log numeric value is nonfinite."));
|
||||
}
|
||||
|
||||
std::array<char, 64U> buffer{};
|
||||
const auto conversion = std::to_chars(
|
||||
buffer.data(), buffer.data() + buffer.size(), value,
|
||||
std::chars_format::general, std::numeric_limits<double>::max_digits10);
|
||||
if (conversion.ec != std::errc{}) {
|
||||
return Result<std::string>::Failure(
|
||||
LogOutputFailure(log_path, "A Newton log numeric value could not format."));
|
||||
}
|
||||
return Result<std::string>::Success(
|
||||
std::string{buffer.data(), conversion.ptr});
|
||||
}
|
||||
|
||||
Result<std::string_view> BranchLabel(
|
||||
const NewtonConvergenceBranch branch,
|
||||
const std::filesystem::path& log_path) {
|
||||
switch (branch) {
|
||||
case NewtonConvergenceBranch::kRelative:
|
||||
return Result<std::string_view>::Success("relative");
|
||||
case NewtonConvergenceBranch::kZeroScale:
|
||||
return Result<std::string_view>::Success("zero-scale");
|
||||
}
|
||||
return Result<std::string_view>::Failure(LogOutputFailure(
|
||||
log_path, "A Newton log convergence branch is not supported."));
|
||||
}
|
||||
|
||||
Result<std::string_view> UnitLabel(const NewtonChannelUnit unit,
|
||||
const std::filesystem::path& log_path) {
|
||||
switch (unit) {
|
||||
case NewtonChannelUnit::kForce:
|
||||
return Result<std::string_view>::Success("force");
|
||||
case NewtonChannelUnit::kForceLength:
|
||||
return Result<std::string_view>::Success("force*length");
|
||||
case NewtonChannelUnit::kLength:
|
||||
return Result<std::string_view>::Success("length");
|
||||
case NewtonChannelUnit::kDimensionless:
|
||||
return Result<std::string_view>::Success("1");
|
||||
}
|
||||
return Result<std::string_view>::Failure(LogOutputFailure(
|
||||
log_path, "A Newton log threshold unit is not supported."));
|
||||
}
|
||||
|
||||
std::string StageLabel(const NewtonIterationStage stage) {
|
||||
switch (stage) {
|
||||
case NewtonIterationStage::kEntered:
|
||||
return "iteration-entered";
|
||||
case NewtonIterationStage::kKinematicsEvaluated:
|
||||
return "trial-state";
|
||||
case NewtonIterationStage::kInternalAssembled:
|
||||
return "residual";
|
||||
case NewtonIterationStage::kExternalAssembled:
|
||||
return "load-boundary";
|
||||
case NewtonIterationStage::kSolvedCorrection:
|
||||
return "correction";
|
||||
case NewtonIterationStage::kConvergenceEvaluated:
|
||||
return "convergence-gates";
|
||||
case NewtonIterationStage::kAccepted:
|
||||
return "commit";
|
||||
case NewtonIterationStage::kFailed:
|
||||
return "failed";
|
||||
}
|
||||
return "failed";
|
||||
}
|
||||
|
||||
bool AreChannelsEvaluated(const NewtonIterationLogRecord& record) {
|
||||
if (!record.failure.present) {
|
||||
return true;
|
||||
}
|
||||
return record.last_completed_stage ==
|
||||
NewtonIterationStage::kConvergenceEvaluated ||
|
||||
record.last_completed_stage == NewtonIterationStage::kAccepted;
|
||||
}
|
||||
|
||||
void AppendField(std::vector<std::string>* fields, const std::string_view label,
|
||||
const std::string& value) {
|
||||
fields->push_back(std::string{label} + "=" + value);
|
||||
}
|
||||
|
||||
Status AppendChannelFields(
|
||||
std::vector<std::string>* fields, const ChannelSpec spec,
|
||||
const NewtonChannelDecision& decision, const bool evaluated,
|
||||
const std::string& not_evaluated, const std::filesystem::path& log_path) {
|
||||
const std::string prefix{spec.label_prefix};
|
||||
if (!evaluated) {
|
||||
for (const std::string_view suffix :
|
||||
{"channel", "numerator_norm", "denominator_scale", "branch",
|
||||
"threshold", "threshold_unit", "pass"}) {
|
||||
AppendField(fields, prefix + "_" + std::string{suffix}, not_evaluated);
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
if (decision.identity != spec.identity) {
|
||||
return LogOutputFailure(log_path,
|
||||
"A Newton log channel identity is invalid.");
|
||||
}
|
||||
const auto numerator = FormatFiniteNumber(decision.numerator_norm, log_path);
|
||||
if (!numerator.HasValue()) {
|
||||
return numerator.GetStatus();
|
||||
}
|
||||
const auto denominator =
|
||||
FormatFiniteNumber(decision.denominator_scale, log_path);
|
||||
if (!denominator.HasValue()) {
|
||||
return denominator.GetStatus();
|
||||
}
|
||||
const auto branch = BranchLabel(decision.branch, log_path);
|
||||
if (!branch.HasValue()) {
|
||||
return branch.GetStatus();
|
||||
}
|
||||
const auto threshold = FormatFiniteNumber(decision.threshold, log_path);
|
||||
if (!threshold.HasValue()) {
|
||||
return threshold.GetStatus();
|
||||
}
|
||||
const auto unit = UnitLabel(decision.threshold_unit, log_path);
|
||||
if (!unit.HasValue()) {
|
||||
return unit.GetStatus();
|
||||
}
|
||||
|
||||
AppendField(fields, prefix + "_channel", decision.identity);
|
||||
AppendField(fields, prefix + "_numerator_norm", numerator.Value());
|
||||
AppendField(fields, prefix + "_denominator_scale", denominator.Value());
|
||||
AppendField(fields, prefix + "_branch", std::string{branch.Value()});
|
||||
AppendField(fields, prefix + "_threshold", threshold.Value());
|
||||
AppendField(fields, prefix + "_threshold_unit", std::string{unit.Value()});
|
||||
AppendField(fields, prefix + "_pass", decision.passed ? "1" : "0");
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<std::string> SerializeRecord(const NewtonIterationLogRecord& record,
|
||||
const std::filesystem::path& log_path) {
|
||||
const auto load_factor = FormatFiniteNumber(record.load_factor, log_path);
|
||||
if (!load_factor.HasValue()) {
|
||||
return Result<std::string>::Failure(load_factor.GetStatus());
|
||||
}
|
||||
|
||||
const bool channels_evaluated = AreChannelsEvaluated(record);
|
||||
const std::string not_evaluated =
|
||||
"not-evaluated(" + EscapeText(record.failure.reason) + ")";
|
||||
std::vector<std::string> fields;
|
||||
fields.reserve(39U);
|
||||
AppendField(&fields, "record", "newton-iteration");
|
||||
AppendField(&fields, "schema", "nsnr-log-v1");
|
||||
AppendField(&fields, "increment", std::to_string(record.increment_index));
|
||||
AppendField(&fields, "target_factor", load_factor.Value());
|
||||
AppendField(&fields, "iteration", std::to_string(record.iteration));
|
||||
AppendField(&fields, "last_completed_stage",
|
||||
StageLabel(record.last_completed_stage));
|
||||
AppendField(&fields, "failing_stage",
|
||||
record.failure.present ? StageLabel(record.failure.stage) : "none");
|
||||
|
||||
const std::array<ChannelSpec, 4U> specs{{
|
||||
{"residual_force", "residual_force"},
|
||||
{"residual_moment", "residual_moment"},
|
||||
{"correction_translation", "correction_translation"},
|
||||
{"correction_rotation", "correction_rotation"},
|
||||
}};
|
||||
const std::array<const NewtonChannelDecision*, 4U> decisions{{
|
||||
&record.force_residual,
|
||||
&record.moment_residual,
|
||||
&record.translation_correction,
|
||||
&record.rotation_correction,
|
||||
}};
|
||||
for (std::size_t index = 0U; index < specs.size(); ++index) {
|
||||
const Status channel_status =
|
||||
AppendChannelFields(&fields, specs[index], *decisions[index],
|
||||
channels_evaluated, not_evaluated, log_path);
|
||||
if (!channel_status.IsOk()) {
|
||||
return Result<std::string>::Failure(channel_status);
|
||||
}
|
||||
}
|
||||
AppendField(&fields, "combined_pass",
|
||||
channels_evaluated ? (record.combined_passed ? "1" : "0")
|
||||
: not_evaluated);
|
||||
AppendField(&fields, "transition", EscapeText(record.transition));
|
||||
AppendField(&fields, "outcome", EscapeText(record.outcome));
|
||||
AppendField(&fields, "diagnostic_code", EscapeText(record.diagnostic_code));
|
||||
|
||||
std::string line;
|
||||
for (std::size_t index = 0U; index < fields.size(); ++index) {
|
||||
if (index != 0U) {
|
||||
line += '\t';
|
||||
}
|
||||
line += fields[index];
|
||||
}
|
||||
line += '\n';
|
||||
return Result<std::string>::Success(std::move(line));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status NewtonIterationLogWriter::Open(const std::filesystem::path& log_path) {
|
||||
if (state_ != State::kReady) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log writer is not ready.");
|
||||
}
|
||||
|
||||
log_path_ = log_path;
|
||||
stream_.open(log_path_, std::ios::binary | std::ios::out | std::ios::trunc);
|
||||
if (!stream_.is_open() || !stream_) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log could not open.");
|
||||
}
|
||||
state_ = State::kOpen;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status NewtonIterationLogWriter::Write(const NewtonIterationLogRecord& record) {
|
||||
if (state_ != State::kOpen) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
|
||||
}
|
||||
|
||||
const auto line = SerializeRecord(record, log_path_);
|
||||
if (!line.HasValue()) {
|
||||
state_ = State::kFailed;
|
||||
return line.GetStatus();
|
||||
}
|
||||
stream_.write(line.Value().data(),
|
||||
static_cast<std::streamsize>(line.Value().size()));
|
||||
if (!stream_) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log record could not write.");
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status NewtonIterationLogWriter::Flush() {
|
||||
if (state_ != State::kOpen) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
|
||||
}
|
||||
|
||||
stream_.flush();
|
||||
if (!stream_) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log could not flush.");
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status NewtonIterationLogWriter::Close() {
|
||||
if (state_ != State::kOpen) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
|
||||
}
|
||||
|
||||
stream_.close();
|
||||
if (stream_.fail()) {
|
||||
state_ = State::kFailed;
|
||||
return LogOutputFailure(log_path_, "The Newton log could not close.");
|
||||
}
|
||||
state_ = State::kClosed;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -7,6 +7,7 @@ add_executable(
|
||||
unit/analysis/analysis_model_test.cpp
|
||||
unit/analysis/analysis_state_test.cpp
|
||||
unit/analysis/nonlinear_static_control_test.cpp
|
||||
unit/analysis/newton_iteration_log_test.cpp
|
||||
unit/assembly/parallel_for_test.cpp
|
||||
unit/assembly/load_assembler_test.cpp
|
||||
unit/assembly/sparse_assembler_test.cpp
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user