Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UR estimation to the osu!mania ruleset #22613

Open
wants to merge 98 commits into
base: pp-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
7882423
add deviation estimation to mania's perfcalc
Natelytle Oct 13, 2022
4af6c26
add UR attribute, change initial min to 300UR
Natelytle Oct 13, 2022
6ea6ba2
bound deviation estimate to numbers greater than 0
Natelytle Oct 13, 2022
3d80168
fix hitwindows
Natelytle Oct 13, 2022
f4c7e9d
account for converts, add note counts
Natelytle Oct 13, 2022
dfd386c
use onlineid for contingency
Natelytle Oct 13, 2022
5296fb5
fix convert hit windows
Natelytle Oct 15, 2022
562954a
account for HR/EZ in deviation calculation
Natelytle Oct 15, 2022
49495db
use erfc approximation
Natelytle Oct 24, 2022
dd1754f
Add rudimentary difficulty scaling curve
Natelytle Oct 27, 2022
79b3541
Change curve
Natelytle Oct 27, 2022
3a51c53
Don't give PP to jittered plays
Natelytle Oct 29, 2022
fb7cdc4
merge master and change hit window formulas
Natelytle Jan 23, 2023
c5233c7
change hit window formulas
Natelytle Jan 23, 2023
8e78adb
fix judgement
Natelytle Jan 23, 2023
4a5b14f
lazer deviation is finally accurate woo
Natelytle Jan 24, 2023
82bf3e7
increase totalhits by 1
Natelytle Jan 24, 2023
522271b
quick legacy LN idea
Natelytle Jan 24, 2023
3a78b9e
Fix formatting
Natelytle Jan 27, 2023
6f5e03c
change legacy LN calculation
Natelytle Jan 27, 2023
c331058
switch to totalvalue
Natelytle Jan 27, 2023
bc7fd8d
add comments
Natelytle Jan 27, 2023
80128d2
rewrite everything with erf for simplicity
Natelytle Jan 31, 2023
91fe6a5
change isLegacy toggle
Natelytle Jan 31, 2023
8fb84c2
whop
Natelytle Jan 31, 2023
68cea94
readability bump
Natelytle Feb 6, 2023
4535b29
rename method
Natelytle Feb 6, 2023
7196325
Merge remote-tracking branch 'master/master' into maniastatacc
Natelytle Feb 7, 2023
b0cd729
fix convert OD with lazer judgements
Natelytle Feb 12, 2023
2b5ab41
Revert unrelated files changed in formatting commit
Natelytle Feb 12, 2023
fde20e1
Add desmos links to likelihood functions
Natelytle Feb 12, 2023
5a0aa24
Code quality overhaul
Natelytle Feb 13, 2023
9ff4c1b
fix const name
Natelytle Feb 13, 2023
01dbce6
More formatting
Natelytle Feb 13, 2023
db0fda0
Remove pointless erf approximation, fix lazer return statement
Natelytle Feb 16, 2023
2d7c99b
Massively improve estimation accuracy using a folded distribution
Natelytle Feb 17, 2023
33a73e3
update desmos, remove redundant parenthesis
Natelytle Feb 18, 2023
97da8cc
Reformatting, increase precision a lot by rewriting in terms of the l…
Natelytle Feb 19, 2023
08ceb02
Rename "Judgements" to "HitWindows"
Natelytle Feb 19, 2023
35119bc
Address reviews
Natelytle Feb 19, 2023
06a2358
account for mania tests
Natelytle Feb 19, 2023
11f6fd0
Fix issue with high misscount plays returning 115 UR
Natelytle Feb 20, 2023
15ca535
Fix incorrect logDiff function, make lazer tail multiplier a const
Natelytle Feb 20, 2023
1e272f1
Address abraker's review, add approximation for legacy LN tails
Natelytle Feb 21, 2023
bafb8f6
Address abraker's review, add approximation for legacy LN tails
Natelytle Feb 21, 2023
1f76d48
Merge remote-tracking branch 'origin/maniastatacc' into maniastatacc
Natelytle Feb 21, 2023
4a8d679
Fix NaNs (whoops)
Natelytle Feb 21, 2023
3a1c479
Avoid repeated code using structs and methods
Natelytle Feb 24, 2023
66529f6
Add comments
Natelytle Feb 24, 2023
bfc642e
Change types, remove pointless duplicate likelihood function
Natelytle Feb 25, 2023
ab8d19e
Fix erfc approximation
Natelytle Feb 25, 2023
6399b9b
expose hit windows publicly for testing purposes
Natelytle Mar 6, 2023
32a3878
Add tests
Natelytle Apr 16, 2023
d0d0aae
Edit comments
Natelytle Apr 16, 2023
34c2cc0
Add comments and fail messages to UR estimation tests
Natelytle Apr 16, 2023
4affdc6
Change UR scaling curve
Natelytle Apr 16, 2023
a10dccf
Revert to live scaling
Natelytle Apr 16, 2023
d44d893
Rename to IsConvert, add handling for converts below od4
Natelytle Apr 16, 2023
9de2585
Move comment
Natelytle Apr 16, 2023
6ace77a
Revert back to previous curve, add more tests
Natelytle Apr 17, 2023
ec19983
Update desmos
Natelytle Apr 17, 2023
d8d4cc7
Change curve to clamp PP to <700ur
Natelytle Apr 17, 2023
18cef8b
Change hit window test to not require a beatmap, test multiple ODs
Natelytle Apr 18, 2023
69b99f6
Change curve (again) to fix jittering edge cases
Natelytle Apr 18, 2023
6e3e522
Merge remote-tracking branch 'osumaster/master' into maniastatacc
Natelytle Apr 22, 2023
0a72493
Band-aid math error on stable LNs, add tail deviation multiplier
Natelytle Jul 30, 2023
a58470d
Fix stable LN logic
Natelytle Jul 31, 2023
f60317c
Nest Ternary into Math.Log
Eve-ning Jul 31, 2023
951861a
Shorten property fetching code
Eve-ning Jul 31, 2023
20de476
Fix incorrect function name logPNote
Eve-ning Jul 31, 2023
7ed9141
Fix incorrect legacyLogPHold function name
Eve-ning Jul 31, 2023
c20e150
Merge master
Natelytle Aug 1, 2023
0ed0915
Collapse boilerplate code to parse judgements
Eve-ning Aug 1, 2023
e9b02c2
Remove resource dependency on test
Eve-ning Aug 1, 2023
b131ac8
Move test case code closer to test callables
Eve-ning Aug 1, 2023
1d7dd8b
Expose getHitWindows for tests
Eve-ning Aug 1, 2023
515ff55
Narrow scope of hitWindows fns
Eve-ning Aug 1, 2023
d00d7fd
Narrow scope of computeEstimatedUr
Eve-ning Aug 1, 2023
3dea273
Update changed values
Eve-ning Aug 2, 2023
b2ad8ad
Add remark on null & exception throwable
Eve-ning Aug 2, 2023
35f6ffe
Improve testing
Eve-ning Aug 2, 2023
5864d87
Add comment justifying args on TestEdge
Eve-ning Aug 2, 2023
104eea3
Delete ur-estimation-test.osu
Eve-ning Aug 2, 2023
da36c2a
Remove unresolvable cref
Eve-ning Aug 2, 2023
dd4a3ec
Add test for More Max Less UR
Eve-ning Aug 2, 2023
9cc6a3a
Switch to using deviation on notes and heads for multiplier scaling
Natelytle Aug 2, 2023
95c7237
Remove IsConvert difficulty attribute
smoogipoo Aug 7, 2023
a2197e2
Store difficulty attribute values
smoogipoo Aug 7, 2023
a05c3b8
Update ManiaUnstableRateEstimationTest.cs
Eve-ning Aug 14, 2023
4eb307b
Merge pull request #1 from Eve-ning/maniastatacc-improve-test
Natelytle Aug 14, 2023
1e80f79
Fix non-existent attribute call
Natelytle Aug 14, 2023
52799d5
Merge remote-tracking branch 'osumaster/master' into maniastatacc
Natelytle Sep 4, 2023
75c295c
Remove hit windows scaling by rate, adjust tests
Natelytle Sep 4, 2023
5de7e42
Merge branch 'master' into maniastatacc
smoogipoo Sep 24, 2023
aa8b9cd
Merge master
Natelytle Dec 13, 2023
ad818a4
Increase readability
Natelytle Jan 15, 2024
4f2496f
Merge branch 'ppy:master' into maniastatacc
Natelytle May 16, 2024
0f47f46
Merge branch 'master' into maniastatacc
smoogipoo May 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions osu.Game.Rulesets.Mania.Tests/ManiaUnstableRateEstimationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using NUnit.Framework;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Scoring;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;

namespace osu.Game.Rulesets.Mania.Tests
{
/// <summary>
/// This test suite tests ManiaPerformanceCalculator.computeEstimatedUr
/// <remarks>
/// This suite focuses on the objective aspects of the calculation, not the accuracy of the calculation.
/// </remarks>
/// </summary>
public class ManiaUnstableRateEstimationTest
{
public enum SpeedMod
{
DoubleTime,
NormalTime,
HalfTime
}

public static IEnumerable<TestCaseData> TestCaseSourceData()
{
yield return new TestCaseData(1037.4609375d, new[] { 3, 3, 3, 3, 3, 3 }, SpeedMod.DoubleTime);
yield return new TestCaseData(1037.4609375d, new[] { 3, 3, 3, 3, 3, 3 }, SpeedMod.NormalTime);
yield return new TestCaseData(1037.4609375d, new[] { 3, 3, 3, 3, 3, 3 }, SpeedMod.HalfTime);
}

/// <summary>
/// A catch-all hardcoded regression test, inclusive of rate changing.
/// </summary>
[TestCaseSource(nameof(TestCaseSourceData))]
public void RegressionTest(double expectedUr, int[] judgementCounts, SpeedMod speedMod)
{
double? estimatedUr = computeUnstableRate(judgementCounts, speedMod: speedMod);
// Platform-dependent math functions (Pow, Cbrt, Exp, etc) and advanced math functions (Erf, FindMinimum) may result in slight differences.
Assert.That(
estimatedUr, Is.EqualTo(expectedUr).Within(0.001),
$"The estimated mania UR {estimatedUr} differed from the expected value {expectedUr}."
);
}

/// <summary>
/// Test anomalous judgement counts where NULLs can occur.
/// </summary>
[TestCase(false, new[] { 1, 0, 0, 0, 0, 0 })]
[TestCase(true, new[] { 0, 0, 0, 0, 0, 1 })]
[TestCase(true, new[] { 0, 0, 0, 0, 0, 0 })]
public void TestNull(bool expectedIsNull, int[] judgementCounts)
{
double? estimatedUr = computeUnstableRate(judgementCounts);
bool isNull = estimatedUr == null;

Assert.That(isNull, Is.EqualTo(expectedIsNull), $"Estimated mania UR {estimatedUr} was/wasn't null.");
}

/// <summary>
/// Ensure that the worst case scenarios don't result in unbounded URs.
/// <remarks>Given Int.MaxValue judgements, it can result in
/// <see cref="MathNet.Numerics.Optimization.MaximumIterationsException"/>.
/// However, we'll only test realistic scenarios.</remarks>
/// </summary>
[Test, Combinatorial]
public void TestEdge(
[Values(100_000, 1, 0)] int judgeMax, // We're only interested in the edge judgements.
[Values(100_000, 1, 0)] int judge50,
[Values(100_000, 1, 0)] int judge0,
[Values(SpeedMod.DoubleTime, SpeedMod.HalfTime, SpeedMod.NormalTime)]
SpeedMod speedMod,
[Values(true, false)] bool isHoldsLegacy,
[Values(true, false)] bool isAllHolds, // This will determine if we use all holds or all notes.
[Values(10, 5, 0)] double od
)
{
// This is tested in TestNull.
if (judgeMax + judge50 == 0) Assert.Ignore();

int noteCount = isAllHolds ? 0 : judgeMax + judge50 + judge0;
int holdCount = isAllHolds ? judgeMax + judge50 + judge0 : 0;

double? estimatedUr = computeUnstableRate(
new[] { judgeMax, 0, 0, 0, judge50, judge0 },
noteCount,
holdCount,
od,
speedMod,
isHoldsLegacy
);
Assert.That(
estimatedUr, Is.AtMost(1_000_000_000),
$"The estimated mania UR {estimatedUr} returned too high for a single note."
);
}

/// <summary>
/// This tests if the UR gets smaller, given more MAX judgements.
/// This follows the logic that:
/// - More MAX judgements implies stronger evidence of smaller UR, as the probability of hitting a MAX judgement is higher.
/// <remarks>
/// It's not necessary, nor logical to test other behaviors.
/// </remarks>
/// </summary>
[Test]
public void TestMoreMaxJudgementsSmallerUr(
[Values(1, 10, 1000)] int count,
[Values(1, 10, 1000)] int step
)
{
int[] judgementCountsLess = { count, 0, 0, 0, 0, 0 };
int[] judgementCountsMore = { count + step, 0, 0, 0, 0, 0 };
double? estimatedUrLessJudgements = computeUnstableRate(judgementCountsLess);
double? estimatedUrMoreJudgements = computeUnstableRate(judgementCountsMore);

// Assert that More Judgements results in a smaller UR.
Assert.That(
estimatedUrMoreJudgements, Is.LessThan(estimatedUrLessJudgements),
$"UR {estimatedUrMoreJudgements} with More Judgements {string.Join(",", judgementCountsMore)} >= "
+ $"UR {estimatedUrLessJudgements} than Less Judgements {string.Join(",", judgementCountsLess)} "
);
}

/// <summary>
/// Evaluates the Unstable Rate
/// </summary>
/// <param name="judgementCounts">Size-6 Int List of Judgements, starting from MAX</param>
/// <param name="noteCount">Number of notes</param>
/// <param name="holdCount">Number of holds</param>
/// <param name="od">Overall Difficulty</param>
/// <param name="speedMod">Speed Mod, <see cref="SpeedMod"/></param>
/// <param name="isHoldsLegacy">Whether to append ClassicMod to simulate Legacy Holds</param>
private double? computeUnstableRate(
IReadOnlyList<int> judgementCounts,
int? noteCount = null,
int holdCount = 0,
double od = 5,
SpeedMod speedMod = SpeedMod.NormalTime,
bool isHoldsLegacy = false)
{
var judgements = new Dictionary<HitResult, int>
{
{ HitResult.Perfect, judgementCounts[0] },
{ HitResult.Great, judgementCounts[1] },
{ HitResult.Good, judgementCounts[2] },
{ HitResult.Ok, judgementCounts[3] },
{ HitResult.Meh, judgementCounts[4] },
{ HitResult.Miss, judgementCounts[5] }
};
noteCount ??= judgements.Sum(kvp => kvp.Value);

var mods = new Mod[] { };

if (isHoldsLegacy) mods = mods.Append(new ManiaModClassic()).ToArray();

switch (speedMod)
{
case SpeedMod.DoubleTime:
mods = mods.Append(new ManiaModDoubleTime()).ToArray();
break;

case SpeedMod.HalfTime:
mods = mods.Append(new ManiaModHalfTime()).ToArray();
break;
}

ManiaPerformanceAttributes perfAttributes = new ManiaPerformanceCalculator().Calculate(
new ScoreInfo
{
Mods = mods,
Statistics = judgements
},
new ManiaDifficultyAttributes
{
NoteCount = (int)noteCount,
HoldNoteCount = holdCount,
OverallDifficulty = od,
Mods = mods
}
);

return perfAttributes.EstimatedUr;
}

/// <summary>
/// This ensures that external changes of hit windows don't break the ur calculator.
/// This includes all ODs.
/// </summary>
[Test]
public void RegressionTestHitWindows(
[Range(0, 10, 0.5)] double overallDifficulty
)
{
DifficultyAttributes attributes = new ManiaDifficultyAttributes { OverallDifficulty = overallDifficulty };

var hitWindows = new ManiaHitWindows();
hitWindows.SetDifficulty(overallDifficulty);

double[] trueHitWindows =
{
hitWindows.WindowFor(HitResult.Perfect),
hitWindows.WindowFor(HitResult.Great),
hitWindows.WindowFor(HitResult.Good),
hitWindows.WindowFor(HitResult.Ok),
hitWindows.WindowFor(HitResult.Meh)
};

ManiaPerformanceAttributes perfAttributes = new ManiaPerformanceCalculator().Calculate(new ScoreInfo(), attributes);

// Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.
Assert.That(perfAttributes.HitWindows, Is.EqualTo(trueHitWindows).Within(0.000001), "The true mania hit windows are different to the ones calculated in ManiaPerformanceCalculator.");
}
}
}
22 changes: 22 additions & 0 deletions osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,32 @@ public class ManiaDifficultyAttributes : DifficultyAttributes
[JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; }

/// <summary>
/// The perceived overall difficulty of the map.
/// </summary>
[JsonProperty("overall_difficulty")]
public double OverallDifficulty { get; set; }

/// <summary>
/// The number of notes in the beatmap.
/// </summary>
public int NoteCount { get; set; }

/// <summary>
/// The number of hold notes in the beatmap.
/// </summary>
public int HoldNoteCount { get; set; }

public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{
foreach (var v in base.ToDatabaseAttributes())
yield return v;

yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
yield return (ATTRIB_ID_NOTE_COUNT, NoteCount);
yield return (ATTRIB_ID_HOLD_NOTE_COUNT, HoldNoteCount);
}

public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
Expand All @@ -34,6 +53,9 @@ public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> val

StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
NoteCount = (int)values[ATTRIB_ID_NOTE_COUNT];
HoldNoteCount = (int)values[ATTRIB_ID_HOLD_NOTE_COUNT];
Comment on lines +56 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to ask why these are not just being read from the IBeatmapOnlineInfo param here and skipped entirely in .ToDatabaseAttributes(), but then realised that wouldn't work because of keymods. I'm not sure how intentional this was but yeah.

This is going to be something that we need to be wary of going forward unless realtime calculation becomes a thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part in particular was added by smoogi to calculate a sheet for the rework

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
HitWindows hitWindows = new ManiaHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);

int noteCount = beatmap.HitObjects.Count(h => h is Note);
int holdNoteCount = beatmap.HitObjects.Count(h => h is HoldNote);

ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
{
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Expand All @@ -54,6 +57,9 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
OverallDifficulty = beatmap.Difficulty.OverallDifficulty,
NoteCount = noteCount,
HoldNoteCount = holdNoteCount,
};

return attributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public class ManiaPerformanceAttributes : PerformanceAttributes
[JsonProperty("difficulty")]
public double Difficulty { get; set; }

[JsonProperty("estimated_ur")]
public double? EstimatedUr { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #20963, would prefer this to be called EstimatedUnstableRate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.


[JsonProperty("hit_windows")]
public double[] HitWindows { get; set; } = null!;

public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
Expand Down
Loading
Loading