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;
using System.Text.Json;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class ScoBrakingPredictorTests
{
    private static ScoExitEstimateInput ExitInput=>new()
    {
        OriginalParametersVerified=true,ForwardServoDomainVerified=true,OrdinaryArrivalControllerVerified=true,
        OriginalBrakeScalingMode=2,RemainingDistanceMetres=10000,CurrentSpeedMetresPerSecond=400,
        CurrentOrdinarySpeedLimitMetresPerSecond=40,OriginalBaseMaximumSpeedMetresPerSecond=1000,
        OriginalBaseBrakeAcceleration=150,OriginalResponseTimeSeconds=1,OriginalJerkMetresPerSecondCubed=100,
        OriginalForwardTargetBiasMetresPerSecond=10,ForwardThrottle=.75,StopResponseSecondsUpper=0,
        ActiveAccelerationUpper=0,EstimatedCoolingAndAssistSettleSeconds=1.5,MaximumPhysicsStepSeconds=.01,
        AdditionalResponseMarginSeconds=.1,MinimumOrdinaryHandoffSpeedMetresPerSecond=5,
        OriginalArrivalWindowSeconds=6,RelativeDistanceMargin=.01,DistanceUncertaintyMetres=2,SafeDropDistanceMetres=1
    };
    private static ScoServoParameters Servo=>new()
    {
        PinnedSourceDomainVerified=true,BaseMaximumSpeedMetresPerSecond=1000,BaseForwardAcceleration=75,
        BaseBrakeAcceleration=150,ResponseTimeSeconds=1,JerkMetresPerSecondCubed=10000,
        ForwardTargetBiasMetresPerSecond=10,ForwardThrottle=.75
    };
    private static ScoServoStepInput ServoStep=>new()
    {
        SpeedMetresPerSecond=400,OrdinaryPhysicsMaximumSpeedMetresPerSecond=40,PreviousServoAccelerationNorm=1000,
        FlightAssistBlend=1,Cooling=false,CoolingDragPerSecond=.36,NativeStepSeconds=.01
    };
    [Test]public void EstimatedBudgetIsExplicitlyUnqualifiedAndGivesCoolingNoBrakeCredit()
    {
        var first=ScoBrakingPredictor.EstimateExit(ExitInput);
        var longer=ScoBrakingPredictor.EstimateExit(ExitInput with{EstimatedCoolingAndAssistSettleSeconds=2});
        Check.That(first.Known&&first.Estimated&&!first.QualifiedForControl&&!first.CoolingDragUsed&&first.RequiresActualOrdinaryHandoff);
        Check.That(Math.Abs(longer.EstimatedStopDistanceMetres-first.EstimatedStopDistanceMetres-200)<1e-9);
        Check.That(first.EstimatedStopDistanceMetres<400*13);
        Check.That(!ScoBrakingPredictor.EstimateExit(ExitInput with{RemainingDistanceMetres=first.EstimatedStopDistanceMetres}).RemainingDistanceExceedsEstimate);
    }
    [Test]public void NativeModeTwoLowerLawDoesNotUseAnUnconditionalQuarterBrake()
    {
        var result=ScoBrakingPredictor.StepOriginalServo(Servo,ServoStep);
        var clamp=150*.4*Math.Sqrt(.4/.04);
        Check.That(result.Known&&!result.QualifiedForControl);
        Check.That(Math.Abs(result.AppliedAcceleration+clamp)<1e-10);
        Check.Equal(32.5d,result.TargetSpeedMetresPerSecond);
        Check.Equal(.5d,result.EffectiveResponseTimeSeconds);
        Check.That(Math.Abs(result.NextSpeedMetresPerSecond-(400-clamp*.01))<1e-10);
    }
    [Test]public void OriginalJerkRampsFromTheActualPriorNormPerPhysicsStep()
    {
        var parameters=Servo with{JerkMetresPerSecondCubed=100};
        var first=ScoBrakingPredictor.StepOriginalServo(parameters,ServoStep with{PreviousServoAccelerationNorm=0,NativeStepSeconds=.1});
        var next=ScoBrakingPredictor.StepOriginalServo(parameters,ServoStep with{PreviousServoAccelerationNorm=first.NextServoAccelerationNorm,SpeedMetresPerSecond=first.NextSpeedMetresPerSecond,NativeStepSeconds=.1});
        Check.Equal(10d,first.NextServoAccelerationNorm);Check.Equal(399d,first.NextSpeedMetresPerSecond);
        Check.Equal(20d,next.NextServoAccelerationNorm);Check.Equal(397d,next.NextSpeedMetresPerSecond);
    }
    [Test]public void CoolingLambdaIsNeitherMultipliedByBlendNorPresentWhenBlendIsZero()
    {
        var original=ScoBrakingPredictor.StepOriginalServo(Servo,ServoStep);
        var mixed=ScoBrakingPredictor.StepOriginalServo(Servo,ServoStep with{FlightAssistBlend=.5,Cooling=true});
        var zero=ScoBrakingPredictor.StepOriginalServo(Servo,ServoStep with{FlightAssistBlend=0,Cooling=true});
        Check.That(Math.Abs(mixed.AppliedAcceleration-(original.AppliedAcceleration*.5-.36*400))<1e-10);
        Check.Equal(0d,zero.AppliedAcceleration);Check.Equal(400d,zero.NextSpeedMetresPerSecond);
    }
    [Test]public void BudgetMaximisesOverHandoffsAndStopsTheOverspeedDomainAtItsFloor()
    {
        foreach(var window in new[]{2d,6d,10d})
        {
            var value=ExitInput with{OriginalArrivalWindowSeconds=window};
            var result=ScoBrakingPredictor.EstimateExit(value);Check.That(result.Known);
            // Independent small-step lower-law integration; no predictor roots
            // or path primitive are reused. Evaluate every possible handoff.
            var v=value.CurrentSpeedMetresPerSecond;var distance=0d;var t=0d;var maximum=window*v;
            const double dt=.0001;
            while(v>result.ServoDomainFloorMetresPerSecond)
            {
                var a=Math.Min(result.DecayRatePerSecond*v,value.OriginalJerkMetresPerSecondCubed*t);
                var next=Math.Max(result.ServoDomainFloorMetresPerSecond,v-a*dt);
                distance+=(v+next)*.5*dt;t+=dt;v=next;
                maximum=Math.Max(maximum,distance+window*v);
            }
            var analytic=result.OverspeedBudgetDistanceMetres+result.OrdinaryArrivalEnvelopeMetres;
            Check.That(Math.Abs(analytic-maximum)<.03);
            Check.That(result.BudgetHandoffSpeedMetresPerSecond>=result.ServoDomainFloorMetresPerSecond-1e-9);
            Check.That(result.BudgetHandoffSeconds<=result.DomainFloorSeconds);
        }
    }
    [Test]public void BiasFloorAndServoLimitedRateAreDerivedRatherThanFitted()
    {
        var original=ScoBrakingPredictor.EstimateExit(ExitInput);Check.That(original.Known);
        Check.That(Math.Abs(original.DecayRatePerSecond-.15)<1e-12);
        Check.That(Math.Abs(original.ServoDomainFloorMetresPerSecond-25)<1e-12);
        var stronger=ScoBrakingPredictor.EstimateExit(ExitInput with{OriginalBaseBrakeAcceleration=500});
        Check.That(stronger.Known);Check.Equal(.125d,stronger.DecayRatePerSecond);Check.Equal(20d,stronger.ServoDomainFloorMetresPerSecond);
    }
    [Test]public void MissingExitSourceGatesAndInvalidParametersNeverAuthoriseAnEstimate()
    {
        foreach(var input in new[]{ExitInput with{OriginalParametersVerified=false},ExitInput with{ForwardServoDomainVerified=false},
            ExitInput with{OrdinaryArrivalControllerVerified=false},ExitInput with{OriginalBrakeScalingMode=1},
            ExitInput with{CurrentSpeedMetresPerSecond=1000},ExitInput with{CurrentOrdinarySpeedLimitMetresPerSecond=400},
            ExitInput with{OriginalJerkMetresPerSecondCubed=double.NaN},
            ExitInput with{CurrentOrdinarySpeedLimitMetresPerSecond=1},ExitInput with{ForwardThrottle=1},
            ExitInput with{EstimatedCoolingAndAssistSettleSeconds=double.PositiveInfinity}})
        {
            var result=ScoBrakingPredictor.EstimateExit(input);
            Check.That(!result.Known&&!result.QualifiedForControl&&!result.RemainingDistanceExceedsEstimate);
        }
        Check.That(!ScoBrakingPredictor.StepOriginalServo(Servo with{PinnedSourceDomainVerified=false},ServoStep).Known);
        Check.That(!ScoBrakingPredictor.StepOriginalServo(Servo,ServoStep with{SpeedMetresPerSecond=20,FlightAssistBlend=.5}).Known);
    }
    private static ScoExitEstimateObservation Observation=>new()
    {
        FreshBindingAndSourceGatesVerified=true,ElapsedSeconds=2,SpeedMetresPerSecond=350,OrdinarySpeedLimitMetresPerSecond=40,
        NativeStepSeconds=.005,FlightAssistBlend=1,ScoState=0,AllowedSpeedResidualFraction=0,AllowedSpeedResidualMetresPerSecond=0
    };
    [Test]public void ActualOrdinaryHandoffEndsTheModelEvenWhenTheCapHasRisen()
    {
        var forecast=ScoBrakingPredictor.EstimateExit(ExitInput);
        var check=ScoBrakingPredictor.CheckExitEstimate(ExitInput,forecast,Observation with{SpeedMetresPerSecond=100,OrdinarySpeedLimitMetresPerSecond=110});
        Check.That(check.WithinEstimatedDomain&&check.OrdinaryHandoffReady&&!check.QualifiedForControl);
        Check.Equal("actual-ordinary-cap-handoff",check.Reason);
    }
    [Test]public void SettleDeadlineIdentityDomainAndBrakeResidualAreOnlineFallbackReasons()
    {
        var forecast=ScoBrakingPredictor.EstimateExit(ExitInput);
        foreach(var observation in new[]{Observation with{FreshBindingAndSourceGatesVerified=false},
            Observation with{ScoState=3},Observation with{FlightAssistBlend=.9},Observation with{NativeStepSeconds=.02},
            Observation with{ElapsedSeconds=5,SpeedMetresPerSecond=400},
            Observation with{OrdinarySpeedLimitMetresPerSecond=20},Observation with{OrdinarySpeedLimitMetresPerSecond=1001},
            Observation with{ElapsedSeconds=1000}})
        {
            var check=ScoBrakingPredictor.CheckExitEstimate(ExitInput,forecast,observation);
            Check.That(!check.WithinEstimatedDomain&&!check.OrdinaryHandoffReady);
        }
    }
    [Test]public void RecordedCurveKeepsSparsePhysicsStepsSeparateFromElapsedTime()
    {
        using var doc=ReadExitFixture();var frames=doc.RootElement.GetProperty("frames").EnumerateArray().Select(x=>x.EnumerateArray().Select(v=>v.GetDouble()).ToArray()).ToArray();
        const double c=299792458;var integral=0d;
        for(var i=1;i<frames.Length;i++)integral+=(frames[i-1][1]+frames[i][1])*.5*(frames[i][0]-frames[i-1][0]);
        var actual=frames[0][6]-frames[^1][6];
        Check.Equal(130,frames.Length);Check.That(Math.Abs((frames[^1][0]-frames[0][0])-7.935)<1e-9);
        Check.That(frames.Sum(x=>x[8])<1); // NOT 7.935 seconds of simulated history.
        Check.That(Math.Abs(integral-actual)/actual<.002);
        Check.That(Math.Abs(actual/c-215.34685571495913)<1e-9);
        Check.That(!doc.RootElement.GetProperty("qualifiedForControl").GetBoolean());
    }
    [Test]public void OriginalScalarOffReplayAgreesWithTheIndependentObservedCurveWithinItsStatedLimits()
    {
        using var doc=ReadExitFixture();var root=doc.RootElement;
        var frames=root.GetProperty("frames").EnumerateArray().Select(x=>x.EnumerateArray().Select(v=>v.GetDouble()).ToArray()).ToArray();
        var first=Array.FindIndex(frames,x=>x[5]==0);const double c=299792458;
        var parameters=new ScoServoParameters
        {
            PinnedSourceDomainVerified=true,BaseMaximumSpeedMetresPerSecond=root.GetProperty("baseMaximumSpeed").GetDouble(),
            BaseBrakeAcceleration=root.GetProperty("baseBrake").GetDouble(),BaseForwardAcceleration=root.GetProperty("baseForward").GetDouble(),
            ResponseTimeSeconds=root.GetProperty("baseResponseTime").GetDouble(),JerkMetresPerSecondCubed=root.GetProperty("laterObservedJerk").GetDouble(),
            ForwardTargetBiasMetresPerSecond=root.GetProperty("laterObservedBias").GetDouble(),ForwardThrottle=root.GetProperty("throttle").GetDouble()
        };
        // Supplied FUTURE cap, and an initially saturated servo norm hypothesis.
        // These are a retrospective source-equation check, not live calibration.
        var speed=frames[first][1];var norm=frames[first][7];var distance=0d;var error=0d;
        for(var i=first+1;i<frames.Length;i++)
        {
            var left=frames[i-1];var right=frames[i];var t=left[0];
            while(t<right[0]-1e-12)
            {
                var dt=Math.Min(.001,right[0]-t);var fraction=(t+.5*dt-left[0])/(right[0]-left[0]);
                var result=ScoBrakingPredictor.StepOriginalServo(parameters,new()
                {
                    SpeedMetresPerSecond=speed,PreviousServoAccelerationNorm=norm,NativeStepSeconds=dt,FlightAssistBlend=1,
                    OrdinaryPhysicsMaximumSpeedMetresPerSecond=left[2]+(right[2]-left[2])*fraction,Cooling=false,CoolingDragPerSecond=0
                });
                Check.That(result.Known);speed=result.NextSpeedMetresPerSecond;norm=result.NextServoAccelerationNorm;
                distance+=result.ApproximateDistanceMetres;t+=dt;
            }
            error=Math.Max(error,Math.Abs(speed-right[1]));
        }
        var actual=frames[first][6]-frames[^1][6];
        Check.That(error/c<.05);Check.That(Math.Abs(distance-actual)/actual<.003);
        Check.That(Math.Abs(speed-frames[^1][1])/c<.005);
    }
    private static JsonDocument ReadExitFixture()=>JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory,"Fixtures","sco-exit-observed.json")));
    // Isolated mathematical fixture, not a claimed calibration of any live ship.
    private static ScoBrakingInput Input=>new()
    {
        OriginalParametersVerified=true,OrdinaryArrivalControllerVerified=true,OverspeedBrakeBoundVerified=true,
        RemainingDistanceMetres=10000,CurrentSpeedMetresPerSecond=400,OriginalBaseMaximumSpeedMetresPerSecond=1000,
        OrdinarySpeedLimitUpperMetresPerSecond=40,OriginalBaseOverspeedBrakeAcceleration=200,
        OriginalBrakeScalingMode=2,OriginalOrdinaryBrakeScaleLower=.04,OriginalExtraBrakeFactorLower=.25,OriginalArrivalWindowSeconds=6,
        StopResponseSecondsUpper=0,ActiveAccelerationMetresPerSecondSquaredUpper=0,PositionUncertaintyMetres=0,SafeDropDistanceMetres=0
    };
    [Test]public void ExactOverspeedIntegralAgreesWithIndependentNumericalOriginalLaw()
    {
        var value=Input;var result=ScoBrakingPredictor.Predict(value);Check.That(result.Known);
        // Simpson integration in velocity space uses the actual original
        // scale formula, rather than the predictor's analytic primitive.
        var intervals=10000;var h=(value.CurrentSpeedMetresPerSecond-value.OrdinarySpeedLimitUpperMetresPerSecond)/intervals;
        var distance=0d;var seconds=0d;var scale=value.OrdinarySpeedLimitUpperMetresPerSecond/value.OriginalBaseMaximumSpeedMetresPerSecond;
        for(var i=0;i<=intervals;i++)
        {
            var speed=value.OrdinarySpeedLimitUpperMetresPerSecond+i*h;
            var ratio=speed/value.OriginalBaseMaximumSpeedMetresPerSecond;
            var originalScaled=value.OriginalBaseOverspeedBrakeAcceleration*Math.Max(scale,ratio*Math.Max(1,Math.Sqrt(ratio/scale)));
            var deceleration=value.OriginalExtraBrakeFactorLower*originalScaled;
            var weight=i==0||i==intervals?1:i%2==0?2:4;
            distance+=weight*speed/deceleration;seconds+=weight/deceleration;
        }
        distance*=h/3;seconds*=h/3;
        Check.That(Math.Abs(distance-result.OverspeedDistanceMetres)<distance*1e-9);
        Check.That(Math.Abs(seconds-result.OverspeedSeconds)<seconds*1e-9);
        Check.Equal(240d,result.OrdinaryArrivalEnvelopeMetres);
        Check.That(result.StopDistanceMetres<value.CurrentSpeedMetresPerSecond*13);
        Check.That(!result.CoolingDragUsed);
    }
    [Test]public void OrdinaryEntryUsesThePostScoLimitRatherThanTheCurrentOverchargeSpeed()
    {
        var first=ScoBrakingPredictor.Predict(Input);
        var faster=ScoBrakingPredictor.Predict(Input with{CurrentSpeedMetresPerSecond=1600});
        Check.Equal(40d,first.OrdinaryEntrySpeedMetresPerSecond);Check.Equal(40d,faster.OrdinaryEntrySpeedMetresPerSecond);
        Check.Equal(240d,faster.OrdinaryArrivalEnvelopeMetres);Check.That(faster.OverspeedDistanceMetres>first.OverspeedDistanceMetres);
        var slow=ScoBrakingPredictor.Predict(Input with{CurrentSpeedMetresPerSecond=20});
        Check.Equal(0d,slow.OverspeedDistanceMetres);Check.Equal(120d,slow.OrdinaryArrivalEnvelopeMetres);
    }
    [Test]public void RealResponseDelayAccountsForDistanceAndFurtherScoAccelerationOnlyDuringThatDelay()
    {
        var baseline=ScoBrakingPredictor.Predict(Input);
        var delayed=ScoBrakingPredictor.Predict(Input with{StopResponseSecondsUpper=.25,ActiveAccelerationMetresPerSecondSquaredUpper=80,PositionUncertaintyMetres=3,SafeDropDistanceMetres=2});
        Check.Equal(420d,delayed.SpeedAtOriginalStopMetresPerSecond);Check.Equal(102.5d,delayed.ResponseDistanceMetres);
        Check.That(delayed.StopDistanceMetres>baseline.StopDistanceMetres+107.5);
        Check.That(ScoBrakingPredictor.Predict(Input with{RemainingDistanceMetres=delayed.StopDistanceMetres-1,StopResponseSecondsUpper=.25,ActiveAccelerationMetresPerSecondSquaredUpper=80,PositionUncertaintyMetres=3,SafeDropDistanceMetres=2}).ContinueOvercharge==false);
    }
    [Test]public void SourceOrBrakeResponseGapsCannotAuthorizeALaterStop()
    {
        foreach(var value in new[]{Input with{OriginalParametersVerified=false},Input with{OrdinaryArrivalControllerVerified=false},Input with{OverspeedBrakeBoundVerified=false},
            Input with{OriginalBrakeScalingMode=3},Input with{OrdinarySpeedLimitUpperMetresPerSecond=1001},Input with{OriginalBaseOverspeedBrakeAcceleration=0},
            Input with{OriginalExtraBrakeFactorLower=.251},Input with{CurrentSpeedMetresPerSecond=double.NaN},Input with{StopResponseSecondsUpper=double.PositiveInfinity}})
        {var result=ScoBrakingPredictor.Predict(value);Check.That(!result.Known&&!result.ContinueOvercharge&&double.IsNaN(result.StopDistanceMetres));}
    }
    [Test]public void ConstantAndSquareRootNativeScalingModesRespectTheirDifferentBrakeStrength()
    {
        var linear=ScoBrakingPredictor.Predict(Input with{OriginalBrakeScalingMode=0});
        var root=ScoBrakingPredictor.Predict(Input with{OriginalBrakeScalingMode=1});
        Check.That(linear.Known&&root.Known);Check.Equal(39600d,linear.OverspeedDistanceMetres);Check.Equal(7920d,root.OverspeedDistanceMetres);
    }
    [Test]public void AProviderSnapshotCannotReplaceALowerScaleBoundForModesThatLoseBrakingNearGravity()
    {
        var current=ScoBrakingPredictor.Predict(Input with{OriginalBrakeScalingMode=0});
        var weaker=ScoBrakingPredictor.Predict(Input with{OriginalBrakeScalingMode=0,OriginalOrdinaryBrakeScaleLower=.01});
        Check.Equal(current.OverspeedDistanceMetres*4,weaker.OverspeedDistanceMetres);
        Check.That(!ScoBrakingPredictor.Predict(Input with{OriginalBrakeScalingMode=0,OriginalOrdinaryBrakeScaleLower=double.NaN}).Known);
        Check.That(!ScoBrakingPredictor.Predict(Input with{OriginalBrakeScalingMode=1,OriginalOrdinaryBrakeScaleLower=.041}).Known);
        // Mode 2 gets stronger when the ordinary limit falls. Its upper
        // speed limit is already the conservative scale for the integral.
        Check.That(ScoBrakingPredictor.Predict(Input with{OriginalOrdinaryBrakeScaleLower=double.NaN}).Known);
    }
    [Test]public void LightSecondScaleAndTinyOverspeedDoNotLoseTheFiniteBrakeDistance()
    {
        const double c=299792458;
        var large=ScoBrakingPredictor.Predict(Input with{CurrentSpeedMetresPerSecond=500*c,OriginalBaseMaximumSpeedMetresPerSecond=2000*c,
            OrdinarySpeedLimitUpperMetresPerSecond=100*c,OriginalBaseOverspeedBrakeAcceleration=400*c});
        Check.That(large.Known&&double.IsFinite(large.StopDistanceMetres)&&large.OverspeedDistanceMetres>0);
        var small=ScoBrakingPredictor.Predict(Input with{CurrentSpeedMetresPerSecond=40.000000001});
        Check.That(small.Known&&small.OverspeedDistanceMetres>0&&small.OverspeedDistanceMetres<1e-6);
    }
}