#include "fesa/analysis/newton_iteration_log.h" #include #include #include #include #include #include #include #include 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(byte); } break; } } return escaped; } Result FormatFiniteNumber(const double value, const std::filesystem::path& log_path) { if (!std::isfinite(value)) { return Result::Failure( LogOutputFailure(log_path, "A Newton log numeric value is nonfinite.")); } std::array buffer{}; const auto conversion = std::to_chars( buffer.data(), buffer.data() + buffer.size(), value, std::chars_format::general, std::numeric_limits::max_digits10); if (conversion.ec != std::errc{}) { return Result::Failure( LogOutputFailure(log_path, "A Newton log numeric value could not format.")); } return Result::Success( std::string{buffer.data(), conversion.ptr}); } Result BranchLabel( const NewtonConvergenceBranch branch, const std::filesystem::path& log_path) { switch (branch) { case NewtonConvergenceBranch::kRelative: return Result::Success("relative"); case NewtonConvergenceBranch::kZeroScale: return Result::Success("zero-scale"); } return Result::Failure(LogOutputFailure( log_path, "A Newton log convergence branch is not supported.")); } Result UnitLabel(const NewtonChannelUnit unit, const std::filesystem::path& log_path) { switch (unit) { case NewtonChannelUnit::kForce: return Result::Success("force"); case NewtonChannelUnit::kForceLength: return Result::Success("force*length"); case NewtonChannelUnit::kLength: return Result::Success("length"); case NewtonChannelUnit::kDimensionless: return Result::Success("1"); } return Result::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* fields, const std::string_view label, const std::string& value) { fields->push_back(std::string{label} + "=" + value); } Status AppendChannelFields( std::vector* 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 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::Failure(load_factor.GetStatus()); } const bool channels_evaluated = AreChannelsEvaluated(record); const std::string not_evaluated = "not-evaluated(" + EscapeText(record.failure.reason) + ")"; std::vector 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 specs{{ {"residual_force", "residual_force"}, {"residual_moment", "residual_moment"}, {"correction_translation", "correction_translation"}, {"correction_rotation", "correction_rotation"}, }}; const std::array 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::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::Success(std::move(line)); } /// @brief Serializes dynamic evidence without static load-factor semantics. Result SerializeRecord( const DynamicNewtonIterationLogRecord& record, const std::filesystem::path& log_path) { const auto time = FormatFiniteNumber(record.time, log_path); if (!time.HasValue()) return Result::Failure(time.GetStatus()); if (record.time < 0.0 || record.stage.empty() || record.outcome.empty()) { return Result::Failure(LogOutputFailure( log_path, "A dynamic Newton log requires nonnegative time, stage, and outcome.")); } std::vector fields; fields.reserve(24U); AppendField(&fields, "record", "dynamic-newton-iteration"); AppendField(&fields, "schema", "hht-log-v1"); AppendField(&fields, "increment", std::to_string(record.increment_index)); AppendField(&fields, "time", time.Value()); AppendField(&fields, "iteration", std::to_string(record.iteration_index)); AppendField(&fields, "stage", EscapeText(record.stage)); const std::array channels{"u", "phi"}; for (std::size_t channel = 0U; channel < channels.size(); ++channel) { const std::string suffix{channels[channel]}; const std::array, 6U> metrics{{ {"residual_" + suffix + "_numerator", record.residual_numerator[channel]}, {"residual_" + suffix + "_scale", record.residual_scale[channel]}, {"coordinate_correction_" + suffix, record.coordinate_correction[channel]}, {"velocity_correction_" + suffix, record.velocity_correction[channel]}, {"acceleration_correction_" + suffix, record.acceleration_correction[channel]}, {"algorithmic_correction_" + suffix, record.algorithmic_correction[channel]}, }}; for (const auto& metric : metrics) { const auto formatted = FormatFiniteNumber(metric.second, log_path); if (!formatted.HasValue()) { return Result::Failure(formatted.GetStatus()); } if (metric.second < 0.0) { return Result::Failure(LogOutputFailure( log_path, "A dynamic Newton log norm or scale is negative.")); } AppendField(&fields, metric.first, formatted.Value()); } AppendField( &fields, "residual_" + suffix + "_branch", record.residual_scale[channel] == 0.0 ? "zero-scale" : "relative"); } AppendField(&fields, "residual_pass", record.residual_pass ? "1" : "0"); AppendField(&fields, "correction_pass", record.correction_pass ? "1" : "0"); AppendField(&fields, "outcome", EscapeText(record.outcome)); AppendField(&fields, "failure_code", EscapeText(record.failure_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::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(line.Value().size())); if (!stream_) { state_ = State::kFailed; return LogOutputFailure(log_path_, "The Newton log record could not write."); } return Status::Ok(); } Status NewtonIterationLogWriter::Write( const DynamicNewtonIterationLogRecord& 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(line.Value().size())); if (!stream_) { state_ = State::kFailed; return LogOutputFailure(log_path_, "The dynamic 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