From 442e68ac1a496529869e7cbb75ad40bc4881aef4 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Tue, 25 Oct 2022 17:41:20 -0400 Subject: [PATCH 01/35] Implement taiko deviation estimation --- .../Difficulty/TaikoPerformanceAttributes.cs | 3 +++ .../Difficulty/TaikoPerformanceCalculator.cs | 27 +++++++++++++++---- .../osu.Game.Rulesets.Taiko.csproj | 4 +++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index b61c13a2df..fa8c3fd27a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -19,6 +19,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + + [JsonProperty("estimated_ur")] + public double EstimatedUR { get; set; } public override IEnumerable GetAttributesForDisplay() { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index dc7bad2f75..7d5cf3accd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; +using MathNet.Numerics; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -21,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMeh; private int countMiss; private double accuracy; + private double estimatedUR; private double effectiveMissCount; @@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - accuracy = customAccuracy; + estimatedUR = 10 * computeEstimatedUR(score, taikoAttributes); // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -64,6 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Difficulty = difficultyValue, Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, + EstimatedUR = 10 * estimatedUR, Total = totalValue }; } @@ -97,10 +100,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0) return 0; - double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0; + double accuracyValue = Math.Pow(75 / estimatedUR, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 70.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); - accuracyValue *= lengthBonus; // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden)) @@ -109,10 +111,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return accuracyValue; } + private double computeEstimatedUR(ScoreInfo score, TaikoDifficultyAttributes attributes) + { + if (totalHits == 0) + return double.PositiveInfinity; + + double greatProbability = 1 - (countOk + countMiss + 1.0) / (totalHits + 1.0); + + if (greatProbability <= 0) + { + return double.PositiveInfinity; + } + + double deviation = attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbability)); + + return deviation; + } + private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; - - private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0; } } diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index b752c13d18..b2953106d8 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -15,4 +15,8 @@ + + + + From d5b06ae9454885e3eeec8de652a8cd51c86d8b7a Mon Sep 17 00:00:00 2001 From: Natelytle Date: Tue, 25 Oct 2022 17:52:34 -0400 Subject: [PATCH 02/35] Fix difficultyvalue acc scaling --- .../Difficulty/TaikoPerformanceCalculator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 7d5cf3accd..ee2105d859 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countOk; private int countMeh; private int countMiss; - private double accuracy; private double estimatedUR; private double effectiveMissCount; @@ -92,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - return difficultyValue * Math.Pow(accuracy, 2.0); + return difficultyValue * Math.Pow(100 * SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUR)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) From 607a006c4f5b11be1f4e8c223c8cfea2b96a9c19 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Tue, 25 Oct 2022 17:55:16 -0400 Subject: [PATCH 03/35] oops --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ee2105d859..ba96e0b95e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - return difficultyValue * Math.Pow(100 * SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUR)), 2.0); + return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUR)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) From 87cba2d828455b88c26e6eb813667c916ec07c60 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Tue, 25 Oct 2022 19:15:58 -0400 Subject: [PATCH 04/35] Slight adjustments --- .../Difficulty/TaikoPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ba96e0b95e..ec5c150ea4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Difficulty = difficultyValue, Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, - EstimatedUR = 10 * estimatedUR, + EstimatedUR = estimatedUR, Total = totalValue }; } @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0) return 0; - double accuracyValue = Math.Pow(75 / estimatedUR, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 70.0; + double accuracyValue = Math.Pow(75 / estimatedUR, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); From 7d3338a0eacb1d4ecbcdb82ee98f8f34d2ac4a3d Mon Sep 17 00:00:00 2001 From: Natelytle Date: Wed, 26 Oct 2022 15:58:20 -0400 Subject: [PATCH 05/35] LTCA Balancing pass --- .../Difficulty/TaikoPerformanceCalculator.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ec5c150ea4..2a7cb6c6b5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countOk; private int countMeh; private int countMiss; - private double estimatedUR; + private double estimatedDeviation; private double effectiveMissCount; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - estimatedUR = 10 * computeEstimatedUR(score, taikoAttributes); + estimatedDeviation = computeEstimatedDeviation(score, taikoAttributes); // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Difficulty = difficultyValue, Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, - EstimatedUR = estimatedUR, + EstimatedUR = estimatedDeviation * 10, Total = totalValue }; } @@ -86,12 +86,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyValue *= 1.025; if (score.Mods.Any(m => m is ModHardRock)) - difficultyValue *= 1.050; + difficultyValue *= 1.10; if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUR)), 2.0); + return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedDeviation)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) @@ -99,18 +99,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0) return 0; - double accuracyValue = Math.Pow(75 / estimatedUR, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; + double accuracyValue = Math.Pow(7.5 / estimatedDeviation, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden)) - accuracyValue *= Math.Max(1.050, 1.075 * lengthBonus); + accuracyValue *= Math.Max(1.0, 1.05 * lengthBonus); return accuracyValue; } - private double computeEstimatedUR(ScoreInfo score, TaikoDifficultyAttributes attributes) + private double computeEstimatedDeviation(ScoreInfo score, TaikoDifficultyAttributes attributes) { if (totalHits == 0) return double.PositiveInfinity; From af919a6550f58da73784931883e59e4b042f910b Mon Sep 17 00:00:00 2001 From: Natelytle Date: Wed, 26 Oct 2022 16:10:36 -0400 Subject: [PATCH 06/35] harshen deviation scaling --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 2a7cb6c6b5..565d166c32 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedDeviation)), 2.0); + return difficultyValue * Math.Pow(SpecialFunctions.Erf(40 / (Math.Sqrt(2) * estimatedDeviation)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) From 2940d18d3393c1e006c715ec47d5d2fa88b1bf61 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Thu, 27 Oct 2022 00:07:32 -0400 Subject: [PATCH 07/35] Fix formatting --- .../Difficulty/TaikoPerformanceAttributes.cs | 2 +- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index fa8c3fd27a..6893f24d20 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } - + [JsonProperty("estimated_ur")] public double EstimatedUR { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 565d166c32..1e57d77d91 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { if (totalHits == 0) return double.PositiveInfinity; - + double greatProbability = 1 - (countOk + countMiss + 1.0) / (totalHits + 1.0); if (greatProbability <= 0) From 883790c7a78a7a69b08a3d5299557d2e245c16f5 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Fri, 28 Oct 2022 16:18:17 -0400 Subject: [PATCH 08/35] Return null instead of infinity --- .../Difficulty/TaikoPerformanceAttributes.cs | 2 +- .../Difficulty/TaikoPerformanceCalculator.cs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 6893f24d20..3786009a1f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public double EffectiveMissCount { get; set; } [JsonProperty("estimated_ur")] - public double EstimatedUR { get; set; } + public double? EstimatedUR { get; set; } public override IEnumerable GetAttributesForDisplay() { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 1e57d77d91..c27efb518d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countOk; private int countMeh; private int countMiss; - private double estimatedDeviation; + private double? estimatedDeviation; private double effectiveMissCount; @@ -91,15 +91,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(40 / (Math.Sqrt(2) * estimatedDeviation)), 2.0); + if (estimatedDeviation == null) + return 0; + + return difficultyValue * Math.Pow(SpecialFunctions.Erf(40 / (Math.Sqrt(2) * (double)estimatedDeviation)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - if (attributes.GreatHitWindow <= 0) + if (attributes.GreatHitWindow <= 0 || estimatedDeviation == null) return 0; - double accuracyValue = Math.Pow(7.5 / estimatedDeviation, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; + double accuracyValue = Math.Pow(7.5 / (double)estimatedDeviation, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); @@ -110,10 +113,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return accuracyValue; } - private double computeEstimatedDeviation(ScoreInfo score, TaikoDifficultyAttributes attributes) + private double? computeEstimatedDeviation(ScoreInfo score, TaikoDifficultyAttributes attributes) { if (totalHits == 0) - return double.PositiveInfinity; + return null; double greatProbability = 1 - (countOk + countMiss + 1.0) / (totalHits + 1.0); From 01c79d8ef29abf4871335db06326eeef56d3fed6 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Fri, 28 Oct 2022 16:20:21 -0400 Subject: [PATCH 09/35] remove other infinity reference --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c27efb518d..4fba926205 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (greatProbability <= 0) { - return double.PositiveInfinity; + return null; } double deviation = attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbability)); From 7403c1cc862f6ebbd9e6a2bc791857b53a973da6 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Fri, 28 Oct 2022 23:23:50 -0400 Subject: [PATCH 10/35] Return null for greatprobability >= 1 --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 4fba926205..003e46d77b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double greatProbability = 1 - (countOk + countMiss + 1.0) / (totalHits + 1.0); - if (greatProbability <= 0) + if (greatProbability <= 0 || greatProbability >= 1) { return null; } From 16301f052e8d64164e6f480a594e28868f940614 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Sun, 30 Oct 2022 15:01:25 -0400 Subject: [PATCH 11/35] Fix low end accuracy, buff high end --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 003e46d77b..c57efd3c4d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0 || estimatedDeviation == null) return 0; - double accuracyValue = Math.Pow(7.5 / (double)estimatedDeviation, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; + double accuracyValue = Math.Pow(7.5 / (double)estimatedDeviation, 1.1) * Math.Pow(attributes.StarRating / 2.7, 0.8) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); From 37c21cdd7cbe4e00ae564dfce66d7a55ab1aa68e Mon Sep 17 00:00:00 2001 From: Natelytle Date: Tue, 1 Nov 2022 15:14:55 -0400 Subject: [PATCH 12/35] fix formatting --- .../Difficulty/TaikoPerformanceAttributes.cs | 2 +- .../Difficulty/TaikoPerformanceCalculator.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 3786009a1f..74770eaa79 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public double EffectiveMissCount { get; set; } [JsonProperty("estimated_ur")] - public double? EstimatedUR { get; set; } + public double? EstimatedUr { get; set; } public override IEnumerable GetAttributesForDisplay() { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c57efd3c4d..0a24e33419 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Difficulty = difficultyValue, Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, - EstimatedUR = estimatedDeviation * 10, + EstimatedUr = estimatedDeviation * 10, Total = totalValue }; } @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (estimatedDeviation == null) return 0; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(40 / (Math.Sqrt(2) * (double)estimatedDeviation)), 2.0); + return difficultyValue * Math.Pow(SpecialFunctions.Erf(40 / (Math.Sqrt(2) * estimatedDeviation.Value)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0 || estimatedDeviation == null) return 0; - double accuracyValue = Math.Pow(7.5 / (double)estimatedDeviation, 1.1) * Math.Pow(attributes.StarRating / 2.7, 0.8) * 100.0; + double accuracyValue = Math.Pow(7.5 / estimatedDeviation.Value, 1.1) * Math.Pow(attributes.StarRating / 2.7, 0.8) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double greatProbability = 1 - (countOk + countMiss + 1.0) / (totalHits + 1.0); - if (greatProbability <= 0 || greatProbability >= 1) + if (greatProbability is <= 0 or >= 1) { return null; } From 2ba163440aae865d656cf954bed3157d39b60d7f Mon Sep 17 00:00:00 2001 From: Natelytle Date: Thu, 24 Nov 2022 19:09:30 -0500 Subject: [PATCH 13/35] account for low acc FC deviation --- .../Difficulty/TaikoPerformanceCalculator.cs | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 0a24e33419..281dc19837 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -12,6 +12,9 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; using MathNet.Numerics; +using MathNet.Numerics.RootFinding; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -66,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, EstimatedUr = estimatedDeviation * 10, - Total = totalValue + Total = (double)estimatedDeviation * 10 }; } @@ -118,20 +121,56 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (totalHits == 0) return null; - double greatProbability = 1 - (countOk + countMiss + 1.0) / (totalHits + 1.0); + // Create a new track to properly calculate the hit windows of 100s and 50s. + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + double overallDifficulty = (50 - attributes.GreatHitWindow) / 3 * clockRate; + double goodHitWindow = 0; + if (overallDifficulty <= 5) + goodHitWindow = (120 - 8 * overallDifficulty) / clockRate; + if (overallDifficulty > 5) + goodHitWindow = 80 - 6 * (overallDifficulty - 5); + + double root2 = Math.Sqrt(2); - if (greatProbability is <= 0 or >= 1) + double logLikelihoodGradient(double d) { - return null; + if (d <= 0) + return 1; + + double p300 = SpecialFunctions.Erf(attributes.GreatHitWindow / root2 * d); + double lnP300 = lnErfcApprox(attributes.GreatHitWindow / root2 * d); + double lnP100 = lnErfcApprox(goodHitWindow / root2 * d); + + double t1 = countGreat * ((attributes.GreatHitWindow * Math.Exp(-Math.Pow(attributes.GreatHitWindow / root2 * d, 2))) / p300); + + double t2a = Math.Exp(Math.Log(goodHitWindow * Math.Exp(-Math.Pow(goodHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); + double t2b = Math.Exp(Math.Log(attributes.GreatHitWindow * Math.Exp(-Math.Pow(attributes.GreatHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); + double t2 = (countOk + 1) * (t2a - t2b); + + double t3 = countMiss * Math.Exp(Math.Log(goodHitWindow * Math.Exp(-Math.Pow(goodHitWindow / root2 * d, 2))) - lnP100); + + return t1 + t2 - t3; } - double deviation = attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbability)); + double root = Brent.FindRootExpand(logLikelihoodGradient, 0.02, 0.3, expandFactor: 2); - return deviation; + return 1 / root; } private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; + + double lnErfcApprox(double x) + { + if (x <= 5) + return Math.Log(SpecialFunctions.Erfc(x)); + + return -Math.Pow(x, 2) - Math.Log(x) - Math.Log(Math.Sqrt(Math.PI)); + } + + double logDiff(double l1, double l2) => l1 + SpecialFunctions.Log1p(-Math.Exp(-(l1 - l2))); } } From 0e4e92b344b6bce55ccf6ceb776cc8bfb450338f Mon Sep 17 00:00:00 2001 From: Natelytle Date: Thu, 24 Nov 2022 19:28:47 -0500 Subject: [PATCH 14/35] totalvalue --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 281dc19837..332f6f8267 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, EstimatedUr = estimatedDeviation * 10, - Total = (double)estimatedDeviation * 10 + Total = totalValue }; } From b579af674e128b94294ef9d379bc9a89d4dc94df Mon Sep 17 00:00:00 2001 From: Natelytle Date: Thu, 24 Nov 2022 19:46:55 -0500 Subject: [PATCH 15/35] fix dt --- .../Difficulty/TaikoPerformanceCalculator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 332f6f8267..f184a7cdce 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -125,7 +125,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var track = new TrackVirtual(10000); score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); double clockRate = track.Rate; - double overallDifficulty = (50 - attributes.GreatHitWindow) / 3 * clockRate; + + double overallDifficulty = (50 - attributes.GreatHitWindow * clockRate) / 3; double goodHitWindow = 0; if (overallDifficulty <= 5) goodHitWindow = (120 - 8 * overallDifficulty) / clockRate; From e3ef180c4669b3197c7a527daaad76435fa2fa7f Mon Sep 17 00:00:00 2001 From: Natelytle Date: Fri, 25 Nov 2022 15:18:39 -0500 Subject: [PATCH 16/35] fixes --- .../Difficulty/TaikoPerformanceCalculator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index f184a7cdce..d1e305fe9b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double? computeEstimatedDeviation(ScoreInfo score, TaikoDifficultyAttributes attributes) { - if (totalHits == 0) + if (totalSuccessfulHits == 0) return null; // Create a new track to properly calculate the hit windows of 100s and 50s. @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return t1 + t2 - t3; } - double root = Brent.FindRootExpand(logLikelihoodGradient, 0.02, 0.3, expandFactor: 2); + double root = Brent.FindRootExpand(logLikelihoodGradient, 0.02, 0.3); return 1 / root; } @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int totalSuccessfulHits => countGreat + countOk + countMeh; - double lnErfcApprox(double x) + private double lnErfcApprox(double x) { if (x <= 5) return Math.Log(SpecialFunctions.Erfc(x)); @@ -172,6 +172,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return -Math.Pow(x, 2) - Math.Log(x) - Math.Log(Math.Sqrt(Math.PI)); } - double logDiff(double l1, double l2) => l1 + SpecialFunctions.Log1p(-Math.Exp(-(l1 - l2))); + private double logDiff(double l1, double l2) => l1 + SpecialFunctions.Log1p(-Math.Exp(-(l1 - l2))); } } From 7b5373ac5a8f70c332840a5b1fe374f96ac51c19 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Sun, 27 Nov 2022 15:24:54 -0500 Subject: [PATCH 17/35] add comments --- .../Difficulty/TaikoPerformanceCalculator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index d1e305fe9b..1d7af4e167 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -116,12 +116,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return accuracyValue; } + /// + /// Estimates the player's tap deviation based on the OD, number of objects, and number of 300s, 100s, and misses, + /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings + /// will always return the same deviation. See: https://www.desmos.com/calculator/qlr946netu + /// private double? computeEstimatedDeviation(ScoreInfo score, TaikoDifficultyAttributes attributes) { if (totalSuccessfulHits == 0) return null; - // Create a new track to properly calculate the hit windows of 100s and 50s. + // Create a new track to properly calculate the hit window of 100s. var track = new TrackVirtual(10000); score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); double clockRate = track.Rate; @@ -135,6 +140,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double root2 = Math.Sqrt(2); + // Log of the most likely deviation resulting in the score's hit judgements, differentiated such that 1 over the most likely deviation returns 0. double logLikelihoodGradient(double d) { if (d <= 0) From 6a27206abdffa6e18bca1c3f27df6614b9abf869 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Fri, 2 Dec 2022 17:01:15 -0500 Subject: [PATCH 18/35] bugfix + tests --- .../Difficulty/TaikoPerformanceCalculator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 19b43d0422..ca4e5fe53a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -135,11 +135,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double clockRate = track.Rate; double overallDifficulty = (50 - attributes.GreatHitWindow * clockRate) / 3; - double goodHitWindow = 0; + double goodHitWindow; if (overallDifficulty <= 5) goodHitWindow = (120 - 8 * overallDifficulty) / clockRate; - if (overallDifficulty > 5) - goodHitWindow = 80 - 6 * (overallDifficulty - 5); + else + goodHitWindow = (80 - 6 * (overallDifficulty - 5)) / clockRate; double root2 = Math.Sqrt(2); @@ -155,9 +155,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double t1 = countGreat * ((attributes.GreatHitWindow * Math.Exp(-Math.Pow(attributes.GreatHitWindow / root2 * d, 2))) / p300); - double t2a = Math.Exp(Math.Log(goodHitWindow * Math.Exp(-Math.Pow(goodHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); - double t2b = Math.Exp(Math.Log(attributes.GreatHitWindow * Math.Exp(-Math.Pow(attributes.GreatHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); - double t2 = (countOk + 1) * (t2a - t2b); + double t2A = Math.Exp(Math.Log(goodHitWindow * Math.Exp(-Math.Pow(goodHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); + double t2B = Math.Exp(Math.Log(attributes.GreatHitWindow * Math.Exp(-Math.Pow(attributes.GreatHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); + double t2 = (countOk + 1) * (t2A - t2B); double t3 = countMiss * Math.Exp(Math.Log(goodHitWindow * Math.Exp(-Math.Pow(goodHitWindow / root2 * d, 2))) - lnP100); From 2b74c4ef8cd60e9d3d6f7907962759573a8896d8 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Sat, 3 Dec 2022 15:39:38 -0500 Subject: [PATCH 19/35] tests return a greathitwindow of 0, add check --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ca4e5fe53a..fd2dd47d30 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// private double? computeEstimatedDeviation(ScoreInfo score, TaikoDifficultyAttributes attributes) { - if (totalSuccessfulHits == 0) + if (totalSuccessfulHits == 0 || attributes.GreatHitWindow == 0) return null; // Create a new track to properly calculate the hit window of 100s. From 45e8d18b1b9618d8c068731a389285dc270dd2b4 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Mon, 5 Dec 2022 22:33:13 -0500 Subject: [PATCH 20/35] fix extremely low OD breaking deviation calc --- .../Difficulty/TaikoPerformanceCalculator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index fd2dd47d30..807e9359fc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -153,13 +153,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double lnP300 = lnErfcApprox(attributes.GreatHitWindow / root2 * d); double lnP100 = lnErfcApprox(goodHitWindow / root2 * d); - double t1 = countGreat * ((attributes.GreatHitWindow * Math.Exp(-Math.Pow(attributes.GreatHitWindow / root2 * d, 2))) / p300); + double t1 = countGreat * (Math.Exp(Math.Log(attributes.GreatHitWindow) + -Math.Pow(attributes.GreatHitWindow / root2 * d, 2)) / p300); - double t2A = Math.Exp(Math.Log(goodHitWindow * Math.Exp(-Math.Pow(goodHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); - double t2B = Math.Exp(Math.Log(attributes.GreatHitWindow * Math.Exp(-Math.Pow(attributes.GreatHitWindow / root2 * d, 2))) - logDiff(lnP300, lnP100)); + double t2A = Math.Exp(Math.Log(goodHitWindow) + -Math.Pow(goodHitWindow / root2 * d, 2) - logDiff(lnP300, lnP100)); + double t2B = Math.Exp(Math.Log(attributes.GreatHitWindow) + -Math.Pow(attributes.GreatHitWindow / root2 * d, 2) - logDiff(lnP300, lnP100)); double t2 = (countOk + 1) * (t2A - t2B); - double t3 = countMiss * Math.Exp(Math.Log(goodHitWindow * Math.Exp(-Math.Pow(goodHitWindow / root2 * d, 2))) - lnP100); + double t3 = countMiss * Math.Exp(Math.Log(goodHitWindow) + -Math.Pow(goodHitWindow / root2 * d, 2) - lnP100); return t1 + t2 - t3; } From 334f60f528b130cb47634e960c8fe47ff01581cc Mon Sep 17 00:00:00 2001 From: Natelytle Date: Wed, 22 Feb 2023 14:55:48 -0500 Subject: [PATCH 21/35] Reformat everything to be simpler --- .../Difficulty/TaikoPerformanceCalculator.cs | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 807e9359fc..84b71a98d4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; using MathNet.Numerics; -using MathNet.Numerics.RootFinding; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countOk; private int countMeh; private int countMiss; - private double? estimatedDeviation; + private double? estimatedUr; private double effectiveMissCount; @@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - estimatedDeviation = computeEstimatedDeviation(score, taikoAttributes); + estimatedUr = computeEstimatedUr(score, taikoAttributes); // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Difficulty = difficultyValue, Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, - EstimatedUr = estimatedDeviation * 10, + EstimatedUr = estimatedUr, Total = totalValue }; } @@ -97,18 +96,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - if (estimatedDeviation == null) + if (estimatedUr == null) return 0; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(40 / (Math.Sqrt(2) * estimatedDeviation.Value)), 2.0); + return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUr.Value)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) { - if (attributes.GreatHitWindow <= 0 || estimatedDeviation == null) + if (attributes.GreatHitWindow <= 0 || estimatedUr == null) return 0; - double accuracyValue = Math.Pow(7.5 / estimatedDeviation.Value, 1.1) * Math.Pow(attributes.StarRating / 2.7, 0.8) * 100.0; + double accuracyValue = Math.Pow(75 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating / 2.7, 0.8) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); @@ -124,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings /// will always return the same deviation. See: https://www.desmos.com/calculator/qlr946netu /// - private double? computeEstimatedDeviation(ScoreInfo score, TaikoDifficultyAttributes attributes) + private double? computeEstimatedUr(ScoreInfo score, TaikoDifficultyAttributes attributes) { if (totalSuccessfulHits == 0 || attributes.GreatHitWindow == 0) return null; @@ -135,52 +134,55 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double clockRate = track.Rate; double overallDifficulty = (50 - attributes.GreatHitWindow * clockRate) / 3; - double goodHitWindow; - if (overallDifficulty <= 5) - goodHitWindow = (120 - 8 * overallDifficulty) / clockRate; - else - goodHitWindow = (80 - 6 * (overallDifficulty - 5)) / clockRate; + double h300 = attributes.GreatHitWindow; + double h100 = overallDifficulty <= 5 ? (120 - 8 * overallDifficulty) / clockRate : (80 - 6 * (overallDifficulty - 5)) / clockRate; - double root2 = Math.Sqrt(2); - - // Log of the most likely deviation resulting in the score's hit judgements, differentiated such that 1 over the most likely deviation returns 0. - double logLikelihoodGradient(double d) + // Returns the likelihood of a deviation resulting in the score's hit judgements. The peak of the curve is the most likely deviation. + double likelihoodGradient(double d) { if (d <= 0) - return 1; + return 0; - double p300 = SpecialFunctions.Erf(attributes.GreatHitWindow / root2 * d); - double lnP300 = lnErfcApprox(attributes.GreatHitWindow / root2 * d); - double lnP100 = lnErfcApprox(goodHitWindow / root2 * d); + double p300 = logDiff(0, logPcHit(h300, d)); + double p100 = logDiff(logPcHit(h300, d), logPcHit(h100, d)); + double p0 = logPcHit(h100, d); - double t1 = countGreat * (Math.Exp(Math.Log(attributes.GreatHitWindow) + -Math.Pow(attributes.GreatHitWindow / root2 * d, 2)) / p300); + double gradient = Math.Exp( + (countGreat * p300 + + (countOk + 0.5) * p100 + + countMiss * p0) / totalHits + ); - double t2A = Math.Exp(Math.Log(goodHitWindow) + -Math.Pow(goodHitWindow / root2 * d, 2) - logDiff(lnP300, lnP100)); - double t2B = Math.Exp(Math.Log(attributes.GreatHitWindow) + -Math.Pow(attributes.GreatHitWindow / root2 * d, 2) - logDiff(lnP300, lnP100)); - double t2 = (countOk + 1) * (t2A - t2B); - - double t3 = countMiss * Math.Exp(Math.Log(goodHitWindow) + -Math.Pow(goodHitWindow / root2 * d, 2) - lnP100); - - return t1 + t2 - t3; + return -gradient; } - double root = Brent.FindRootExpand(logLikelihoodGradient, 0.02, 0.3); + double deviation = FindMinimum.OfScalarFunction(likelihoodGradient, 30); - return 1 / root; + return deviation * 10; } private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; - private double lnErfcApprox(double x) + private double logPcHit(double x, double deviation) => logErfcApprox(x / (deviation * Math.Sqrt(2))); + + // There is a numerical approximation to increase how far you can calculate Erfc(x). + private double logErfcApprox(double x) => x <= 5 ? Math.Log(SpecialFunctions.Erfc(x)) : -Math.Pow(x, 2) - Math.Log(x) - Math.Log(Math.Sqrt(Math.PI)); + + // Log rules make subtraction of the non-log value non-trivial, this method simply subtracts the base value of 2 logs. + private double logDiff(double firstLog, double secondLog) { - if (x <= 5) - return Math.Log(SpecialFunctions.Erfc(x)); + double maxVal = Math.Max(firstLog, secondLog); - return -Math.Pow(x, 2) - Math.Log(x) - Math.Log(Math.Sqrt(Math.PI)); + // Avoid negative infinity - negative infinity (NaN) by checking if the higher value is negative infinity. + // Shouldn't ever happen, but good for redundancy purposes. + if (double.IsNegativeInfinity(maxVal)) + { + return maxVal; + } + + return firstLog + SpecialFunctions.Log1p(-Math.Exp(-(firstLog - secondLog))); } - - private double logDiff(double l1, double l2) => l1 + SpecialFunctions.Log1p(-Math.Exp(-(l1 - l2))); } } From adf16187b1d0e1ceb2520f5a3eccb7551ce7047a Mon Sep 17 00:00:00 2001 From: Natelytle Date: Wed, 22 Feb 2023 21:03:02 -0500 Subject: [PATCH 22/35] Change accuracy scaling --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 84b71a98d4..88c26e9e6a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0 || estimatedUr == null) return 0; - double accuracyValue = Math.Pow(75 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating / 2.7, 0.8) * 100.0; + double accuracyValue = Math.Pow(60 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); From 858afcd0b3ecdd10869cf540bcd7d79a9e8b1b22 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Mon, 20 Mar 2023 22:00:33 -0400 Subject: [PATCH 23/35] Pass OK hit window as a separate difficulty attribute, fix erfc approximation --- .../Difficulty/TaikoDifficultyAttributes.cs | 9 +++++++++ .../Difficulty/TaikoDifficultyCalculator.cs | 1 + .../Difficulty/TaikoPerformanceCalculator.cs | 18 ++++++------------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 72452e27b3..f5c9280590 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } + /// + /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. + /// + [JsonProperty("ok_hit_window")] + public double OkHitWindow { get; set; } + public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 24b5f5939a..90bcb74533 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -95,6 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, + OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), }; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 88c26e9e6a..e430616d54 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -12,8 +12,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; using MathNet.Numerics; -using osu.Framework.Audio.Track; -using osu.Framework.Extensions.IEnumerableExtensions; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -125,17 +123,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// private double? computeEstimatedUr(ScoreInfo score, TaikoDifficultyAttributes attributes) { - if (totalSuccessfulHits == 0 || attributes.GreatHitWindow == 0) + if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0) return null; - // Create a new track to properly calculate the hit window of 100s. - var track = new TrackVirtual(10000); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - double clockRate = track.Rate; - - double overallDifficulty = (50 - attributes.GreatHitWindow * clockRate) / 3; double h300 = attributes.GreatHitWindow; - double h100 = overallDifficulty <= 5 ? (120 - 8 * overallDifficulty) / clockRate : (80 - 6 * (overallDifficulty - 5)) / clockRate; + double h100 = attributes.OkHitWindow; // Returns the likelihood of a deviation resulting in the score's hit judgements. The peak of the curve is the most likely deviation. double likelihoodGradient(double d) @@ -167,8 +159,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double logPcHit(double x, double deviation) => logErfcApprox(x / (deviation * Math.Sqrt(2))); - // There is a numerical approximation to increase how far you can calculate Erfc(x). - private double logErfcApprox(double x) => x <= 5 ? Math.Log(SpecialFunctions.Erfc(x)) : -Math.Pow(x, 2) - Math.Log(x) - Math.Log(Math.Sqrt(Math.PI)); + // There's a numerical approximation to increase how far you can calculate ln(erfc(x)). + private double logErfcApprox(double x) => x <= 5 + ? Math.Log(SpecialFunctions.Erfc(x)) + : -Math.Pow(x, 2) - Math.Log(x * Math.Sqrt(Math.PI)); // https://www.desmos.com/calculator/kdbxwxgf01 // Log rules make subtraction of the non-log value non-trivial, this method simply subtracts the base value of 2 logs. private double logDiff(double firstLog, double secondLog) From 9aa11e00905452f9181755f3997d517c84c3365b Mon Sep 17 00:00:00 2001 From: Natelytle Date: Mon, 20 Mar 2023 23:13:33 -0400 Subject: [PATCH 24/35] update desmos --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index e430616d54..be250f3b80 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// /// Estimates the player's tap deviation based on the OD, number of objects, and number of 300s, 100s, and misses, /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings - /// will always return the same deviation. See: https://www.desmos.com/calculator/qlr946netu + /// will always return the same deviation. See: https://www.desmos.com/calculator/x3mvtir093 /// private double? computeEstimatedUr(ScoreInfo score, TaikoDifficultyAttributes attributes) { From 31c8cf09332410866c994586f38483d55010c3a2 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Thu, 27 Jul 2023 22:44:56 -0400 Subject: [PATCH 25/35] Buff accuracy scaling --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index a38eaf6a31..39246f962c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0 || estimatedUr == null) return 0; - double accuracyValue = Math.Pow(60 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; + double accuracyValue = Math.Pow(70 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 110.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); From 4de024675c1738d788238d66e88605ff5652159e Mon Sep 17 00:00:00 2001 From: Natelytle Date: Thu, 27 Jul 2023 23:02:07 -0400 Subject: [PATCH 26/35] Make comments more professional --- .../Difficulty/TaikoPerformanceCalculator.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 39246f962c..f5cdb02c82 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -115,9 +115,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } /// - /// Estimates the player's tap deviation based on the OD, number of objects, and number of 300s, 100s, and misses, - /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings - /// will always return the same deviation. See: https://www.desmos.com/calculator/x3mvtir093 + /// Calculates the tap deviation for a player using the OD, object count, and scores of 300s, 100s, and misses, with an assumed mean hit error of 0. + /// Consistency is ensured as identical SS scores on the same map and settings yield the same deviation. /// private double? computeEstimatedUr(ScoreInfo score, TaikoDifficultyAttributes attributes) { @@ -127,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double h300 = attributes.GreatHitWindow; double h100 = attributes.OkHitWindow; - // Returns the likelihood of a deviation resulting in the score's hit judgements. The peak of the curve is the most likely deviation. + // Determines the probability of a deviation leading to the score's hit evaluations. The curve's apex represents the most probable deviation. double likelihoodGradient(double d) { if (d <= 0) @@ -157,18 +156,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double logPcHit(double x, double deviation) => logErfcApprox(x / (deviation * Math.Sqrt(2))); - // There's a numerical approximation to increase how far you can calculate ln(erfc(x)). + // Utilises a numerical approximation to extend the computation range of ln(erfc(x)). private double logErfcApprox(double x) => x <= 5 ? Math.Log(SpecialFunctions.Erfc(x)) : -Math.Pow(x, 2) - Math.Log(x * Math.Sqrt(Math.PI)); // https://www.desmos.com/calculator/kdbxwxgf01 - // Log rules make subtraction of the non-log value non-trivial, this method simply subtracts the base value of 2 logs. + // Subtracts the base value of two logs, circumventing log rules that typically complicate subtraction of non-logarithmic values. private double logDiff(double firstLog, double secondLog) { double maxVal = Math.Max(firstLog, secondLog); - // Avoid negative infinity - negative infinity (NaN) by checking if the higher value is negative infinity. - // Shouldn't ever happen, but good for redundancy purposes. + // To avoid a NaN result, a check is performed to prevent subtraction of two negative infinity values. if (double.IsNegativeInfinity(maxVal)) { return maxVal; From e56942012cf24981d517ec3d50128412feb7e76b Mon Sep 17 00:00:00 2001 From: Natelytle Date: Fri, 28 Jul 2023 11:38:30 -0400 Subject: [PATCH 27/35] Serialize ok hit window attribute to db --- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 9d498d705a..451aed183d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); + yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -67,6 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty StarRating = values[ATTRIB_ID_DIFFICULTY]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; + OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 5a01faa417..b21b2cf567 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_LEGACY_ACCURACY_SCORE = 23; protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25; protected const int ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO = 27; + protected const int ATTRIB_ID_OK_HIT_WINDOW = 29; /// /// The mods which were applied to the beatmap. From 5f0020b0ca05aceeb6102d076dd7c3dfa8d59dd2 Mon Sep 17 00:00:00 2001 From: Natelytle Date: Sun, 30 Jul 2023 20:14:15 -0400 Subject: [PATCH 28/35] Reduce accuracy scaling --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index af988ef2d1..723a6612bc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0 || estimatedUr == null) return 0; - double accuracyValue = Math.Pow(70 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 110.0; + double accuracyValue = Math.Pow(65 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); From caba0510db86b66f9e238c640a7a09e272d67fdb Mon Sep 17 00:00:00 2001 From: nathen Date: Sat, 9 Mar 2024 23:10:53 -0500 Subject: [PATCH 29/35] Compute the upper bound on deviation with a 99% confidence interval --- .../Difficulty/TaikoPerformanceCalculator.cs | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 723a6612bc..322e3874ba 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - estimatedUr = computeEstimatedUr(score, taikoAttributes); + estimatedUr = computeDeviationUpperBound(taikoAttributes) * 10; // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0 || estimatedUr == null) return 0; - double accuracyValue = Math.Pow(65 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; + double accuracyValue = Math.Pow(70 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); @@ -115,10 +115,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } /// - /// Calculates the tap deviation for a player using the OD, object count, and scores of 300s, 100s, and misses, with an assumed mean hit error of 0. - /// Consistency is ensured as identical SS scores on the same map and settings yield the same deviation. + /// Computes an upper bound on the player's tap deviation based on the OD, number of circles and sliders, + /// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that + /// two SS scores on the same map with the same settings will always return the same deviation. /// - private double? computeEstimatedUr(ScoreInfo score, TaikoDifficultyAttributes attributes) + private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes) { if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0) return null; @@ -126,53 +127,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double h300 = attributes.GreatHitWindow; double h100 = attributes.OkHitWindow; - // Determines the probability of a deviation leading to the score's hit evaluations. The curve's apex represents the most probable deviation. - double likelihoodGradient(double d) + const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + + // The upper bound on deviation, calculated with the ratio of 300s to 100s, and the great hit window. + double? calcDeviationGreatWindow() { - if (d <= 0) - return 0; + if (countGreat == 0) return null; - double p300 = logDiff(0, logPcHit(h300, d)); - double p100 = logDiff(logPcHit(h300, d), logPcHit(h100, d)); - double p0 = logPcHit(h100, d); + double n = totalSuccessfulHits; - double gradient = Math.Exp( - (countGreat * p300 - + (countOk + 0.5) * p100 - + countMiss * p0) / totalHits - ); + // Proportion of greats hit, ignoring misses. + double p = countGreat / n; - return -gradient; + // 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); + + // We can be 99% confident that the deviation is not higher than: + return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } - double deviation = FindMinimum.OfScalarFunction(likelihoodGradient, 30); + // The upper bound on deviation, calculated with the ratio of 300s + 100s to misses, and the good hit window. + // This will return a lower value than the first method when the number of 100s is high, but the miss count is low. + double? calcDeviationGoodWindow() + { + if (totalSuccessfulHits == 0) return null; - return deviation * 10; + double n = totalHits; + + // Proportion of greats + goods hit. + double p = totalSuccessfulHits / 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); + + // We can be 99% confident that the deviation is not higher than: + return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + } + + if (calcDeviationGreatWindow() is null) + return calcDeviationGoodWindow(); + + return Math.Min(calcDeviationGreatWindow()!.Value, calcDeviationGoodWindow()!.Value); } private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; - - private double logPcHit(double x, double deviation) => logErfcApprox(x / (deviation * Math.Sqrt(2))); - - // Utilises a numerical approximation to extend the computation range of ln(erfc(x)). - private double logErfcApprox(double x) => x <= 5 - ? Math.Log(SpecialFunctions.Erfc(x)) - : -Math.Pow(x, 2) - Math.Log(x * Math.Sqrt(Math.PI)); // https://www.desmos.com/calculator/kdbxwxgf01 - - // Subtracts the base value of two logs, circumventing log rules that typically complicate subtraction of non-logarithmic values. - private double logDiff(double firstLog, double secondLog) - { - double maxVal = Math.Max(firstLog, secondLog); - - // To avoid a NaN result, a check is performed to prevent subtraction of two negative infinity values. - if (double.IsNegativeInfinity(maxVal)) - { - return maxVal; - } - - return firstLog + SpecialFunctions.Log1p(-Math.Exp(-(firstLog - secondLog))); - } } } From 6ddb2b7f8b1a40f3fc86892b00c67486884ac025 Mon Sep 17 00:00:00 2001 From: nathen Date: Sun, 10 Mar 2024 00:19:04 -0500 Subject: [PATCH 30/35] Include misses in the great window deviation calc --- .../Difficulty/TaikoPerformanceCalculator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 322e3874ba..8998b5cfa3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -129,12 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - // The upper bound on deviation, calculated with the ratio of 300s to 100s, and the great hit window. + // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. double? calcDeviationGreatWindow() { if (countGreat == 0) return null; - double n = totalSuccessfulHits; + double n = totalHits; // Proportion of greats hit, ignoring misses. double p = countGreat / n; @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } - // The upper bound on deviation, calculated with the ratio of 300s + 100s to misses, and the good hit window. + // The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window. // This will return a lower value than the first method when the number of 100s is high, but the miss count is low. double? calcDeviationGoodWindow() { From 537059504aa12c4dbc4a790c8dc24603ccdd94f6 Mon Sep 17 00:00:00 2001 From: nathen Date: Sun, 10 Mar 2024 00:20:06 -0500 Subject: [PATCH 31/35] Fix comment --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 8998b5cfa3..c532d24f4d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double n = totalHits; - // Proportion of greats hit, ignoring misses. + // Proportion of greats hit. double p = countGreat / n; // We can be 99% confident that p is at least this value. From a9b3416a3fe2e77f66c249c85636ca476ac3027c Mon Sep 17 00:00:00 2001 From: nathen Date: Sun, 10 Mar 2024 00:37:28 -0500 Subject: [PATCH 32/35] Remove MathNet.Numerics dependency --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- .../osu.Game.Rulesets.Taiko.csproj | 4 - osu.Game/Utils/SpecialFunctions.cs | 694 ++++++++++++++++++ 3 files changed, 695 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Utils/SpecialFunctions.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c532d24f4d..ca11397801 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; -using MathNet.Numerics; +using osu.Game.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty { diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index 6ad5425d5b..cacba55c2a 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -15,8 +15,4 @@ - - - - diff --git a/osu.Game/Utils/SpecialFunctions.cs b/osu.Game/Utils/SpecialFunctions.cs new file mode 100644 index 0000000000..0b0f0598bb --- /dev/null +++ b/osu.Game/Utils/SpecialFunctions.cs @@ -0,0 +1,694 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +// All code is referenced from the following: +// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/SpecialFunctions/Erf.cs +// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Optimization/NelderMeadSimplex.cs + +/* + Copyright (c) 2002-2022 Math.NET +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using System; + +namespace osu.Game.Utils +{ + public class SpecialFunctions + { + private const double sqrt2_pi = 2.5066282746310005024157652848110452530069867406099d; + + /// + /// ************************************** + /// COEFFICIENTS FOR METHOD ErfImp * + /// ************************************** + /// + /// Polynomial coefficients for a numerator of ErfImp + /// calculation for Erf(x) in the interval [1e-10, 0.5]. + /// + private static readonly double[] erf_imp_an = { 0.00337916709551257388990745, -0.00073695653048167948530905, -0.374732337392919607868241, 0.0817442448733587196071743, -0.0421089319936548595203468, 0.0070165709512095756344528, -0.00495091255982435110337458, 0.000871646599037922480317225 }; + + /// Polynomial coefficients for a denominator of ErfImp + /// calculation for Erf(x) in the interval [1e-10, 0.5]. + /// + private static readonly double[] erf_imp_ad = { 1, -0.218088218087924645390535, 0.412542972725442099083918, -0.0841891147873106755410271, 0.0655338856400241519690695, -0.0120019604454941768171266, 0.00408165558926174048329689, -0.000615900721557769691924509 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [0.5, 0.75]. + /// + private static readonly double[] erf_imp_bn = { -0.0361790390718262471360258, 0.292251883444882683221149, 0.281447041797604512774415, 0.125610208862766947294894, 0.0274135028268930549240776, 0.00250839672168065762786937 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [0.5, 0.75]. + /// + private static readonly double[] erf_imp_bd = { 1, 1.8545005897903486499845, 1.43575803037831418074962, 0.582827658753036572454135, 0.124810476932949746447682, 0.0113724176546353285778481 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [0.75, 1.25]. + /// + private static readonly double[] erf_imp_cn = { -0.0397876892611136856954425, 0.153165212467878293257683, 0.191260295600936245503129, 0.10276327061989304213645, 0.029637090615738836726027, 0.0046093486780275489468812, 0.000307607820348680180548455 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [0.75, 1.25]. + /// + private static readonly double[] erf_imp_cd = { 1, 1.95520072987627704987886, 1.64762317199384860109595, 0.768238607022126250082483, 0.209793185936509782784315, 0.0319569316899913392596356, 0.00213363160895785378615014 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [1.25, 2.25]. + /// + private static readonly double[] erf_imp_dn = { -0.0300838560557949717328341, 0.0538578829844454508530552, 0.0726211541651914182692959, 0.0367628469888049348429018, 0.00964629015572527529605267, 0.00133453480075291076745275, 0.778087599782504251917881e-4 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [1.25, 2.25]. + /// + private static readonly double[] erf_imp_dd = { 1, 1.75967098147167528287343, 1.32883571437961120556307, 0.552528596508757581287907, 0.133793056941332861912279, 0.0179509645176280768640766, 0.00104712440019937356634038, -0.106640381820357337177643e-7 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [2.25, 3.5]. + /// + private static readonly double[] erf_imp_en = { -0.0117907570137227847827732, 0.014262132090538809896674, 0.0202234435902960820020765, 0.00930668299990432009042239, 0.00213357802422065994322516, 0.00025022987386460102395382, 0.120534912219588189822126e-4 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [2.25, 3.5]. + /// + private static readonly double[] erf_imp_ed = { 1, 1.50376225203620482047419, 0.965397786204462896346934, 0.339265230476796681555511, 0.0689740649541569716897427, 0.00771060262491768307365526, 0.000371421101531069302990367 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [3.5, 5.25]. + /// + private static readonly double[] erf_imp_fn = { -0.00546954795538729307482955, 0.00404190278731707110245394, 0.0054963369553161170521356, 0.00212616472603945399437862, 0.000394984014495083900689956, 0.365565477064442377259271e-4, 0.135485897109932323253786e-5 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [3.5, 5.25]. + /// + private static readonly double[] erf_imp_fd = { 1, 1.21019697773630784832251, 0.620914668221143886601045, 0.173038430661142762569515, 0.0276550813773432047594539, 0.00240625974424309709745382, 0.891811817251336577241006e-4, -0.465528836283382684461025e-11 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [5.25, 8]. + /// + private static readonly double[] erf_imp_gn = { -0.00270722535905778347999196, 0.0013187563425029400461378, 0.00119925933261002333923989, 0.00027849619811344664248235, 0.267822988218331849989363e-4, 0.923043672315028197865066e-6 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [5.25, 8]. + /// + private static readonly double[] erf_imp_gd = { 1, 0.814632808543141591118279, 0.268901665856299542168425, 0.0449877216103041118694989, 0.00381759663320248459168994, 0.000131571897888596914350697, 0.404815359675764138445257e-11 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [8, 11.5]. + /// + private static readonly double[] erf_imp_hn = { -0.00109946720691742196814323, 0.000406425442750422675169153, 0.000274499489416900707787024, 0.465293770646659383436343e-4, 0.320955425395767463401993e-5, 0.778286018145020892261936e-7 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [8, 11.5]. + /// + private static readonly double[] erf_imp_hd = { 1, 0.588173710611846046373373, 0.139363331289409746077541, 0.0166329340417083678763028, 0.00100023921310234908642639, 0.24254837521587225125068e-4 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [11.5, 17]. + /// + private static readonly double[] erf_imp_in = { -0.00056907993601094962855594, 0.000169498540373762264416984, 0.518472354581100890120501e-4, 0.382819312231928859704678e-5, 0.824989931281894431781794e-7 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [11.5, 17]. + /// + private static readonly double[] erf_imp_id = { 1, 0.339637250051139347430323, 0.043472647870310663055044, 0.00248549335224637114641629, 0.535633305337152900549536e-4, -0.117490944405459578783846e-12 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [17, 24]. + /// + private static readonly double[] erf_imp_jn = { -0.000241313599483991337479091, 0.574224975202501512365975e-4, 0.115998962927383778460557e-4, 0.581762134402593739370875e-6, 0.853971555085673614607418e-8 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [17, 24]. + /// + private static readonly double[] erf_imp_jd = { 1, 0.233044138299687841018015, 0.0204186940546440312625597, 0.000797185647564398289151125, 0.117019281670172327758019e-4 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [24, 38]. + /// + private static readonly double[] erf_imp_kn = { -0.000146674699277760365803642, 0.162666552112280519955647e-4, 0.269116248509165239294897e-5, 0.979584479468091935086972e-7, 0.101994647625723465722285e-8 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [24, 38]. + /// + private static readonly double[] erf_imp_kd = { 1, 0.165907812944847226546036, 0.0103361716191505884359634, 0.000286593026373868366935721, 0.298401570840900340874568e-5 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [38, 60]. + /// + private static readonly double[] erf_imp_ln = { -0.583905797629771786720406e-4, 0.412510325105496173512992e-5, 0.431790922420250949096906e-6, 0.993365155590013193345569e-8, 0.653480510020104699270084e-10 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [38, 60]. + /// + private static readonly double[] erf_imp_ld = { 1, 0.105077086072039915406159, 0.00414278428675475620830226, 0.726338754644523769144108e-4, 0.477818471047398785369849e-6 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [60, 85]. + /// + private static readonly double[] erf_imp_mn = { -0.196457797609229579459841e-4, 0.157243887666800692441195e-5, 0.543902511192700878690335e-7, 0.317472492369117710852685e-9 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [60, 85]. + /// + private static readonly double[] erf_imp_md = { 1, 0.052803989240957632204885, 0.000926876069151753290378112, 0.541011723226630257077328e-5, 0.535093845803642394908747e-15 }; + + /// Polynomial coefficients for a numerator in ErfImp + /// calculation for Erfc(x) in the interval [85, 110]. + /// + private static readonly double[] erf_imp_nn = { -0.789224703978722689089794e-5, 0.622088451660986955124162e-6, 0.145728445676882396797184e-7, 0.603715505542715364529243e-10 }; + + /// Polynomial coefficients for a denominator in ErfImp + /// calculation for Erfc(x) in the interval [85, 110]. + /// + private static readonly double[] erf_imp_nd = { 1, 0.0375328846356293715248719, 0.000467919535974625308126054, 0.193847039275845656900547e-5 }; + + /// + /// ************************************** + /// COEFFICIENTS FOR METHOD ErfInvImp * + /// ************************************** + /// + /// Polynomial coefficients for a numerator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0, 0.5]. + /// + private static readonly double[] erv_inv_imp_an = { -0.000508781949658280665617, -0.00836874819741736770379, 0.0334806625409744615033, -0.0126926147662974029034, -0.0365637971411762664006, 0.0219878681111168899165, 0.00822687874676915743155, -0.00538772965071242932965 }; + + /// Polynomial coefficients for a denominator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0, 0.5]. + /// + private static readonly double[] erv_inv_imp_ad = { 1, -0.970005043303290640362, -1.56574558234175846809, 1.56221558398423026363, 0.662328840472002992063, -0.71228902341542847553, -0.0527396382340099713954, 0.0795283687341571680018, -0.00233393759374190016776, 0.000886216390456424707504 }; + + /// Polynomial coefficients for a numerator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.5, 0.75]. + /// + private static readonly double[] erv_inv_imp_bn = { -0.202433508355938759655, 0.105264680699391713268, 8.37050328343119927838, 17.6447298408374015486, -18.8510648058714251895, -44.6382324441786960818, 17.445385985570866523, 21.1294655448340526258, -3.67192254707729348546 }; + + /// Polynomial coefficients for a denominator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.5, 0.75]. + /// + private static readonly double[] erv_inv_imp_bd = { 1, 6.24264124854247537712, 3.9713437953343869095, -28.6608180499800029974, -20.1432634680485188801, 48.5609213108739935468, 10.8268667355460159008, -22.6436933413139721736, 1.72114765761200282724 }; + + /// Polynomial coefficients for a numerator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x less than 3. + /// + private static readonly double[] erv_inv_imp_cn = { -0.131102781679951906451, -0.163794047193317060787, 0.117030156341995252019, 0.387079738972604337464, 0.337785538912035898924, 0.142869534408157156766, 0.0290157910005329060432, 0.00214558995388805277169, -0.679465575181126350155e-6, 0.285225331782217055858e-7, -0.681149956853776992068e-9 }; + + /// Polynomial coefficients for a denominator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x less than 3. + /// + private static readonly double[] erv_inv_imp_cd = { 1, 3.46625407242567245975, 5.38168345707006855425, 4.77846592945843778382, 2.59301921623620271374, 0.848854343457902036425, 0.152264338295331783612, 0.01105924229346489121 }; + + /// Polynomial coefficients for a numerator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 3 and 6. + /// + private static readonly double[] erv_inv_imp_dn = { -0.0350353787183177984712, -0.00222426529213447927281, 0.0185573306514231072324, 0.00950804701325919603619, 0.00187123492819559223345, 0.000157544617424960554631, 0.460469890584317994083e-5, -0.230404776911882601748e-9, 0.266339227425782031962e-11 }; + + /// Polynomial coefficients for a denominator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 3 and 6. + /// + private static readonly double[] erv_inv_imp_dd = { 1, 1.3653349817554063097, 0.762059164553623404043, 0.220091105764131249824, 0.0341589143670947727934, 0.00263861676657015992959, 0.764675292302794483503e-4 }; + + /// Polynomial coefficients for a numerator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 6 and 18. + /// + private static readonly double[] erv_inv_imp_en = { -0.0167431005076633737133, -0.00112951438745580278863, 0.00105628862152492910091, 0.000209386317487588078668, 0.149624783758342370182e-4, 0.449696789927706453732e-6, 0.462596163522878599135e-8, -0.281128735628831791805e-13, 0.99055709973310326855e-16 }; + + /// Polynomial coefficients for a denominator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 6 and 18. + /// + private static readonly double[] erv_inv_imp_ed = { 1, 0.591429344886417493481, 0.138151865749083321638, 0.0160746087093676504695, 0.000964011807005165528527, 0.275335474764726041141e-4, 0.282243172016108031869e-6 }; + + /// Polynomial coefficients for a numerator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 18 and 44. + /// + private static readonly double[] erv_inv_imp_fn = { -0.0024978212791898131227, -0.779190719229053954292e-5, 0.254723037413027451751e-4, 0.162397777342510920873e-5, 0.396341011304801168516e-7, 0.411632831190944208473e-9, 0.145596286718675035587e-11, -0.116765012397184275695e-17 }; + + /// Polynomial coefficients for a denominator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 18 and 44. + /// + private static readonly double[] erv_inv_imp_fd = { 1, 0.207123112214422517181, 0.0169410838120975906478, 0.000690538265622684595676, 0.145007359818232637924e-4, 0.144437756628144157666e-6, 0.509761276599778486139e-9 }; + + /// Polynomial coefficients for a numerator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x greater than 44. + /// + private static readonly double[] erv_inv_imp_gn = { -0.000539042911019078575891, -0.28398759004727721098e-6, 0.899465114892291446442e-6, 0.229345859265920864296e-7, 0.225561444863500149219e-9, 0.947846627503022684216e-12, 0.135880130108924861008e-14, -0.348890393399948882918e-21 }; + + /// Polynomial coefficients for a denominator of ErfInvImp + /// calculation for Erf^-1(z) in the interval [0.75, 1] with x greater than 44. + /// + private static readonly double[] erv_inv_imp_gd = { 1, 0.0845746234001899436914, 0.00282092984726264681981, 0.468292921940894236786e-4, 0.399968812193862100054e-6, 0.161809290887904476097e-8, 0.231558608310259605225e-11 }; + + /// Calculates the error function. + /// The value to evaluate. + /// the error function evaluated at given value. + /// + /// + /// returns 1 if x == double.PositiveInfinity. + /// returns -1 if x == double.NegativeInfinity. + /// + /// + public static double Erf(double x) + { + if (x == 0) + { + return 0; + } + + if (double.IsPositiveInfinity(x)) + { + return 1; + } + + if (double.IsNegativeInfinity(x)) + { + return -1; + } + + if (double.IsNaN(x)) + { + return double.NaN; + } + + return erfImp(x, false); + } + + /// Calculates the complementary error function. + /// The value to evaluate. + /// the complementary error function evaluated at given value. + /// + /// + /// returns 0 if x == double.PositiveInfinity. + /// returns 2 if x == double.NegativeInfinity. + /// + /// + public static double Erfc(double x) + { + if (x == 0) + { + return 1; + } + + if (double.IsPositiveInfinity(x)) + { + return 0; + } + + if (double.IsNegativeInfinity(x)) + { + return 2; + } + + if (double.IsNaN(x)) + { + return double.NaN; + } + + return erfImp(x, true); + } + + /// Calculates the inverse error function evaluated at z. + /// The inverse error function evaluated at given value. + /// + /// + /// returns double.PositiveInfinity if z >= 1.0. + /// returns double.NegativeInfinity if z <= -1.0. + /// + /// + /// Calculates the inverse error function evaluated at z. + /// value to evaluate. + /// the inverse error function evaluated at Z. + public static double ErfInv(double z) + { + if (z == 0.0) + { + return 0.0; + } + + if (z >= 1.0) + { + return double.PositiveInfinity; + } + + if (z <= -1.0) + { + return double.NegativeInfinity; + } + + double p, q, s; + + if (z < 0) + { + p = -z; + q = 1 - p; + s = -1; + } + else + { + p = z; + q = 1 - z; + s = 1; + } + + return erfInvImpl(p, q, s); + } + + /// + /// Implementation of the error function. + /// + /// Where to evaluate the error function. + /// Whether to compute 1 - the error function. + /// the error function. + private static double erfImp(double z, bool invert) + { + if (z < 0) + { + if (!invert) + { + return -erfImp(-z, false); + } + + if (z < -0.5) + { + return 2 - erfImp(-z, true); + } + + return 1 + erfImp(-z, false); + } + + double result; + + // Big bunch of selection statements now to pick which + // implementation to use, try to put most likely options + // first: + if (z < 0.5) + { + // We're going to calculate erf: + if (z < 1e-10) + { + result = (z * 1.125) + (z * 0.003379167095512573896158903121545171688); + } + else + { + // Worst case absolute error found: 6.688618532e-21 + result = (z * 1.125) + (z * evaluatePolynomial(z, erf_imp_an) / evaluatePolynomial(z, erf_imp_ad)); + } + } + else if (z < 110) + { + // We'll be calculating erfc: + invert = !invert; + double r, b; + + if (z < 0.75) + { + // Worst case absolute error found: 5.582813374e-21 + r = evaluatePolynomial(z - 0.5, erf_imp_bn) / evaluatePolynomial(z - 0.5, erf_imp_bd); + b = 0.3440242112F; + } + else if (z < 1.25) + { + // Worst case absolute error found: 4.01854729e-21 + r = evaluatePolynomial(z - 0.75, erf_imp_cn) / evaluatePolynomial(z - 0.75, erf_imp_cd); + b = 0.419990927F; + } + else if (z < 2.25) + { + // Worst case absolute error found: 2.866005373e-21 + r = evaluatePolynomial(z - 1.25, erf_imp_dn) / evaluatePolynomial(z - 1.25, erf_imp_dd); + b = 0.4898625016F; + } + else if (z < 3.5) + { + // Worst case absolute error found: 1.045355789e-21 + r = evaluatePolynomial(z - 2.25, erf_imp_en) / evaluatePolynomial(z - 2.25, erf_imp_ed); + b = 0.5317370892F; + } + else if (z < 5.25) + { + // Worst case absolute error found: 8.300028706e-22 + r = evaluatePolynomial(z - 3.5, erf_imp_fn) / evaluatePolynomial(z - 3.5, erf_imp_fd); + b = 0.5489973426F; + } + else if (z < 8) + { + // Worst case absolute error found: 1.700157534e-21 + r = evaluatePolynomial(z - 5.25, erf_imp_gn) / evaluatePolynomial(z - 5.25, erf_imp_gd); + b = 0.5571740866F; + } + else if (z < 11.5) + { + // Worst case absolute error found: 3.002278011e-22 + r = evaluatePolynomial(z - 8, erf_imp_hn) / evaluatePolynomial(z - 8, erf_imp_hd); + b = 0.5609807968F; + } + else if (z < 17) + { + // Worst case absolute error found: 6.741114695e-21 + r = evaluatePolynomial(z - 11.5, erf_imp_in) / evaluatePolynomial(z - 11.5, erf_imp_id); + b = 0.5626493692F; + } + else if (z < 24) + { + // Worst case absolute error found: 7.802346984e-22 + r = evaluatePolynomial(z - 17, erf_imp_jn) / evaluatePolynomial(z - 17, erf_imp_jd); + b = 0.5634598136F; + } + else if (z < 38) + { + // Worst case absolute error found: 2.414228989e-22 + r = evaluatePolynomial(z - 24, erf_imp_kn) / evaluatePolynomial(z - 24, erf_imp_kd); + b = 0.5638477802F; + } + else if (z < 60) + { + // Worst case absolute error found: 5.896543869e-24 + r = evaluatePolynomial(z - 38, erf_imp_ln) / evaluatePolynomial(z - 38, erf_imp_ld); + b = 0.5640528202F; + } + else if (z < 85) + { + // Worst case absolute error found: 3.080612264e-21 + r = evaluatePolynomial(z - 60, erf_imp_mn) / evaluatePolynomial(z - 60, erf_imp_md); + b = 0.5641309023F; + } + else + { + // Worst case absolute error found: 8.094633491e-22 + r = evaluatePolynomial(z - 85, erf_imp_nn) / evaluatePolynomial(z - 85, erf_imp_nd); + b = 0.5641584396F; + } + + double g = Math.Exp(-z * z) / z; + result = (g * b) + (g * r); + } + else + { + // Any value of z larger than 28 will underflow to zero: + result = 0; + invert = !invert; + } + + if (invert) + { + result = 1 - result; + } + + return result; + } + + /// Calculates the complementary inverse error function evaluated at z. + /// The complementary inverse error function evaluated at given value. + /// We have tested this implementation against the arbitrary precision mpmath library + /// and found cases where we can only guarantee 9 significant figures correct. + /// + /// returns double.PositiveInfinity if z <= 0.0. + /// returns double.NegativeInfinity if z >= 2.0. + /// + /// + /// calculates the complementary inverse error function evaluated at z. + /// value to evaluate. + /// the complementary inverse error function evaluated at Z. + public static double ErfcInv(double z) + { + if (z <= 0.0) + { + return double.PositiveInfinity; + } + + if (z >= 2.0) + { + return double.NegativeInfinity; + } + + double p, q, s; + + if (z > 1) + { + q = 2 - z; + p = 1 - q; + s = -1; + } + else + { + p = 1 - z; + q = z; + s = 1; + } + + return erfInvImpl(p, q, s); + } + + /// + /// The implementation of the inverse error function. + /// + /// First intermediate parameter. + /// Second intermediate parameter. + /// Third intermediate parameter. + /// the inverse error function. + private static double erfInvImpl(double p, double q, double s) + { + double result; + + if (p <= 0.5) + { + // Evaluate inverse erf using the rational approximation: + // + // x = p(p+10)(Y+R(p)) + // + // Where Y is a constant, and R(p) is optimized for a low + // absolute error compared to |Y|. + // + // double: Max error found: 2.001849e-18 + // long double: Max error found: 1.017064e-20 + // Maximum Deviation Found (actual error term at infinite precision) 8.030e-21 + const float y = 0.0891314744949340820313f; + double g = p * (p + 10); + double r = evaluatePolynomial(p, erv_inv_imp_an) / evaluatePolynomial(p, erv_inv_imp_ad); + result = (g * y) + (g * r); + } + else if (q >= 0.25) + { + // Rational approximation for 0.5 > q >= 0.25 + // + // x = sqrt(-2*log(q)) / (Y + R(q)) + // + // Where Y is a constant, and R(q) is optimized for a low + // absolute error compared to Y. + // + // double : Max error found: 7.403372e-17 + // long double : Max error found: 6.084616e-20 + // Maximum Deviation Found (error term) 4.811e-20 + const float y = 2.249481201171875f; + double g = Math.Sqrt(-2 * Math.Log(q)); + double xs = q - 0.25; + double r = evaluatePolynomial(xs, erv_inv_imp_bn) / evaluatePolynomial(xs, erv_inv_imp_bd); + result = g / (y + r); + } + else + { + // For q < 0.25 we have a series of rational approximations all + // of the general form: + // + // let: x = sqrt(-log(q)) + // + // Then the result is given by: + // + // x(Y+R(x-B)) + // + // where Y is a constant, B is the lowest value of x for which + // the approximation is valid, and R(x-B) is optimized for a low + // absolute error compared to Y. + // + // Note that almost all code will really go through the first + // or maybe second approximation. After than we're dealing with very + // small input values indeed: 80 and 128 bit long double's go all the + // way down to ~ 1e-5000 so the "tail" is rather long... + double x = Math.Sqrt(-Math.Log(q)); + + if (x < 3) + { + // Max error found: 1.089051e-20 + const float y = 0.807220458984375f; + double xs = x - 1.125; + double r = evaluatePolynomial(xs, erv_inv_imp_cn) / evaluatePolynomial(xs, erv_inv_imp_cd); + result = (y * x) + (r * x); + } + else if (x < 6) + { + // Max error found: 8.389174e-21 + const float y = 0.93995571136474609375f; + double xs = x - 3; + double r = evaluatePolynomial(xs, erv_inv_imp_dn) / evaluatePolynomial(xs, erv_inv_imp_dd); + result = (y * x) + (r * x); + } + else if (x < 18) + { + // Max error found: 1.481312e-19 + const float y = 0.98362827301025390625f; + double xs = x - 6; + double r = evaluatePolynomial(xs, erv_inv_imp_en) / evaluatePolynomial(xs, erv_inv_imp_ed); + result = (y * x) + (r * x); + } + else if (x < 44) + { + // Max error found: 5.697761e-20 + const float y = 0.99714565277099609375f; + double xs = x - 18; + double r = evaluatePolynomial(xs, erv_inv_imp_fn) / evaluatePolynomial(xs, erv_inv_imp_fd); + result = (y * x) + (r * x); + } + else + { + // Max error found: 1.279746e-20 + const float y = 0.99941349029541015625f; + double xs = x - 44; + double r = evaluatePolynomial(xs, erv_inv_imp_gn) / evaluatePolynomial(xs, erv_inv_imp_gd); + result = (y * x) + (r * x); + } + } + + return s * result; + } + + /// + /// Evaluate a polynomial at point x. + /// Coefficients are ordered ascending by power with power k at index k. + /// Example: coefficients [3,-1,2] represent y=2x^2-x+3. + /// + /// The location where to evaluate the polynomial at. + /// The coefficients of the polynomial, coefficient for power k at index k. + /// + /// is a null reference. + /// + private static double evaluatePolynomial(double z, params double[] coefficients) + { + // 2020-10-07 jbialogrodzki #730 Since this is public API we should probably + // handle null arguments? It doesn't seem to have been done consistently in this class though. + if (coefficients == null) + { + throw new ArgumentNullException(nameof(coefficients)); + } + + // 2020-10-07 jbialogrodzki #730 Zero polynomials need explicit handling. + // Without this check, we attempted to peek coefficients at negative indices! + int n = coefficients.Length; + + if (n == 0) + { + return 0; + } + + double sum = coefficients[n - 1]; + + for (int i = n - 2; i >= 0; --i) + { + sum *= z; + sum += coefficients[i]; + } + + return sum; + } + } +} From 17145673421118baee2d6971277771625512a78a Mon Sep 17 00:00:00 2001 From: Nathen Date: Wed, 29 May 2024 09:40:39 -0400 Subject: [PATCH 33/35] Save deviation calculations to variables --- .../Difficulty/TaikoPerformanceCalculator.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ca11397801..a8ccaedfd0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -129,6 +129,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + double? deviationGreatWindow = calcDeviationGreatWindow(); + double? deviationGoodWindow = calcDeviationGoodWindow(); + + if (deviationGreatWindow is null) + return deviationGoodWindow; + + return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); + // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. double? calcDeviationGreatWindow() { @@ -163,11 +171,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // We can be 99% confident that the deviation is not higher than: return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } - - if (calcDeviationGreatWindow() is null) - return calcDeviationGoodWindow(); - - return Math.Min(calcDeviationGreatWindow()!.Value, calcDeviationGoodWindow()!.Value); } private int totalHits => countGreat + countOk + countMeh + countMiss; From f8f18b6cbd49750916c4fb75b85f0ad93e2f98c4 Mon Sep 17 00:00:00 2001 From: Nathen Date: Wed, 29 May 2024 09:40:59 -0400 Subject: [PATCH 34/35] Fix naming convention --- .../Difficulty/TaikoPerformanceAttributes.cs | 4 ++-- .../Difficulty/TaikoPerformanceCalculator.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 6d85aa8f3d..7c74e43db1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } - [JsonProperty("estimated_ur")] - public double? EstimatedUr { get; set; } + [JsonProperty("estimated_unstable_rate")] + public double? EstimatedUnstableRate { get; set; } public override IEnumerable GetAttributesForDisplay() { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index a8ccaedfd0..ab809a0ade 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countOk; private int countMeh; private int countMiss; - private double? estimatedUr; + private double? estimatedUnstableRate; private double effectiveMissCount; @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - estimatedUr = computeDeviationUpperBound(taikoAttributes) * 10; + estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10; // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Difficulty = difficultyValue, Accuracy = accuracyValue, EffectiveMissCount = effectiveMissCount, - EstimatedUr = estimatedUr, + EstimatedUnstableRate = estimatedUnstableRate, Total = totalValue }; } @@ -92,18 +92,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - if (estimatedUr == null) + if (estimatedUnstableRate == null) return 0; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUr.Value)), 2.0); + return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUnstableRate.Value)), 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) { - if (attributes.GreatHitWindow <= 0 || estimatedUr == null) + if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null) return 0; - double accuracyValue = Math.Pow(70 / estimatedUr.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; + double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); From 2fb22f1febef5ad5ab120c24875e69b798ed984e Mon Sep 17 00:00:00 2001 From: Nathen Date: Sun, 23 Jun 2024 19:17:19 -0400 Subject: [PATCH 35/35] Move the return value for deviation below the local functions --- .../Difficulty/TaikoPerformanceCalculator.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ab809a0ade..e42b015176 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -129,14 +129,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - double? deviationGreatWindow = calcDeviationGreatWindow(); - double? deviationGoodWindow = calcDeviationGoodWindow(); - - if (deviationGreatWindow is null) - return deviationGoodWindow; - - return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); - // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. double? calcDeviationGreatWindow() { @@ -171,6 +163,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // We can be 99% confident that the deviation is not higher than: return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } + + double? deviationGreatWindow = calcDeviationGreatWindow(); + double? deviationGoodWindow = calcDeviationGoodWindow(); + + if (deviationGreatWindow is null) + return deviationGoodWindow; + + return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); } private int totalHits => countGreat + countOk + countMeh + countMiss;