Implement private HHT field convergence policy and oracle tests

This commit is contained in:
KOKO\Mimi
2026-10-19 02:20:58 +09:00
parent adb809bc01
commit ad06d7ad6c
7 changed files with 454 additions and 0 deletions
+1
View File
@@ -16,6 +16,7 @@
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_algebra.cpp" />
<ClInclude Include="..\src\fesa\analysis\hht_development_probe.h" />
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_assembly.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_field_convergence.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_reduced_operators.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_reduced_procedure.cpp" />
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_procedure.cpp" />
+1
View File
@@ -21,6 +21,7 @@
<ClCompile Include="..\tests\unit\analysis\hht_temporal_verification_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_dynamic_algebra_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_development_probe_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_field_convergence_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_reduced_shell_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\hht_dynamic_procedure_test.cpp" />
<ClCompile Include="..\tests\unit\analysis\nonlinear_analysis_state_test.cpp" />
+1
View File
@@ -9,6 +9,7 @@ add_library(
analysis/hht_dynamic_algebra.cpp
analysis/hht_development_probe.h
analysis/hht_dynamic_assembly.cpp
analysis/hht_field_convergence.cpp
analysis/hht_reduced_operators.cpp
analysis/hht_reduced_procedure.cpp
analysis/hht_dynamic_procedure.cpp
+166
View File
@@ -0,0 +1,166 @@
#include "analysis/hht_field_convergence.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace fesa::hht_field_convergence {
namespace {
constexpr double kRn = .005, kCn = .01, kRp = .02, kEpsilon = 1e-5;
constexpr double kZeroCorrection = .001, kLinear = 1e-8;
constexpr double kInactive = 1e-5, kSmallTranslation = 1e-8;
bool Nonnegative(double x) { return std::isfinite(x) && x >= 0; }
bool ValidOptional(const std::optional<double>& x) {
return !x || Nonnegative(*x);
}
bool ValidHistory(const History& h) {
return Nonnegative(h.nonzero_sum) && Nonnegative(h.maximum_sum) &&
h.nonzero_count <= h.maximum_count &&
(h.nonzero_count != 0 || h.nonzero_sum == 0) &&
(h.maximum_count != 0 || h.maximum_sum == 0);
}
} // namespace
Evaluation Evaluate(const std::array<FieldInput, 2>& fields,
const std::array<History, 2>& committed,
std::size_t iteration, double reference_length) {
Evaluation result;
result.status = Status::Failure(FailureCategory::kSolver,
{{Severity::kError, "invalid-state", {}, {}, {}, "Invalid field convergence arithmetic or input."}});
result.pending = committed;
if (!std::isfinite(reference_length) || reference_length <= 0) return result;
for (std::size_t a = 0; a < 2; ++a) {
const auto& f = fields[a];
const auto& h = committed[a];
auto& out = result.fields[a];
if (!ValidHistory(h) || !Nonnegative(f.residual) || !Nonnegative(f.increment) ||
!ValidOptional(f.correction) || !ValidOptional(f.previous_residual) ||
!ValidOptional(f.older_residual)) return result;
if (f.flux.empty()) {
if (f.active) { result.status = Status::Failure(FailureCategory::kSolver,
{{Severity::kError, "invalid-flux-population", {}, {}, {}, "Empty active flux population."}}); return result; }
continue;
}
const double historic_maximum = h.maximum_count == 0 ? 0 :
h.maximum_sum / static_cast<double>(h.maximum_count);
const double cutoff = kInactive * historic_maximum;
double maximum = 0;
for (const auto& x : f.flux) {
if (!std::isfinite(x.internal) || !std::isfinite(x.inertia)) return result;
const double total = x.internal + x.inertia;
if (!std::isfinite(total)) return result;
maximum = std::max(maximum, std::abs(total));
}
out.maximum_flux = maximum;
const bool filter = maximum >= .1 * historic_maximum;
double sum = 0;
std::size_t count = 0;
for (const auto& x : f.flux) {
const double b = std::abs(x.internal + x.inertia);
if (!filter || b >= cutoff) {
sum += b;
if (!std::isfinite(sum)) return result;
++count;
}
}
if (count == 0) { result.status = Status::Failure(FailureCategory::kSolver,
{{Severity::kError, "invalid-flux-population", {}, {}, {}, "Empty active flux population."}}); return result; }
const double spatial = sum / static_cast<double>(count);
out.spatial_flux = spatial;
if (h.nonzero_count == std::numeric_limits<std::uint64_t>::max() ||
h.maximum_count == std::numeric_limits<std::uint64_t>::max()) return result;
const double base = h.nonzero_count == 0 ? .01 :
h.nonzero_sum / static_cast<double>(h.nonzero_count);
const double next_sum = h.nonzero_sum + spatial;
const double next_maximum = h.maximum_sum + maximum;
if (!Nonnegative(next_sum) || !Nonnegative(next_maximum)) return result;
const double included = next_sum / static_cast<double>(h.nonzero_count + 1);
const bool eligible = spatial > kEpsilon * (h.nonzero_count == 0 ? base : included);
const double raw = eligible ? included : base;
if (!std::isfinite(raw) || raw <= 0) return result;
out.time_average_flux = raw;
out.residual_scale = raw;
auto& pending = result.pending[a];
if (eligible) { pending.nonzero_sum = next_sum; ++pending.nonzero_count; }
pending.maximum_sum = next_maximum;
++pending.maximum_count;
}
// Both conversions read raw means, never a converted peer scale.
if (fields[0].active && fields[1].active) {
const double force = *result.fields[0].time_average_flux;
const double moment = *result.fields[1].time_average_flux;
const std::array<double, 2> converted{moment / reference_length, reference_length * force};
for (std::size_t a = 0; a < 2; ++a) {
if (!Nonnegative(converted[a])) return result;
auto& out = result.fields[a];
if (*out.spatial_flux <= *out.time_average_flux &&
*out.time_average_flux < converted[a]) out.residual_scale = converted[a];
}
}
bool all_linear = iteration >= 1;
for (std::size_t a = 0; a < 2; ++a) {
if (!fields[a].active) continue;
const double scale = *result.fields[a].residual_scale;
if (!std::isfinite(scale) || scale <= 0) return result;
const double ratio = fields[a].residual / scale;
if (!Nonnegative(ratio)) return result;
all_linear = all_linear && ratio <= kLinear;
}
bool all_pass = true;
for (std::size_t a = 0; a < 2; ++a) {
const auto& f = fields[a];
if (!f.active) continue;
auto& out = result.fields[a];
const double scale = *out.residual_scale;
const double ratio = f.residual / scale;
if (all_linear) {
out.branch = Branch::kLinear;
out.residual_threshold = kLinear;
out.residual_pass = true; out.field_pass = true;
continue;
}
const bool zero = *out.spatial_flux <= kEpsilon * scale;
bool nonquadratic = false;
if (!zero && iteration > 9 && f.previous_residual && f.older_residual &&
*f.older_residual > *f.previous_residual && *f.previous_residual > f.residual && f.residual > 0) {
const double recent = f.residual / *f.previous_residual;
const double older = *f.previous_residual / *f.older_residual;
if (!Nonnegative(recent) || !Nonnegative(older)) return result;
nonquadratic = recent >= older;
}
out.branch = zero ? Branch::kZeroFlux :
(nonquadratic ? Branch::kNonquadratic : Branch::kNormal);
const double residual_threshold = zero ? kEpsilon : (nonquadratic ? kRp : kRn);
out.residual_threshold = residual_threshold;
out.residual_pass = ratio <= residual_threshold;
if (zero && *out.residual_pass) { out.field_pass = true; continue; }
if (!zero && iteration >= 1 && a == 0 && f.increment < kSmallTranslation * reference_length) {
out.field_pass = out.residual_pass;
} else {
const double correction_threshold = zero ? kZeroCorrection : kCn;
out.correction_threshold = correction_threshold;
const double bound = correction_threshold * f.increment;
if (!Nonnegative(bound)) return result;
if (iteration >= 1 && f.correction) out.correction_pass = *f.correction <= bound;
if (!zero && !nonquadratic && iteration >= 2 && f.correction &&
f.previous_residual && f.older_residual) {
const double denominator = std::min(*f.previous_residual, *f.older_residual);
if (denominator > 0) {
const double quotient = f.residual / denominator;
if (!Nonnegative(quotient)) return result;
const double estimate = quotient * *f.correction;
if (!Nonnegative(estimate)) return result;
out.estimated_correction = estimate;
out.correction_pass = out.correction_pass.value_or(false) || estimate <= bound;
}
}
out.field_pass = zero ? out.correction_pass.value_or(false) :
(*out.residual_pass && out.correction_pass.value_or(false));
}
all_pass = all_pass && *out.field_pass;
}
result.field_pass = all_pass;
result.status = Status::Ok();
return result;
}
} // namespace fesa::hht_field_convergence
+52
View File
@@ -0,0 +1,52 @@
#ifndef FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
#define FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
#include "fesa/core/status.h"
namespace fesa::hht_field_convergence {
inline constexpr const char* kPolicyId = "abaqus2025-field-on-fesa-v1";
// Ordered element rows first, then external source entries (including explicit zero).
// External entries use internal=value, inertia=0. No assembled cancellation here.
struct FluxOccurrence { double internal = 0.0; double inertia = 0.0; };
struct History {
double nonzero_sum = 0.0;
std::uint64_t nonzero_count = 0;
double maximum_sum = 0.0;
std::uint64_t maximum_count = 0;
};
struct FieldInput {
bool active = false; // At least one free full DOF of this field.
std::vector<FluxOccurrence> flux;
double residual = 0.0; // max |I+fint-F| on full free endpoint DOFs, fi=1.
double increment = 0.0; // max |q_trial-q_last_accepted| in fixed full u/phi chart.
std::optional<double> correction; // max |q_trial-q_previous_selected|, i>=1.
std::optional<double> previous_residual;
std::optional<double> older_residual;
};
enum class Branch { kNotEvaluated, kLinear, kZeroFlux, kNonquadratic, kNormal };
struct FieldResult {
std::optional<double> spatial_flux, maximum_flux, time_average_flux, residual_scale;
std::optional<double> estimated_correction, residual_threshold, correction_threshold;
std::optional<bool> residual_pass, correction_pass, field_pass;
Branch branch = Branch::kNotEvaluated;
};
struct Evaluation {
Status status = Status::Failure(FailureCategory::kSolver, {});
bool field_pass = false;
std::array<FieldResult, 2> fields;
// Pure pending snapshot: procedure owns exactly-once commit after every gate/output.
std::array<History, 2> pending;
};
// Index 0: force/translation; index 1: generalized moment/total rotation phi.
Evaluation Evaluate(const std::array<FieldInput, 2>& fields,
const std::array<History, 2>& committed,
std::size_t iteration, double reference_length);
} // namespace fesa::hht_field_convergence
#endif // FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
+1
View File
@@ -15,6 +15,7 @@ add_executable(
unit/analysis/hht_temporal_verification_test.cpp
unit/analysis/hht_dynamic_algebra_test.cpp
unit/analysis/hht_development_probe_test.cpp
unit/analysis/hht_field_convergence_test.cpp
unit/analysis/hht_reduced_shell_test.cpp
unit/analysis/hht_dynamic_procedure_test.cpp
unit/analysis/nonlinear_analysis_state_test.cpp
@@ -0,0 +1,232 @@
#include "analysis/hht_field_convergence.h"
#include <gtest/gtest.h>
#include <cmath>
#include <limits>
namespace fesa::hht_field_convergence {
namespace {
using Inputs = std::array<FieldInput, 2>;
using Histories = std::array<History, 2>;
Inputs Normal() {
Inputs f{}; f[0].active = true; f[0].flux = {{2.0, 0.0}};
f[0].residual = .01; f[0].increment = 1; f[0].correction = .01;
return f;
}
TEST(HhtFieldConvergence, SignedElementTotalAndSourcePopulationOracle) {
auto f = Normal(); f[0].flux.assign(30, {});
f[0].flux[0] = {8, -6}; f[0].flux[1] = {-2, 0};
f[0].flux[24] = {3, 0}; f[0].flux[27] = {-3, 0};
Histories h{}; h[0].maximum_sum = 10; h[0].maximum_count = 1;
auto r = Evaluate(f, h, 1, 2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux, 2.5);
r = Evaluate(f, {}, 1, 2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux, 1.0/3.0);
}
TEST(HhtFieldConvergence, NormalBoundaryAndInitialCorrectionMissing) {
auto f = Normal(); auto r = Evaluate(f, {}, 1, 2);
ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass);
f[0].residual = std::nextafter(.005 * 2, INFINITY);
r = Evaluate(f, {}, 1, 2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
f = Normal(); r = Evaluate(f, {}, 0, 2);
ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
EXPECT_FALSE(r.fields[0].correction_pass.has_value());
}
TEST(HhtFieldConvergence, ScalarPopulationThresholdAndReactivation) {
auto f=Normal(); Histories h{}; h[0].maximum_sum=100; h[0].maximum_count=1;
f[0].flux={{0,0},{.001,0},{10,0}};
auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,5.0005);
f[0].flux[2].internal=std::nextafter(10.,0.);
r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,(.001+std::nextafter(10.,0.))/3);
f[0].flux={{0,0},{0,0}}; r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,0);
}
TEST(HhtFieldConvergence, InitialAverageAndNonzeroEligibilityBoundary) {
auto f=Normal(); const double edge=1e-5*.01;
for (double x:{std::nextafter(edge,0.),edge,std::nextafter(edge,INFINITY)}) {
f[0].flux={{x,0}}; auto r=Evaluate(f,{},0,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].time_average_flux,x>edge?x:.01);
EXPECT_EQ(r.pending[0].nonzero_count,x>edge?1U:0U);
EXPECT_EQ(r.pending[0].maximum_count,1U);
}
}
TEST(HhtFieldConvergence, PendingHistoryIsRecomputedAndRollbackIsPure) {
auto f=Normal(); Histories h{}; h[0]={10,1,4,1};
f[0].flux={{30,0}}; auto a=Evaluate(f,h,1,2); ASSERT_TRUE(a.status.IsOk());
EXPECT_DOUBLE_EQ(*a.fields[0].time_average_flux,20);
f[0].flux={{70,0}}; auto b=Evaluate(f,h,2,2); ASSERT_TRUE(b.status.IsOk());
EXPECT_DOUBLE_EQ(*b.fields[0].time_average_flux,40);
EXPECT_DOUBLE_EQ(b.pending[0].nonzero_sum,80);
EXPECT_DOUBLE_EQ(b.pending[0].maximum_sum,74);
EXPECT_EQ(b.pending[0].maximum_count,2U);
EXPECT_DOUBLE_EQ(h[0].nonzero_sum,10); EXPECT_EQ(h[0].nonzero_count,1U);
f[0].flux={{0,0}}; auto z=Evaluate(f,h,1,2); ASSERT_TRUE(z.status.IsOk());
EXPECT_EQ(z.pending[0].nonzero_count,1U); EXPECT_EQ(z.pending[0].maximum_count,2U);
EXPECT_DOUBLE_EQ(z.pending[0].maximum_sum,4);
}
TEST(HhtFieldConvergence, RawScaleConversionIsSimultaneousAndUnitConsistent) {
auto f=Normal(); Histories h{}; h[0]={3,1,3,1}; h[1]={.3,1,.3,1};
f[0].flux={{3,0}}; f[1].active=true; f[1].flux={{.1,0}};
auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].residual_scale,3);
EXPECT_DOUBLE_EQ(*r.fields[1].time_average_flux,.2);
EXPECT_DOUBLE_EQ(*r.fields[1].residual_scale,6);
f[1].active=false; r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.fields[1].branch,Branch::kNotEvaluated);
EXPECT_FALSE(r.fields[1].field_pass.has_value());
}
TEST(HhtFieldConvergence, EstimatedCorrectionHasRealHistoryAndNormalOnly) {
auto f=Normal(); f[0].flux={{400,0}}; f[0].residual=2;
f[0].correction=.04; f[0].previous_residual=4; f[0].older_residual=8;
auto r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].estimated_correction,.02); EXPECT_FALSE(r.field_pass);
f[0].residual=1; r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].estimated_correction,.01); EXPECT_TRUE(r.field_pass);
f[0].older_residual=0; r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_FALSE(r.fields[0].estimated_correction.has_value()); EXPECT_FALSE(r.field_pass);
f[0].older_residual=8; r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_FALSE(r.fields[0].estimated_correction.has_value()); EXPECT_FALSE(r.field_pass);
}
TEST(HhtFieldConvergence, NonquadraticIterationAndRateBoundaries) {
auto f=Normal(); f[0].flux={{100,0}}; f[0].residual=2;
f[0].previous_residual=4; f[0].older_residual=8;
auto r=Evaluate(f,{},9,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.fields[0].branch,Branch::kNormal); EXPECT_FALSE(r.field_pass);
r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.fields[0].branch,Branch::kNonquadratic); EXPECT_TRUE(r.field_pass);
EXPECT_FALSE(r.fields[0].estimated_correction.has_value());
f[0].older_residual=16; f[0].residual=1;
r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_EQ(r.fields[0].branch,Branch::kNonquadratic);
f[0].residual=.5; r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.fields[0].branch,Branch::kNormal);
f[0].residual=4; r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.fields[0].branch,Branch::kNormal);
}
TEST(HhtFieldConvergence, ZeroFluxUsesResidualOrActualCorrectionAtBoundary) {
auto f=Normal(); Histories h{}; h[0]={2,1,2,1};
f[0].flux={{0,0}}; f[0].residual=1e-5*2; f[0].correction.reset();
auto r=Evaluate(f,h,0,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass);
EXPECT_EQ(r.fields[0].branch,Branch::kZeroFlux); EXPECT_FALSE(r.fields[0].correction_pass.has_value());
f[0].residual=std::nextafter(1e-5*2,INFINITY);
r=Evaluate(f,h,0,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
f[0].correction=.001; r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass);
f[0].correction=std::nextafter(.001,INFINITY);
r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
f[0].increment=0; f[0].correction=0;
r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass);
}
TEST(HhtFieldConvergence, LinearRequiresEveryActiveFieldAndAnActualIteration) {
auto f=Normal(); f[1]=f[0]; f[1].flux={{4,0}};
f[0].residual=2e-8; f[1].residual=4e-8; f[0].correction.reset(); f[1].correction.reset();
auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass);
EXPECT_EQ(r.fields[0].branch,Branch::kLinear); EXPECT_EQ(r.fields[1].branch,Branch::kLinear);
f[1].residual=std::nextafter(4e-8,INFINITY);
r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
EXPECT_EQ(r.fields[0].branch,Branch::kNormal);
f[1].residual=4e-8; r=Evaluate(f,{},0,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
}
TEST(HhtFieldConvergence, SmallTranslationExemptionIsStrictAndNeverRotational) {
auto f=Normal(); f[0].increment=std::nextafter(2e-8,0.); f[0].correction=1;
auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass);
EXPECT_FALSE(r.fields[0].correction_pass.has_value());
f[0].increment=2e-8; r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
f[1]=f[0]; f[0].active=false; f[1].increment=0; f[1].correction=0;
r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass);
f[1].correction=std::numeric_limits<double>::denorm_min();
r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass);
}
TEST(HhtFieldConvergence, InvalidAndOverflowNeverBecomeMissingOrPassing) {
const double inf=std::numeric_limits<double>::infinity();
const double nan=std::numeric_limits<double>::quiet_NaN();
const double max=std::numeric_limits<double>::max();
for(double x:{0.,-1.,inf,nan}) EXPECT_FALSE(Evaluate(Normal(),{},1,x).status.IsOk());
for(double x:{-1.,inf,nan}) {
auto f=Normal(); f[0].residual=x; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk());
f=Normal(); f[0].correction=x; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk());
f=Normal(); f[0].older_residual=x; EXPECT_FALSE(Evaluate(f,{},2,2).status.IsOk());
f=Normal(); f[0].flux={{x,0}};
if(x!=-1.) EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk());
}
auto f=Normal(); f[0].flux.clear(); EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk());
f=Normal(); f[0].flux={{max,max}}; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk());
f=Normal(); f[0].flux={{max,0},{max,0}}; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk());
f=Normal(); f[0].older_residual=std::numeric_limits<double>::denorm_min();
f[0].previous_residual=1; EXPECT_FALSE(Evaluate(f,{},2,2).status.IsOk());
Histories h{}; h[0]={max,1,max,1}; f=Normal(); f[0].flux={{max,0}}; EXPECT_FALSE(Evaluate(f,h,1,2).status.IsOk());
h={}; h[0].maximum_count=std::numeric_limits<std::uint64_t>::max();
EXPECT_FALSE(Evaluate(Normal(),h,1,2).status.IsOk());
h={}; h[0]={0,1,0,1}; f=Normal(); f[0].flux={{0,0}};
EXPECT_FALSE(Evaluate(f,h,1,2).status.IsOk());
f=Normal(); f[0].residual=1e200; f[0].correction=1e200;
f[0].older_residual=1; f[0].previous_residual=1;
const auto overflow=Evaluate(f,{},2,2);
EXPECT_FALSE(overflow.status.IsOk()); EXPECT_FALSE(overflow.field_pass);
f=Normal(); f[1]=f[0]; f[0].flux={{max,0}};
EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk());
Inputs inactive{}; auto r=Evaluate(inactive,{},0,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_TRUE(r.field_pass); EXPECT_FALSE(r.fields[0].field_pass.has_value());
}
TEST(HhtFieldConvergence, DecisionThresholdsBelowEqualAboveWithoutTolerance) {
auto f=Normal();
for(double x:{std::nextafter(.01,0.),.01,std::nextafter(.01,INFINITY)}) {
f[0].correction=x; auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.field_pass,x<=.01);
}
for(double x:{std::nextafter(.01,0.),.01,std::nextafter(.01,INFINITY)}) {
f=Normal(); f[0].residual=x; auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.field_pass,x<=.01);
}
for(double x:{std::nextafter(2.,0.),2.,std::nextafter(2.,INFINITY)}) {
f=Normal(); f[0].flux={{100,0}}; f[0].residual=x;
f[0].previous_residual=4; f[0].older_residual=8;
auto r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk());
// For below 2 the detector selects normal; equality/above selects Rp.
EXPECT_EQ(r.field_pass,x==2.);
}
}
TEST(HhtFieldConvergence, MixedIncidenceRetainsEveryElementScalarOccurrence) {
auto f=Normal(); f[0].flux={{6,0},{-2,0},{-1,0},{9,0},{0,0}};
// Three occurrences can share one global DOF and one belong to another.
// Node averaging would produce 6, signed assembly would produce 3 and 9.
auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,3.6);
EXPECT_DOUBLE_EQ(*r.fields[0].maximum_flux,9);
}
TEST(HhtFieldConvergence, RemainingThresholdsHaveAdjacentRepresentableBoundaries) {
for (double x:{std::nextafter(.001,0.),.001,std::nextafter(.001,INFINITY)}) {
auto f=Normal(); Histories h{}; h[0]={2,1,2,1};
f[0].flux={{0,0}}; f[0].residual=.1; f[0].correction=x;
auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.field_pass,x<=.001);
}
for (double x:{std::nextafter(2e-5,0.),2e-5,std::nextafter(2e-5,INFINITY)}) {
auto f=Normal(); Histories h{}; h[0]={2,1,2,1};
f[0].flux={{0,0}}; f[0].residual=x; f[0].correction.reset();
auto r=Evaluate(f,h,0,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.field_pass,x<=2e-5);
}
for (double x:{std::nextafter(2e-8,0.),2e-8,std::nextafter(2e-8,INFINITY)}) {
auto f=Normal(); f[0].increment=x; f[0].correction=1;
auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.field_pass,x<2e-8);
f=Normal(); f[0].residual=x; f[0].correction.reset();
r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.fields[0].branch,x<=2e-8?Branch::kLinear:Branch::kNormal);
}
for (double x:{std::nextafter(.001,0.),.001,std::nextafter(.001,INFINITY)}) {
auto f=Normal(); Histories h{}; h[0]={0,0,100,1};
f[0].flux={{0,0},{x,0},{10,0}};
auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,x<.001?10:(10+x)/2);
}
for (double x:{std::nextafter(2.,0.),2.,std::nextafter(2.,INFINITY)}) {
auto f=Normal(); f[0].flux={{100,0}}; f[0].residual=x;
f[0].previous_residual=4; f[0].older_residual=16;
auto r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk());
EXPECT_EQ(r.fields[0].branch,Branch::kNonquadratic);
EXPECT_EQ(r.field_pass,x<=2);
}
}
} // namespace
} // namespace fesa::hht_field_convergence