328 lines
13 KiB
C++
328 lines
13 KiB
C++
#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
|