1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-03 03:20:16 +08:00

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 <tsunyoku@gmail.com>
This commit is contained in:
Natelytle
2025-06-08 18:41:13 -04:00
committed by GitHub
Unverified
parent 6ae8a68389
commit c4b07413b1
@@ -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);
}
/// <summary>
@@ -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.
/// </summary>
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;