feat(nonlinear-static-newton-raphson): step 11 - equilibrium-projection-and-reference-gate

This commit is contained in:
KOKO\Mimi
2026-08-28 09:22:33 +09:00
parent 65b56b1755
commit 6dbf1b2bcf
30 changed files with 13122 additions and 728 deletions
@@ -80,6 +80,14 @@ std::string ValueAfterEquals(const std::string& field) {
return field.substr(field.find('=') + 1U);
}
fesa::Vector VectorFrom(const std::vector<double>& values) {
fesa::Vector result{values.size()};
for (std::size_t index = 0U; index < values.size(); ++index) {
result[index] = values[index];
}
return result;
}
fesa::NewtonChannelDecision MakeDecision(const std::string& identity) {
return {identity,
0.0012345678901234567,
@@ -324,4 +332,84 @@ TEST(NewtonIterationLog, LeavesZeroByteLogForNonNewtonControlledFailurePath) {
EXPECT_EQ(std::filesystem::file_size(path), 0U);
}
// NSNR-LOG-012
TEST(NewtonIterationLog,
SerializesEvaluatorProducedExternalScaleResidualDecisions) {
TempDirectory directory{"external-scale"};
const fesa::NewtonConvergenceInput zero_external{
{"residual_force", VectorFrom({1.0e-3}), VectorFrom({2.0}),
VectorFrom({0.0})},
{"residual_moment", VectorFrom({1.0e-3}), VectorFrom({3.0}),
VectorFrom({0.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
const auto zero_decisions = fesa::EvaluateNewtonConvergence(zero_external);
auto zero_record = CompleteRecord();
zero_record.force_residual = zero_decisions.force_residual;
zero_record.moment_residual = zero_decisions.moment_residual;
zero_record.translation_correction = zero_decisions.translation_correction;
zero_record.rotation_correction = zero_decisions.rotation_correction;
zero_record.combined_passed = zero_decisions.combined_passed;
const auto zero_fields =
WriteAndRead(directory.Path() / "zero_log.txt", zero_record);
ASSERT_EQ(zero_fields.size(), 39U);
EXPECT_EQ(FieldValue(zero_fields, "schema"), "schema=nsnr-log-v1");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_denominator_scale"),
"residual_force_denominator_scale=0");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_denominator_scale"),
"residual_moment_denominator_scale=0");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_branch"),
"residual_force_branch=zero-scale");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_branch"),
"residual_moment_branch=zero-scale");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_threshold"),
"residual_force_threshold=0.001");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_threshold"),
"residual_moment_threshold=0.001");
EXPECT_EQ(FieldValue(zero_fields, "residual_force_threshold_unit"),
"residual_force_threshold_unit=force");
EXPECT_EQ(FieldValue(zero_fields, "residual_moment_threshold_unit"),
"residual_moment_threshold_unit=force*length");
const fesa::NewtonConvergenceInput nonzero_external{
{"residual_force", VectorFrom({2.0e-2}), VectorFrom({2.0}),
VectorFrom({1.0})},
{"residual_moment", VectorFrom({2.0e-2}), VectorFrom({1.0}),
VectorFrom({2.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
const auto relative_decisions =
fesa::EvaluateNewtonConvergence(nonzero_external);
auto relative_record = CompleteRecord();
relative_record.force_residual = relative_decisions.force_residual;
relative_record.moment_residual = relative_decisions.moment_residual;
relative_record.translation_correction =
relative_decisions.translation_correction;
relative_record.rotation_correction = relative_decisions.rotation_correction;
relative_record.combined_passed = relative_decisions.combined_passed;
const auto relative_fields =
WriteAndRead(directory.Path() / "relative_log.txt", relative_record);
ASSERT_EQ(relative_fields.size(), 39U);
EXPECT_EQ(FieldValue(relative_fields, "residual_force_denominator_scale"),
"residual_force_denominator_scale=2");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_denominator_scale"),
"residual_moment_denominator_scale=2");
EXPECT_EQ(FieldValue(relative_fields, "residual_force_branch"),
"residual_force_branch=relative");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_branch"),
"residual_moment_branch=relative");
EXPECT_EQ(FieldValue(relative_fields, "residual_force_threshold"),
"residual_force_threshold=0.01");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_threshold"),
"residual_moment_threshold=0.01");
EXPECT_EQ(FieldValue(relative_fields, "residual_force_threshold_unit"),
"residual_force_threshold_unit=1");
EXPECT_EQ(FieldValue(relative_fields, "residual_moment_threshold_unit"),
"residual_moment_threshold_unit=1");
}
} // namespace
@@ -143,6 +143,18 @@ fesa::NewtonConvergenceInput RelativeConvergenceInput() {
};
}
fesa::NewtonConvergenceInput ExactZeroExternalResidualInput(
const double force_numerator, const double moment_numerator) {
return {
{"residual_force", VectorFrom({force_numerator}), VectorFrom({2.0}),
VectorFrom({0.0})},
{"residual_moment", VectorFrom({moment_numerator}), VectorFrom({3.0}),
VectorFrom({0.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
}
// NSNR-CTRL-001, NSNR-CTRL-002, and NSNR-CTRL-017
TEST(NonlinearStaticControl, BuildsFixedFactorSequenceWithExactEndpoint) {
const auto fixture = MakeFixture();
@@ -362,4 +374,161 @@ TEST(NonlinearConvergence, AppliesTypedZeroScaleGatesToEmptyChannels) {
}
}
// NSNR-CTRL-018
TEST(NonlinearConvergence,
SelectsTypedZeroScaleFromExactZeroExternalResidualChannels) {
const auto accepted = fesa::EvaluateNewtonConvergence(
ExactZeroExternalResidualInput(fesa::kNewtonZeroScaleThreshold,
fesa::kNewtonZeroScaleThreshold));
EXPECT_TRUE(accepted.combined_passed);
const std::array<fesa::NewtonChannelDecision, 2U> decisions{
accepted.force_residual, accepted.moment_residual};
const std::array<fesa::NewtonChannelUnit, 2U> units{
fesa::NewtonChannelUnit::kForce,
fesa::NewtonChannelUnit::kForceLength};
for (std::size_t index = 0U; index < decisions.size(); ++index) {
EXPECT_DOUBLE_EQ(decisions[index].denominator_scale, 0.0);
EXPECT_EQ(decisions[index].branch,
fesa::NewtonConvergenceBranch::kZeroScale);
EXPECT_DOUBLE_EQ(decisions[index].threshold,
fesa::kNewtonZeroScaleThreshold);
EXPECT_EQ(decisions[index].threshold_unit, units[index]);
EXPECT_TRUE(decisions[index].passed);
}
const double just_over = std::nextafter(
fesa::kNewtonZeroScaleThreshold,
std::numeric_limits<double>::infinity());
const auto force_rejected =
fesa::EvaluateNewtonConvergence(ExactZeroExternalResidualInput(
just_over, fesa::kNewtonZeroScaleThreshold));
const auto moment_rejected =
fesa::EvaluateNewtonConvergence(ExactZeroExternalResidualInput(
fesa::kNewtonZeroScaleThreshold, just_over));
EXPECT_FALSE(force_rejected.force_residual.passed);
EXPECT_FALSE(force_rejected.combined_passed);
EXPECT_FALSE(moment_rejected.moment_residual.passed);
EXPECT_FALSE(moment_rejected.combined_passed);
}
// NSNR-CTRL-019
TEST(NonlinearConvergence,
UsesRelativeResidualScaleOnlyWhenMatchingExternalScaleIsNonzero) {
fesa::NewtonConvergenceInput input{
{"residual_force", VectorFrom({2.0e-2}), VectorFrom({2.0}),
VectorFrom({1.0})},
{"residual_moment", VectorFrom({2.0e-2}), VectorFrom({1.0}),
VectorFrom({2.0})},
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
};
const auto accepted = fesa::EvaluateNewtonConvergence(input);
EXPECT_TRUE(accepted.combined_passed);
for (const auto& decision : {accepted.force_residual,
accepted.moment_residual}) {
EXPECT_DOUBLE_EQ(decision.denominator_scale, 2.0);
EXPECT_EQ(decision.branch, fesa::NewtonConvergenceBranch::kRelative);
EXPECT_DOUBLE_EQ(decision.threshold, fesa::kNewtonRelativeThreshold);
EXPECT_EQ(decision.threshold_unit, fesa::NewtonChannelUnit::kDimensionless);
EXPECT_TRUE(decision.passed);
}
const double just_over = std::nextafter(2.0e-2,
std::numeric_limits<double>::infinity());
input.force_residual.free_residual = VectorFrom({just_over});
EXPECT_FALSE(fesa::EvaluateNewtonConvergence(input).force_residual.passed);
input.force_residual.free_residual = VectorFrom({2.0e-2});
input.moment_residual.free_residual = VectorFrom({just_over});
EXPECT_FALSE(fesa::EvaluateNewtonConvergence(input).moment_residual.passed);
input.moment_residual.free_residual = VectorFrom({2.0e-2});
input.force_residual.free_residual = VectorFrom({9.0e-4});
input.force_residual.free_internal = VectorFrom({5.0e-3});
input.force_residual.free_external = VectorFrom({1.0e-2});
const auto relative_rejected = fesa::EvaluateNewtonConvergence(input);
EXPECT_FALSE(relative_rejected.force_residual.passed);
EXPECT_FALSE(relative_rejected.combined_passed);
}
// NSNR-CTRL-020
TEST(NonlinearConvergence,
RejectsNonfiniteResidualInputsBeforeSelectingOrAcceptingAChannel) {
const std::array<double, 2U> invalid_values{
std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity()};
for (const double invalid : invalid_values) {
for (const bool is_moment : {false, true}) {
for (const std::size_t vector_index : {0U, 1U, 2U}) {
auto input = RelativeConvergenceInput();
fesa::NewtonResidualChannelInput* channel =
is_moment ? &input.moment_residual : &input.force_residual;
if (vector_index == 0U) {
channel->free_residual = VectorFrom({invalid});
} else if (vector_index == 1U) {
channel->free_internal = VectorFrom({invalid});
} else {
channel->free_external = VectorFrom({invalid});
}
const auto result = fesa::EvaluateNewtonConvergence(input);
const auto& decision =
is_moment ? result.moment_residual : result.force_residual;
EXPECT_FALSE(decision.passed);
EXPECT_FALSE(result.combined_passed);
}
}
}
auto input = RelativeConvergenceInput();
const double smallest_external = std::nextafter(
0.0, std::numeric_limits<double>::infinity());
input.force_residual.free_residual = VectorFrom({0.0});
input.force_residual.free_internal = VectorFrom({0.0});
input.force_residual.free_external = VectorFrom({smallest_external});
const auto relative = fesa::EvaluateNewtonConvergence(input);
EXPECT_TRUE(relative.force_residual.passed);
EXPECT_DOUBLE_EQ(relative.force_residual.denominator_scale,
smallest_external);
EXPECT_EQ(relative.force_residual.branch,
fesa::NewtonConvergenceBranch::kRelative);
EXPECT_DOUBLE_EQ(relative.force_residual.threshold,
fesa::kNewtonRelativeThreshold);
EXPECT_EQ(relative.force_residual.threshold_unit,
fesa::NewtonChannelUnit::kDimensionless);
}
// NSNR-CTRL-021
TEST(NonlinearConvergence, RequiresEveryResidualAndCorrectionChannelToPass) {
const auto accepted = fesa::EvaluateNewtonConvergence(RelativeConvergenceInput());
EXPECT_TRUE(accepted.combined_passed);
EXPECT_DOUBLE_EQ(accepted.translation_correction.denominator_scale, 1.0);
EXPECT_DOUBLE_EQ(accepted.rotation_correction.denominator_scale, 1.0);
EXPECT_DOUBLE_EQ(accepted.translation_correction.threshold,
fesa::kNewtonRelativeThreshold);
EXPECT_DOUBLE_EQ(accepted.rotation_correction.threshold,
fesa::kNewtonRelativeThreshold);
EXPECT_EQ(accepted.translation_correction.threshold_unit,
fesa::NewtonChannelUnit::kDimensionless);
EXPECT_EQ(accepted.rotation_correction.threshold_unit,
fesa::NewtonChannelUnit::kDimensionless);
for (const std::size_t failed_channel : {0U, 1U, 2U, 3U}) {
auto input = RelativeConvergenceInput();
const double just_over = std::nextafter(
1.0e-2, std::numeric_limits<double>::infinity());
if (failed_channel == 0U) {
input.force_residual.free_residual = VectorFrom({just_over});
} else if (failed_channel == 1U) {
input.moment_residual.free_residual = VectorFrom({just_over});
} else if (failed_channel == 2U) {
input.translation_correction.fresh_correction = VectorFrom({just_over});
} else {
input.rotation_correction.fresh_correction = VectorFrom({just_over});
}
const auto result = fesa::EvaluateNewtonConvergence(input);
EXPECT_FALSE(result.combined_passed);
}
}
} // namespace