diff --git a/tests/unit/analysis/hht_field_convergence_test.cpp b/tests/unit/analysis/hht_field_convergence_test.cpp index fd7b8b8..cdab714 100644 --- a/tests/unit/analysis/hht_field_convergence_test.cpp +++ b/tests/unit/analysis/hht_field_convergence_test.cpp @@ -228,5 +228,98 @@ TEST(HhtFieldConvergence, RemainingThresholdsHaveAdjacentRepresentableBoundaries EXPECT_EQ(r.field_pass,x<=2); } } +TEST(HhtFieldConvergence, CurrentIncludedEligibilityBoundaryPreservesPendingNonzeroHistory) { + Histories h{}; h[0]={199999,1,199999,1}; + // 199999+1 rounds exactly to 200000; current-included mean is 100000. + // Its epsilon threshold is exactly 1, while the old-base threshold is 1.99999. + for (double b:{std::nextafter(1.,0.),1.,std::nextafter(1.,INFINITY)}) { + auto f=Normal(); f[0].flux={{b,0}}; + const auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].spatial_flux,b); + EXPECT_EQ(*r.fields[0].time_average_flux,b>1?100000:199999); + EXPECT_EQ(r.pending[0].nonzero_sum,b>1?200000:199999); + EXPECT_EQ(r.pending[0].nonzero_count,b>1?2U:1U); + EXPECT_EQ(h[0].nonzero_sum,199999); + EXPECT_EQ(h[0].nonzero_count,1U); + } + auto f=Normal(); f[0].flux={{1.5,0}}; + const auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].time_average_flux,100000.25); + EXPECT_EQ(r.pending[0].nonzero_sum,200000.5); + EXPECT_EQ(r.pending[0].nonzero_count,2U); +} +TEST(HhtFieldConvergence, ConversionSpatialPrerequisiteHasAdjacentBoundaries) { + Histories h{}; h[0]={1023,1023,1023,1023}; h[1]={4,1,4,1}; + // Adding any adjacent-to-one b to 1023 rounds to 1024: raw force stays 1. + // Peer raw moment stays 4 and L=1 makes the candidate force scale exactly 4. + for (double b:{std::nextafter(1.,0.),1.,std::nextafter(1.,INFINITY)}) { + auto f=Normal(); f[0].flux={{b,0}}; f[1]=f[0]; f[1].flux={{4,0}}; + const auto r=Evaluate(f,h,1,1); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].spatial_flux,b); + EXPECT_EQ(*r.fields[0].time_average_flux,1); + EXPECT_EQ(*r.fields[1].time_average_flux,4); + EXPECT_EQ(*r.fields[0].residual_scale,b<=1?4:1); + EXPECT_EQ(r.pending[0].nonzero_sum,1024); + EXPECT_EQ(r.pending[0].nonzero_count,1024U); + EXPECT_EQ(r.pending[1].nonzero_sum,8); + EXPECT_EQ(r.pending[1].nonzero_count,2U); + EXPECT_EQ(h[0].nonzero_sum,1023); + EXPECT_EQ(h[0].nonzero_count,1023U); + EXPECT_EQ(h[1].nonzero_sum,4); + } +} +TEST(HhtFieldConvergence, ConversionRawVersusConvertedScaleHasAdjacentBoundaries) { + Histories h{}; h[0]={1,1,1,1}; h[1]=h[0]; + // Zero samples preserve both raw means at 1. Only L changes convF=1/L. + for (double length:{std::nextafter(1.,0.),1.,std::nextafter(1.,INFINITY)}) { + auto f=Normal(); f[0].flux={{0,0}}; f[1]=f[0]; + const auto r=Evaluate(f,h,1,length); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].time_average_flux,1); + EXPECT_EQ(*r.fields[1].time_average_flux,1); + // Reciprocating the adjacent value below 1 rounds to the adjacent value above 1. + EXPECT_EQ(*r.fields[0].residual_scale, + length<1?std::nextafter(1.,INFINITY):1); + for (std::size_t a=0; a<2; ++a) { + EXPECT_EQ(r.pending[a].nonzero_sum,1); + EXPECT_EQ(r.pending[a].nonzero_count,1U); + EXPECT_EQ(h[a].nonzero_sum,1); + EXPECT_EQ(h[a].nonzero_count,1U); + } + } +} +TEST(HhtFieldConvergence, ZeroFluxClassificationTestsSpatialFluxBoundary) { + // A peer raw mean of 1 converts the small force field to a fixed scale of 1. + // Vary bbar, not rho, across epsilon*scale; rho=.1 prevents the linear exemption. + for (double b:{std::nextafter(1e-5,0.),1e-5,std::nextafter(1e-5,INFINITY)}) { + auto f=Normal(); f[0].flux={{b,0}}; f[0].residual=.1; + f[1]=f[0]; f[1].flux={{1,0}}; + const auto r=Evaluate(f,{},1,1); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].spatial_flux,b); + EXPECT_EQ(*r.fields[0].residual_scale,1); + EXPECT_EQ(r.fields[0].branch,b<=1e-5?Branch::kZeroFlux:Branch::kNormal); + } +} +TEST(HhtFieldConvergence, EstimatedCorrectionAdjacentBoundariesDriveNormalCorrectionPass) { + for (double estimate:{std::nextafter(.01,0.),.01,std::nextafter(.01,INFINITY)}) { + auto f=Normal(); f[0].flux={{400,0}}; f[0].residual=1; + // Prior residual minimum is 4; power-of-two scaling preserves adjacent bits. + // The actual correction exceeds .01 in all three cases, isolating the estimate. + f[0].correction=4*estimate; f[0].previous_residual=4; f[0].older_residual=8; + const auto r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,Branch::kNormal); + ASSERT_GT(*f[0].correction,.01); + EXPECT_EQ(*r.fields[0].estimated_correction,estimate); + EXPECT_EQ(*r.fields[0].correction_pass,estimate<=.01); + EXPECT_EQ(r.field_pass,estimate<=.01); + } +} +TEST(HhtFieldConvergence, PopulationReactivationThresholdImmediatelyAboveUsesFilteredPopulation) { + auto f=Normal(); Histories h{}; h[0]={0,0,100,1}; + const double maximum=std::nextafter(10.,INFINITY); + f[0].flux={{0,0},{.001,0},{maximum,0}}; + const auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].maximum_flux,maximum); + EXPECT_EQ(*r.fields[0].spatial_flux,(10.000000000000002+.001)/2); +} } // namespace } // namespace fesa::hht_field_convergence