From d1009f8c0231b3e18df65a589bd9fee506aa006a Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Mon, 12 Jul 2021 15:02:18 -0400 Subject: [PATCH 1/3] Implement cumulative survival chance planning --- TestFlightAPI/TestFlightAPI/TestFlightAPI.cs | 26 +++++-- TestFlightReliability_EngineCycle.cs | 77 ++++++++++++++++---- 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/TestFlightAPI/TestFlightAPI/TestFlightAPI.cs b/TestFlightAPI/TestFlightAPI/TestFlightAPI.cs index e51dd09..3a81f1a 100644 --- a/TestFlightAPI/TestFlightAPI/TestFlightAPI.cs +++ b/TestFlightAPI/TestFlightAPI/TestFlightAPI.cs @@ -226,7 +226,7 @@ internal static string UnitStringForMTBFUnitShort(MTBFUnits units) return "-"; } } - + // Simply converts the failure rate to the reliability rate at time t. public static float FailureRateToReliability(float failureRate, float t) { @@ -260,7 +260,7 @@ public static string GetPartName(string partName) partName = partName.Replace(".", "-"); return partName.Replace("_", "-"); } - + public static ITestFlightCore ResolveAlias(Part part, string alias) { // Look at each Core on the part and find the one that defines the given alias @@ -324,7 +324,7 @@ public static List GetAllTestFlightModulesForPart(Part part) } return modules; } - + public static List GetAllTestFlightModulesForAlias(Part part, string alias) { @@ -610,6 +610,21 @@ public static T InvokeTool(string methodName, object[] parameters) public static string PartWithLeastData() => InvokeTool("PartWithLeastData", null); public static string PartWithNoData(string partList) => InvokeTool("PartWithNoData", new object[] { partList }); public static TestFlightPartData GetPartDataForPart(string partName) => InvokeTool("GetPartDataForPart", new object[] { partName }); + + public static float Integrate(Func f, float begin, float end, float stepSize = 0.5f) + { + float left = begin; + float prevValue = f(left); + float acc = 0f; + while (left < end) + { + left += stepSize; + float value = f(left); + acc += (prevValue + value) * stepSize * 0.5f; + prevValue = value; + } + return acc; + } } public struct TestFlightFailureDetails @@ -722,7 +737,7 @@ string Configuration /// /// The current burn time for scope or 0 if this module does not track burn time. float GetScopedRunTime(RatingScope ratingScope); - + /// /// Should return a string if the module wants to report any information to the user in the TestFlight Editor window. /// @@ -799,7 +814,7 @@ string Configuration /// /// A string of information to display to the user, or "" if none string GetModuleInfo(string configuration); - + void SetActiveConfig(string configuration); } @@ -985,4 +1000,3 @@ bool DebugEnabled float GetTechTransfer(); } } - diff --git a/TestFlightReliability_EngineCycle.cs b/TestFlightReliability_EngineCycle.cs index eef4559..96f4a00 100644 --- a/TestFlightReliability_EngineCycle.cs +++ b/TestFlightReliability_EngineCycle.cs @@ -24,19 +24,19 @@ public class TestFlightReliability_EngineCycle : TestFlightReliabilityBase /// [KSPField] public FloatCurve continuousCycle; - + /// /// maximum rated cumulative run time of the engine over the entire lifecycle /// - [KSPField] + [KSPField(guiName = "Rated burn time", groupName = "TestFlight_Plan", groupDisplayName = "TestFlight Planning", guiActiveEditor = true, guiUnits = "s")] public float ratedBurnTime = 0f; /// /// maximum rated continuous burn time between restarts /// - [KSPField] + [KSPField(guiName = "Rated continuous burn time", groupName = "TestFlight_Plan", groupDisplayName = "TestFlight Planning", guiUnits = "s")] public float ratedContinuousBurnTime; - + [KSPField] public string engineID = ""; [KSPField] @@ -59,6 +59,15 @@ public class TestFlightReliability_EngineCycle : TestFlightReliabilityBase [KSPField(isPersistant = true)] public double previousOperatingTime = 0d; + [KSPField(guiName = "Burn time", groupName = "TestFlight_Plan", groupDisplayName = "TestFlight Planning", guiActiveEditor = true, guiUnits = "s"), + UI_FloatRange(minValue = 0f, stepIncrement = 1f, scene = UI_Scene.Editor)] + public float plannerBurnTime; + [KSPField(guiName = "Continuous burn time", groupName = "TestFlight_Plan", groupDisplayName = "TestFlight Planning", guiActiveEditor = true, guiUnits = "s"), + UI_FloatRange(minValue = 1f, stepIncrement = 1f, scene = UI_Scene.Editor)] + public float plannerContinuousBurnTime; + [KSPField(guiName = "Survival chance", groupName = "TestFlight_Plan", groupDisplayName = "TestFlight Planning", guiActiveEditor = true, guiFormat = "P2")] + public float plannerSurvivalChance; + public override void OnStart(StartState state) { base.OnStart(state); @@ -66,6 +75,12 @@ public override void OnStart(StartState state) engine.InitWithEngine(this.part, engineID); Fields[nameof(currentRunTime)].guiUnits = $"s/{ratedContinuousBurnTime}s"; Fields[nameof(engineOperatingTime)].guiUnits = $"s/{ratedBurnTime}s"; + + Fields[nameof(plannerBurnTime)].uiControlEditor.onFieldChanged += onPlannerChanged; + Fields[nameof(plannerContinuousBurnTime)].uiControlEditor.onFieldChanged += onPlannerChanged; + if (ratedBurnTime == ratedContinuousBurnTime) plannerBurnTime = ratedBurnTime; // Set the total burn time slider to the rated burn time only if there isn't a continuous burn time. + plannerContinuousBurnTime = ratedContinuousBurnTime; + UpdatePlanner(); } public override double GetBaseFailureRate(float flightData) @@ -85,23 +100,23 @@ protected void UpdateCycle() if (!core.IsPartOperating()) { // engine is not running - + // reset continuous run time currentRunTime = 0; } else { // engine is running - + // increase both continuous and cumulative run times currentRunTime += deltaTime; // continuous engineOperatingTime += deltaTime; // cumulative - + // calculate total failure rate modifier float cumulativeModifier = cycle.Evaluate((float)engineOperatingTime); float continuousModifier = continuousCycle.Evaluate((float)currentRunTime); - + core.SetTriggerMomentaryFailureModifier("EngineCycle", cumulativeModifier * continuousModifier, this); } } @@ -117,8 +132,8 @@ public override void OnUpdate() UpdateCycle(); - // We intentionally do NOT call our base class OnUpdate() because that would kick off a second round of - // failure checks which is already handled by the main Reliability module that should + // We intentionally do NOT call our base class OnUpdate() because that would kick off a second round of + // failure checks which is already handled by the main Reliability module that should // already be on the part (This PartModule should be added in addition to the normal reliability module) // base.OnUpdate(); } @@ -126,11 +141,11 @@ public override void OnLoad(ConfigNode node) { base.OnLoad(node); } - + public override void SetActiveConfig(string alias) { base.SetActiveConfig(alias); - + if (currentConfig == null) return; // update current values with those from the current config node @@ -164,6 +179,12 @@ public override void SetActiveConfig(string alias) { ratedContinuousBurnTime = ratedBurnTime; } + + ((UI_FloatRange)(Fields[nameof(plannerBurnTime)].uiControlEditor)).maxValue = cycle.maxTime; + plannerBurnTime = Mathf.Clamp(plannerBurnTime, 0f, cycle.maxTime); + ((UI_FloatRange)(Fields[nameof(plannerContinuousBurnTime)].uiControlEditor)).maxValue = continuousCycle.maxTime; + plannerContinuousBurnTime = Mathf.Clamp(plannerContinuousBurnTime, 1f, continuousCycle.maxTime); + UpdatePlanner(); } public override string GetModuleInfo(string configuration, float reliabilityAtTime) @@ -215,7 +236,7 @@ public override float GetRatedTime(string configuration, RatingScope ratingScope return nodeBurnTime; } break; - + case RatingScope.Continuous: if (configNode.HasValue("ratedContinuousBurnTime")) { @@ -230,8 +251,8 @@ public override float GetRatedTime(string configuration, RatingScope ratingScope return base.GetRatedTime(configuration, ratingScope); } - - + + public override float GetRatedTime(RatingScope ratingScope) { @@ -301,6 +322,30 @@ public override List GetTestFlightInfo(float reliabilityAtTime) } return infoStrings; } + + protected void onPlannerChanged(BaseField f, object obj) => UpdatePlanner(); + + protected void UpdatePlanner() + { + bool hasContinuousBurnTime = ratedBurnTime != ratedContinuousBurnTime; + + Fields[nameof(ratedContinuousBurnTime)].guiActiveEditor = hasContinuousBurnTime; + Fields[nameof(plannerBurnTime)].guiName = hasContinuousBurnTime ? "Cumulative burn time" : "Burn time"; + Fields[nameof(plannerContinuousBurnTime)].guiActiveEditor = hasContinuousBurnTime; + + float burnTime = hasContinuousBurnTime ? plannerContinuousBurnTime : plannerBurnTime; + + var instantaneousFailureRateMult = (Func)((t) => + { + float mult = cycle.Evaluate(hasContinuousBurnTime ? t + plannerBurnTime : t); + if (hasContinuousBurnTime) mult *= continuousCycle.Evaluate(t); + return Mathf.Max(mult, 1f); + }); + + // Cumulative survival chance at time T = 1 - F(T) = $exp(-\int_0^T h(t)dt)$. + // Since the base failure rate is constant, we can pull it out of the integral. + plannerSurvivalChance = Mathf.Exp(-(float)core.GetBaseFailureRate() + * TestFlightUtil.Integrate(instantaneousFailureRateMult, 0f, burnTime)); + } } } - From 43019ca28945f3cf90a65e76457ece91de4392dc Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Mon, 27 Sep 2021 21:44:56 -0400 Subject: [PATCH 2/3] Tentative fix for what appears to be a cache invalidation problem --- TestFlightCore/TestFlightCore/TestFlightCore.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TestFlightCore/TestFlightCore/TestFlightCore.cs b/TestFlightCore/TestFlightCore/TestFlightCore.cs index 9a0ff64..278a5f5 100644 --- a/TestFlightCore/TestFlightCore/TestFlightCore.cs +++ b/TestFlightCore/TestFlightCore/TestFlightCore.cs @@ -189,6 +189,12 @@ bool SetActiveConfigFromInterop() currentConfig.TryGetValue("rndRate", ref rndRate); currentConfig.TryGetValue("rndCost", ref rndCost); + // Invalidate cached value. + if (prevConfig != currentConfig) + { + baseFailureRate = 0f; + } + return prevConfig == currentConfig; } From a554dbe79058ad7f67bfe1a36a20fc1e7bde1485 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Mon, 3 Jan 2022 15:51:33 -0500 Subject: [PATCH 3/3] Fix planner issues in symmetry --- TestFlightReliability_EngineCycle.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/TestFlightReliability_EngineCycle.cs b/TestFlightReliability_EngineCycle.cs index 96f4a00..4a6cbed 100644 --- a/TestFlightReliability_EngineCycle.cs +++ b/TestFlightReliability_EngineCycle.cs @@ -77,7 +77,9 @@ public override void OnStart(StartState state) Fields[nameof(engineOperatingTime)].guiUnits = $"s/{ratedBurnTime}s"; Fields[nameof(plannerBurnTime)].uiControlEditor.onFieldChanged += onPlannerChanged; + Fields[nameof(plannerBurnTime)].uiControlEditor.onSymmetryFieldChanged += onPlannerChanged; Fields[nameof(plannerContinuousBurnTime)].uiControlEditor.onFieldChanged += onPlannerChanged; + Fields[nameof(plannerContinuousBurnTime)].uiControlEditor.onSymmetryFieldChanged += onPlannerChanged; if (ratedBurnTime == ratedContinuousBurnTime) plannerBurnTime = ratedBurnTime; // Set the total burn time slider to the rated burn time only if there isn't a continuous burn time. plannerContinuousBurnTime = ratedContinuousBurnTime; UpdatePlanner(); @@ -156,7 +158,7 @@ public override void SetActiveConfig(string alias) } else { - cycle.Add(0f,1f); + cycle.Add(0f, 1f); } continuousCycle = new FloatCurve(); @@ -180,10 +182,6 @@ public override void SetActiveConfig(string alias) ratedContinuousBurnTime = ratedBurnTime; } - ((UI_FloatRange)(Fields[nameof(plannerBurnTime)].uiControlEditor)).maxValue = cycle.maxTime; - plannerBurnTime = Mathf.Clamp(plannerBurnTime, 0f, cycle.maxTime); - ((UI_FloatRange)(Fields[nameof(plannerContinuousBurnTime)].uiControlEditor)).maxValue = continuousCycle.maxTime; - plannerContinuousBurnTime = Mathf.Clamp(plannerContinuousBurnTime, 1f, continuousCycle.maxTime); UpdatePlanner(); } @@ -327,6 +325,13 @@ public override List GetTestFlightInfo(float reliabilityAtTime) protected void UpdatePlanner() { + if (core == null) return; + + ((UI_FloatRange)(Fields[nameof(plannerBurnTime)].uiControlEditor)).maxValue = cycle.maxTime; + plannerBurnTime = Mathf.Clamp(plannerBurnTime, 0f, cycle.maxTime); + ((UI_FloatRange)(Fields[nameof(plannerContinuousBurnTime)].uiControlEditor)).maxValue = continuousCycle.maxTime; + plannerContinuousBurnTime = Mathf.Clamp(plannerContinuousBurnTime, 1f, continuousCycle.maxTime); + bool hasContinuousBurnTime = ratedBurnTime != ratedContinuousBurnTime; Fields[nameof(ratedContinuousBurnTime)].guiActiveEditor = hasContinuousBurnTime; @@ -335,12 +340,12 @@ protected void UpdatePlanner() float burnTime = hasContinuousBurnTime ? plannerContinuousBurnTime : plannerBurnTime; - var instantaneousFailureRateMult = (Func)((t) => + Func instantaneousFailureRateMult = (t) => { float mult = cycle.Evaluate(hasContinuousBurnTime ? t + plannerBurnTime : t); if (hasContinuousBurnTime) mult *= continuousCycle.Evaluate(t); return Mathf.Max(mult, 1f); - }); + }; // Cumulative survival chance at time T = 1 - F(T) = $exp(-\int_0^T h(t)dt)$. // Since the base failure rate is constant, we can pull it out of the integral.