Connect HHT field convergence to production acceptance and v2 logs

This commit is contained in:
KOKO\Mimi
2026-10-19 02:49:53 +09:00
parent 0e392a7364
commit dd54b0ae1e
9 changed files with 811 additions and 23 deletions
@@ -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