using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class ScoHeatSampleHistoryTests
{
private static void Near(double expected,double actual)=>Check.That(Math.Abs(expected-actual)<1e-7);
[Test]public void FastCallbacksAccumulateEnoughRealTimeForAHeatSlope()
{
var history=new ScoHeatSampleHistory();
Check.That(!history.Observe(1000,40)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1004,40.04)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1008,40.08)&&double.IsNaN(history.Slope));
Check.That(history.Observe(1012,40.12));Near(10,history.Slope);
Check.That(!history.Observe(1016,40.16));Near(10,history.Slope);
Check.That(!history.Observe(1020,40.20));
Check.That(history.Observe(1024,40.24));Near(10,history.Slope);
}
[Test]public void DuplicateTimestampDoesNotReplaceTheDerivativeBaseline()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);
Check.That(!history.Observe(1000,41));
Check.That(history.Observe(1010,40.2));Near(20,history.Slope);
Check.That(!history.Observe(1010,50));Near(20,history.Slope);
Check.That(history.Observe(1030,40.4));Near(10,history.Slope);
}
[Test]public void ClockRollbackRequiresANewValidInterval()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1020,40.2);
Check.That(!history.Observe(1019,41)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1024,41.05)&&double.IsNaN(history.Slope));
Check.That(history.Observe(1029,41.1));Near(10,history.Slope);
}
[Test]public void LongGapCannotReuseThePreviousSlope()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1020,40.2);
Check.That(!history.Observe(2021,50)&&double.IsNaN(history.Slope));
Check.That(history.Observe(2031,50.1));Near(10,history.Slope);
Check.That(history.Observe(3031,49.1));Near(-1,history.Slope);
}
[Test]public void InvalidHeatAndExplicitResetRequireFreshSamples()
{
foreach(var heat in new[]{double.NaN,double.PositiveInfinity,-1d})
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1020,40.2);
Check.That(!history.Observe(1021,heat)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1030,40.3)&&double.IsNaN(history.Slope));
Check.That(history.Observe(1040,40.4));Near(10,history.Slope);
history.Reset();Check.That(double.IsNaN(history.Slope));
Check.That(!history.Observe(1050,40.5));
}
}
[Test]public void DerivativePreservesRapidHeatingAndCoolingForTheGuard()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);
Check.That(history.Observe(1010,42));Near(200,history.Slope);
Check.That(history.Observe(1030,41));Near(-50,history.Slope);
}
[Test]public void LowerPositiveSlopesCannotKeepAnOldPeakAliveBeyondItsWindow()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1010,40.2);
Near(20,history.ConservativeSlope(1010));Check.Equal(1260ul,history.RecentPeakExpiresAt);
history.Observe(1030,40.4);history.Observe(1240,42.5);
Near(10,history.Slope);Near(20,history.ConservativeSlope(1240));Check.Equal(1260ul,history.RecentPeakExpiresAt);
history.Observe(1261,42.71);Near(10,history.ConservativeSlope(1261));Check.Equal(1511ul,history.RecentPeakExpiresAt);
}
[Test]public void SamePhaseFrameIntervalsMeasureTheWholeSimulationHeatStep()
{
// A 0.63 point update across 63 ms is 10 points/s. A second read only
// 10 ms away from an update must not be mixed into this phase's history.
var history=new ScoHeatSampleHistory();history.Observe(1000,40);
history.Observe(1063,40.63);history.Observe(1126,41.26);
Near(10,history.Slope);Check.Equal(1063ul,history.SlopeStartAt);Check.Equal(1126ul,history.SlopeEndAt);
Near(40.63,history.SlopeStartHeat);Near(41.26,history.SlopeEndHeat);
Near(64.26,41.26+Math.Max(0,history.ConservativeSlope(1126))*2+3);
}
[Test]public void ExpiringHighestPeakPreservesTheNextPeakStillInsideTheWindow()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1010,40.2);
history.Observe(1110,41.7);history.Observe(1210,42.7);history.Observe(1270,43.3);
Near(15,history.ConservativeSlope(1270));Check.Equal(1360ul,history.RecentPeakExpiresAt);
Near(10,history.ConservativeSlope(1361));
}
[Test]public void ResetAndClockDiscontinuityDiscardThePeakAndItsEvidence()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1010,42);
Near(200,history.ConservativeSlope(1010));
history.Observe(1009,42);
Check.That(double.IsNaN(history.ConservativeSlope(1009))&&double.IsNaN(history.SlopeStartHeat));
Check.Equal(0ul,history.RecentPeakExpiresAt);Check.Equal(0ul,history.SlopeEndAt);
}
}
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class ScoHeatSampleHistoryTests
{
private static void Near(double expected,double actual)=>Check.That(Math.Abs(expected-actual)<1e-7);
[Test]public void FastCallbacksAccumulateEnoughRealTimeForAHeatSlope()
{
var history=new ScoHeatSampleHistory();
Check.That(!history.Observe(1000,40)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1004,40.04)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1008,40.08)&&double.IsNaN(history.Slope));
Check.That(history.Observe(1012,40.12));Near(10,history.Slope);
Check.That(!history.Observe(1016,40.16));Near(10,history.Slope);
Check.That(!history.Observe(1020,40.20));
Check.That(history.Observe(1024,40.24));Near(10,history.Slope);
}
[Test]public void DuplicateTimestampDoesNotReplaceTheDerivativeBaseline()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);
Check.That(!history.Observe(1000,41));
Check.That(history.Observe(1010,40.2));Near(20,history.Slope);
Check.That(!history.Observe(1010,50));Near(20,history.Slope);
Check.That(history.Observe(1030,40.4));Near(10,history.Slope);
}
[Test]public void ClockRollbackRequiresANewValidInterval()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1020,40.2);
Check.That(!history.Observe(1019,41)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1024,41.05)&&double.IsNaN(history.Slope));
Check.That(history.Observe(1029,41.1));Near(10,history.Slope);
}
[Test]public void LongGapCannotReuseThePreviousSlope()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1020,40.2);
Check.That(!history.Observe(2021,50)&&double.IsNaN(history.Slope));
Check.That(history.Observe(2031,50.1));Near(10,history.Slope);
Check.That(history.Observe(3031,49.1));Near(-1,history.Slope);
}
[Test]public void InvalidHeatAndExplicitResetRequireFreshSamples()
{
foreach(var heat in new[]{double.NaN,double.PositiveInfinity,-1d})
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1020,40.2);
Check.That(!history.Observe(1021,heat)&&double.IsNaN(history.Slope));
Check.That(!history.Observe(1030,40.3)&&double.IsNaN(history.Slope));
Check.That(history.Observe(1040,40.4));Near(10,history.Slope);
history.Reset();Check.That(double.IsNaN(history.Slope));
Check.That(!history.Observe(1050,40.5));
}
}
[Test]public void DerivativePreservesRapidHeatingAndCoolingForTheGuard()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);
Check.That(history.Observe(1010,42));Near(200,history.Slope);
Check.That(history.Observe(1030,41));Near(-50,history.Slope);
}
[Test]public void LowerPositiveSlopesCannotKeepAnOldPeakAliveBeyondItsWindow()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1010,40.2);
Near(20,history.ConservativeSlope(1010));Check.Equal(1260ul,history.RecentPeakExpiresAt);
history.Observe(1030,40.4);history.Observe(1240,42.5);
Near(10,history.Slope);Near(20,history.ConservativeSlope(1240));Check.Equal(1260ul,history.RecentPeakExpiresAt);
history.Observe(1261,42.71);Near(10,history.ConservativeSlope(1261));Check.Equal(1511ul,history.RecentPeakExpiresAt);
}
[Test]public void SamePhaseFrameIntervalsMeasureTheWholeSimulationHeatStep()
{
// A 0.63 point update across 63 ms is 10 points/s. A second read only
// 10 ms away from an update must not be mixed into this phase's history.
var history=new ScoHeatSampleHistory();history.Observe(1000,40);
history.Observe(1063,40.63);history.Observe(1126,41.26);
Near(10,history.Slope);Check.Equal(1063ul,history.SlopeStartAt);Check.Equal(1126ul,history.SlopeEndAt);
Near(40.63,history.SlopeStartHeat);Near(41.26,history.SlopeEndHeat);
Near(64.26,41.26+Math.Max(0,history.ConservativeSlope(1126))*2+3);
}
[Test]public void ExpiringHighestPeakPreservesTheNextPeakStillInsideTheWindow()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1010,40.2);
history.Observe(1110,41.7);history.Observe(1210,42.7);history.Observe(1270,43.3);
Near(15,history.ConservativeSlope(1270));Check.Equal(1360ul,history.RecentPeakExpiresAt);
Near(10,history.ConservativeSlope(1361));
}
[Test]public void ResetAndClockDiscontinuityDiscardThePeakAndItsEvidence()
{
var history=new ScoHeatSampleHistory();history.Observe(1000,40);history.Observe(1010,42);
Near(200,history.ConservativeSlope(1010));
history.Observe(1009,42);
Check.That(double.IsNaN(history.ConservativeSlope(1009))&&double.IsNaN(history.SlopeStartHeat));
Check.Equal(0ul,history.RecentPeakExpiresAt);Check.Equal(0ul,history.SlopeEndAt);
}
}