diff --git a/msvc/fesa_solver.vcxproj b/msvc/fesa_solver.vcxproj index 3ab073a..9d64409 100644 --- a/msvc/fesa_solver.vcxproj +++ b/msvc/fesa_solver.vcxproj @@ -16,6 +16,7 @@ + diff --git a/msvc/fesa_unit_tests.vcxproj b/msvc/fesa_unit_tests.vcxproj index 80e95ee..21ccaa7 100644 --- a/msvc/fesa_unit_tests.vcxproj +++ b/msvc/fesa_unit_tests.vcxproj @@ -21,6 +21,7 @@ + diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index 804c94a..7f70e0b 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -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 diff --git a/src/fesa/analysis/hht_field_convergence.cpp b/src/fesa/analysis/hht_field_convergence.cpp new file mode 100644 index 0000000..c887db0 --- /dev/null +++ b/src/fesa/analysis/hht_field_convergence.cpp @@ -0,0 +1,166 @@ +#include "analysis/hht_field_convergence.h" + +#include +#include +#include + +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& 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& fields, + const std::array& 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(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(count); + out.spatial_flux = spatial; + if (h.nonzero_count == std::numeric_limits::max() || + h.maximum_count == std::numeric_limits::max()) return result; + const double base = h.nonzero_count == 0 ? .01 : + h.nonzero_sum / static_cast(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(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 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 diff --git a/src/fesa/analysis/hht_field_convergence.h b/src/fesa/analysis/hht_field_convergence.h new file mode 100644 index 0000000..e4123c6 --- /dev/null +++ b/src/fesa/analysis/hht_field_convergence.h @@ -0,0 +1,52 @@ +#ifndef FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_ +#define FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_ + +#include +#include +#include +#include +#include + +#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 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 correction; // max |q_trial-q_previous_selected|, i>=1. + std::optional previous_residual; + std::optional older_residual; +}; +enum class Branch { kNotEvaluated, kLinear, kZeroFlux, kNonquadratic, kNormal }; +struct FieldResult { + std::optional spatial_flux, maximum_flux, time_average_flux, residual_scale; + std::optional estimated_correction, residual_threshold, correction_threshold; + std::optional residual_pass, correction_pass, field_pass; + Branch branch = Branch::kNotEvaluated; +}; +struct Evaluation { + Status status = Status::Failure(FailureCategory::kSolver, {}); + bool field_pass = false; + std::array fields; + // Pure pending snapshot: procedure owns exactly-once commit after every gate/output. + std::array pending; +}; +// Index 0: force/translation; index 1: generalized moment/total rotation phi. +Evaluation Evaluate(const std::array& fields, + const std::array& committed, + std::size_t iteration, double reference_length); +} // namespace fesa::hht_field_convergence + +#endif // FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1e77eac..9c4a638 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 diff --git a/tests/unit/analysis/hht_field_convergence_test.cpp b/tests/unit/analysis/hht_field_convergence_test.cpp new file mode 100644 index 0000000..fd7b8b8 --- /dev/null +++ b/tests/unit/analysis/hht_field_convergence_test.cpp @@ -0,0 +1,232 @@ +#include "analysis/hht_field_convergence.h" +#include +#include +#include +namespace fesa::hht_field_convergence { +namespace { +using Inputs = std::array; +using Histories = std::array; +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::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::infinity(); + const double nan=std::numeric_limits::quiet_NaN(); + const double max=std::numeric_limits::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::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::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