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.ImmediateStopEndPercentCheck.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); } }