From c4b07413b1b5983ca05081d44679859d75eaccef Mon Sep 17 00:00:00 2001 From: Natelytle <92956514+Natelytle@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:41:13 -0400 Subject: [PATCH] Refactor and re-comment osu! standard deviation calculations (#33218) * Refactor * Fix typo * Prevent double.PositiveInfinity from occuring * Fix leftover code branch * Fix some idiot putting Math.Max instead of Math.Min * Address NaN values --------- Co-authored-by: James Wilson --- .../Difficulty/OsuPerformanceCalculator.cs | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index ac9ace2a24..272fe9bb65 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; @@ -398,7 +397,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh); double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk); - return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + return calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh); } /// @@ -407,45 +406,45 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// will always return the same deviation. Misses are ignored because they are usually due to misaiming. /// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution. /// - private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss) + private double? calculateDeviation(double relevantCountGreat, double relevantCountOk, double relevantCountMeh) { if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) return null; - double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; - - // The probability that a player hits a circle is unknown, but we can estimate it to be - // the number of greats on circles divided by the number of circles, and then add one - // to the number of circles as a bias correction. - double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh); - const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - - // Proportion of greats hit on circles, ignoring misses and 50s. + // The sample proportion of successful hits. + double n = Math.Max(1, relevantCountGreat + relevantCountOk); double p = relevantCountGreat / n; - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); + // 99% critical value for the normal distribution (one-tailed). + const double z = 2.32634787404; - // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. - // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + // We can be 99% confident that the population proportion is at least this value. + double pLowerBound = Math.Min(p, (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4)); - double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2)) - / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation))); + double deviation; - deviation *= Math.Sqrt(1 - randomValue); + // Tested max precision for the deviation calculation. + if (pLowerBound > 1e-06) + { + // Compute deviation assuming greats and oks are normally distributed. + deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); - // Value deviation approach as greatCount approaches 0 - double limitValue = okHitWindow / Math.Sqrt(3); + // Subtract the deviation provided by tails that land outside the ok hit window from the deviation computed above. + // This is equivalent to calculating the deviation of a normal distribution truncated at +-okHitWindow. + double okHitWindowTailAmount = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2)) + / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation))); - // If precision is not enough to compute true deviation - use limit value - if (Precision.AlmostEquals(pLowerBound, 0.0) || randomValue >= 1 || deviation > limitValue) - deviation = limitValue; + deviation *= Math.Sqrt(1 - okHitWindowTailAmount); + } + else + { + // A tested limit value for the case of a score only containing oks. + deviation = okHitWindow / Math.Sqrt(3); + } - // Then compute the variance for mehs. + // Compute and add the variance for mehs, assuming that they are uniformly distributed. double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3; - // Find the total deviation. deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); return deviation;