XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class ScoThermalPredictorTests
{
    private static ScoThermalInput Model=>new(){Verified=true,HeatPercent=90,HeatCapacity=100,HeatDisplayScale=1,
        ScoInputUpper=50,OtherInputUpper=2,CoolingPassive=10,CoolingMaximum=10,CoolingScale=1,CoolingHeatLower=0,CoolingHeatUpper=1,
        LeaseRemainingSeconds=.2,ResponseDelaySeconds=.01,AfterStopSeconds=.2};
    private static void Near(double expected,double actual,double error=1e-7)=>Check.That(Math.Abs(expected-actual)<=error,$"Expected {expected}, got {actual}.");

    [Test]public void TurningScoOffRemovesOnlyItsFutureHeatSource()
    {
        var forecast=ScoThermalPredictor.Predict(Model);
        Check.That(forecast.Known&&!forecast.ShouldStop);Near(42,forecast.ScoOnNetRisePercentPerSecond);Near(-8,forecast.ScoOffNetRisePercentPerSecond);
        Near(98.82,forecast.ContinueUntilDeadlinePeakPercent);Near(90.42,forecast.ImmediateStopPeakPercent);
        Near(87.22,forecast.ImmediateStopEndPercent);Near(97.22,forecast.DeadlineStopEndPercent);
    }
    [Test]public void ExistingStoredHeatIsNotErasedWhenTheSourceStops()
    {
        var forecast=ScoThermalPredictor.Predict(Model with{HeatPercent=99,ResponseDelaySeconds=0});
        Check.That(forecast.Known&&forecast.ShouldStop);Near(99,forecast.ImmediateStopPeakPercent);
        Check.That(forecast.ImmediateStopEndPercent>90&&forecast.ImmediateStopEndPercent<99);
    }
    [Test]public void OtherEnvironmentalOrUnattributedSourcesRemainAfterScoStops()
    {
        var forecast=ScoThermalPredictor.Predict(Model with{OtherInputUpper=20});
        Check.That(forecast.Known);Near(10,forecast.ScoOffNetRisePercentPerSecond);
        Check.That(forecast.ImmediateStopEndPercent>Model.HeatPercent);
    }
    [Test]public void OtherSourcesCanReach100AfterTheDeadlineHasAlreadyRemovedSco()
    {
        var forecast=ScoThermalPredictor.Predict(Model with{HeatPercent=98,ScoInputUpper=2,OtherInputUpper=12,
            LeaseRemainingSeconds=.2,ResponseDelaySeconds=0,AfterStopSeconds=1});
        Check.That(forecast.Known&&forecast.ShouldStop);Near(98.8,forecast.ContinueUntilDeadlinePeakPercent);
        Near(100.8,forecast.DeadlineStopPeakPercent);Near(100.4,forecast.ImmediateStopPeakPercent);
        Check.Equal("other-source-overheat-after-deadline",forecast.Reason);
    }
    [Test]public void ThresholdUsesActualLeaseAndDelayRatherThanTwoSecondsOfHeating()
    {
        var shortLease=ScoThermalPredictor.Predict(Model with{HeatPercent=80,LeaseRemainingSeconds=.02,ResponseDelaySeconds=.005});
        Check.That(shortLease.Known&&!shortLease.ShouldStop);Near(81.05,shortLease.ContinueUntilDeadlinePeakPercent);
        var nearLimit=ScoThermalPredictor.Predict(Model with{HeatPercent=99});
        Check.That(nearLimit.Known&&nearLimit.ShouldStop);Near(.21,nearLimit.StopHorizonSeconds);
    }
    [Test]public void HudScaleAndHeatCapacityDetermineNativeEnergyConversion()
    {
        var forecast=ScoThermalPredictor.Predict(Model with{HeatCapacity=289,HeatDisplayScale=1.5});
        Check.That(forecast.Known);Near(42*100/(289*1.5),forecast.ScoOnNetRisePercentPerSecond);
        Near(-8*100/(289*1.5),forecast.ScoOffNetRisePercentPerSecond);
    }
    [Test]public void SourceAgeDoesNotDoubleCountHeatWhenTheHeatValueIsCurrent()
    {
        var current=ScoThermalPredictor.Predict(Model);
        var olderSources=ScoThermalPredictor.Predict(Model with{SourceAgeSeconds=.19});
        Near(current.ContinueUntilDeadlinePeakPercent,olderSources.ContinueUntilDeadlinePeakPercent);
        var olderHeat=ScoThermalPredictor.Predict(Model with{HeatAgeSeconds=.1});
        Check.That(olderHeat.ContinueUntilDeadlinePeakPercent>current.ContinueUntilDeadlinePeakPercent);
    }
    [Test]public void UnknownOrExpiredInputsCannotAuthorizeSco()
    {
        foreach(var value in new[]{Model with{Verified=false},Model with{SourceAgeSeconds=.201},Model with{HeatAgeSeconds=.201},
            Model with{LeaseRemainingSeconds=.251},Model with{LeaseRemainingSeconds=0},Model with{CoolingMaximum=double.NaN},Model with{HeatCapacity=0}})
        {var forecast=ScoThermalPredictor.Predict(value);Check.That(!forecast.Known&&forecast.ShouldStop);}
    }
    [Test]public void QuadraticCoolingUpperEndTracksAnIndependentExactSolution()
    {
        var forecast=ScoThermalPredictor.Predict(Model with{HeatPercent=80,ScoInputUpper=0,OtherInputUpper=0,CoolingPassive=0,CoolingMaximum=100,
            LeaseRemainingSeconds=.2,ResponseDelaySeconds=0,AfterStopSeconds=.2});
        // H'=-.01 H^2, H(t)=H0/(1+.01 H0 t). Implicit Euler must remain above the true cooling trajectory.
        var exact=80/(1+.01*80*.4);
        Check.That(forecast.Known&&!forecast.ShouldStop);Near(80,forecast.ImmediateStopPeakPercent);
        Check.That(forecast.ImmediateStopEndPercent>=exact&&forecast.ImmediateStopEndPercent<exact+.2);
    }
    [Test]public void TimeTo100IsAConservativeLowerBoundUnderTheSuppliedEnvelope()
    {
        var constant=ScoThermalPredictor.Predict(Model);Near(10d/42,constant.SecondsTo100LowerBound!.Value);
        var equilibriumBelowLimit=ScoThermalPredictor.Predict(Model with{ScoInputUpper=0,OtherInputUpper=2,CoolingMaximum=100});
        Check.That(equilibriumBelowLimit.Known&&equilibriumBelowLimit.SecondsTo100LowerBound is null);
    }
    [Test]public void CurrentSimulationStepCannotSpendCoolingThatHasNotRunYet()
    {
        var model=Model with{HeatPercent=98,ScoInputUpper=100,OtherInputUpper=0,CoolingPassive=100,CoolingMaximum=100,
            LeaseRemainingSeconds=.001,ResponseDelaySeconds=.03,AfterStopSeconds=0};
        var continuous=ScoThermalPredictor.Predict(model);Check.That(continuous.Known&&!continuous.ShouldStop);Near(98,continuous.ContinueUntilDeadlinePeakPercent);
        Check.That(ScoThermalPredictor.TryBoundCurrentSimulationStep(model,.03,out var gross));Near(101,gross);
    }
    [Test]public void ActualSimulationStepAndCurrentStoredHeatAreIndependentOfUtcLeaseLength()
    {
        var model=Model with{HeatPercent=97,HeatCapacity=289,HeatDisplayScale=1.5,ScoInputUpper=100,OtherInputUpper=10,LeaseRemainingSeconds=.001};
        Check.That(ScoThermalPredictor.TryBoundCurrentSimulationStep(model,.25,out var peak));Near(97+110*.25*100/(289*1.5),peak);
        foreach(var dt in new[]{0,double.NaN,-.01,.251})Check.That(!ScoThermalPredictor.TryBoundCurrentSimulationStep(model,dt,out _));
        Check.That(!ScoThermalPredictor.TryBoundCurrentSimulationStep(model with{SourceAgeSeconds=.201},.01,out _));
    }
}

[TestFixture]
public class ScoThermalSourceLedgerTests
{
    private static void Near(double expected,double actual)=>Check.That(Math.Abs(expected-actual)<1e-7,$"Expected {expected}, got {actual}.");
    [Test]public void AccountsSourcesInSimulationTimeAndDoesNotCallUnknownInputAmbient()
    {
        var ledger=new ScoThermalSourceLedger();ledger.ObserveTick(.02,1000);
        for(ulong index=1;index<=5;index++)
        {ledger.ObserveInput(1,"FSD Overcharge",2,true);ledger.ObserveInput(2,"Power source",.4,false);ledger.ObserveInput(0,"",.2,false);ledger.ObserveTick(.02,1000+index*7);}
        var value=ledger.Read(1035);Check.That(value.Ready&&value.Sources.Count==3);Near(.1,value.WindowSimulationSeconds);
        Near(100,value.ScoInputUpper);Near(30,value.OtherInputUpper);Near(130,value.TotalRequestedEnergyPerSecond);
        Check.That(value.Sources.Single(row=>row.SourceId==0).Label=="Unidentified source");
    }
    [Test]public void FirstPartialIntervalAndNonMonotonicClockCannotCreateAHeatSpike()
    {
        var ledger=new ScoThermalSourceLedger();ledger.ObserveInput(1,"SCO",100,true);ledger.ObserveTick(.01,1000);
        Check.That(!ledger.Read(1000).Ready);ledger.ObserveInput(1,"SCO",1,true);ledger.ObserveTick(.1,1100);
        Near(10,ledger.Read(1100).ScoInputUpper);
        ledger.ObserveTick(.01,1099);Check.That(!ledger.Read(1099).Ready);Near(0,ledger.Read(1099).ScoInputUpper);
    }
    [Test]public void NewSourceIsIncludedImmediatelyDuringAnIncompleteWindow()
    {
        var ledger=new ScoThermalSourceLedger();ledger.ObserveTick(.02,1000);ledger.ObserveTick(.1,1100);
        ledger.ObserveInput(2,"new source",4,false);ledger.ObserveTick(.02,1120);
        var value=ledger.Read(1120);Check.That(value.Ready);Near(200,value.OtherInputUpper);
    }
    [Test]public void AHistoricalPeakExpiresDuringAPartialWindowWithoutWaitingForTheNextBucket()
    {
        var ledger=new ScoThermalSourceLedger();ledger.ObserveTick(.02,1000);
        ledger.ObserveInput(1,"SCO",10,true);ledger.ObserveTick(.1,1100);
        ledger.ObserveInput(1,"SCO",5,true);ledger.ObserveTick(.1,1200);
        ledger.ObserveTick(.1,1300);ledger.ObserveTick(.02,1320);
        Near(50,ledger.Read(1320).ScoInputUpper);
    }
    [Test]public void RetainsNegativeRequestsButNeverUsesThemAsUnverifiedFutureCooling()
    {
        var ledger=new ScoThermalSourceLedger();ledger.ObserveTick(.02,1000);ledger.ObserveInput(3,"negative requested input",-4,false);ledger.ObserveTick(.1,1100);
        var value=ledger.Read(1100);Check.That(value.Ready);Near(-40,value.TotalRequestedEnergyPerSecond);Near(0,value.OtherInputUpper);
    }
    [Test]public void ExpiredSnapshotsAndInvalidDtRequireANewCompleteWindow()
    {
        var ledger=new ScoThermalSourceLedger();ledger.ObserveTick(.02,1000);ledger.ObserveTick(.1,1100);
        Check.That(ledger.Read(1300).Ready&&!ledger.Read(1301).Ready);
        ledger.ObserveTick(.3,1400);Check.That(!ledger.Read(1400).Ready);ledger.ObserveTick(.01,1410);ledger.ObserveTick(.1,1510);
        Check.That(ledger.Read(1510).Ready);
    }
}