From bdce750238b2e4383821678cc545e92db946ca41 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 13 Nov 2023 02:35:26 -0800 Subject: [PATCH] Add violin plots for reliability And move custom imraii stuff to imraii2 --- Craftimizer/Craftimizer.csproj | 1 + Craftimizer/ImGuiExtras.cs | 2 +- Craftimizer/ImGuiUtils.cs | 102 +++++++++++++++++-------- Craftimizer/ImRaii2.cs | 92 +++++++++++++++++++++++ Craftimizer/Windows/MacroClipboard.cs | 2 +- Craftimizer/Windows/MacroEditor.cs | 104 +++++++++++++------------- Craftimizer/Windows/MacroList.cs | 2 +- Craftimizer/Windows/RecipeNote.cs | 4 +- Craftimizer/Windows/Settings.cs | 22 ++++-- Craftimizer/packages.lock.json | 6 ++ 10 files changed, 243 insertions(+), 94 deletions(-) create mode 100644 Craftimizer/ImRaii2.cs diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index aa4cd9f..94acb1e 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -39,6 +39,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Craftimizer/ImGuiExtras.cs b/Craftimizer/ImGuiExtras.cs index bc60e1a..14adb76 100644 --- a/Craftimizer/ImGuiExtras.cs +++ b/Craftimizer/ImGuiExtras.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace Craftimizer; +namespace Craftimizer.Plugin; internal static unsafe class ImGuiExtras { diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 68b83f1..7f223c9 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -2,12 +2,16 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using ImPlotNET; +using MathNet.Numerics.Statistics; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -130,37 +134,6 @@ public static void EndGroupPanel() ImGui.PopID(); } - private struct EndUnconditionally : ImRaii.IEndObject, IDisposable - { - private Action EndAction { get; } - - public bool Success { get; } - - public bool Disposed { get; private set; } - - public EndUnconditionally(Action endAction, bool success) - { - EndAction = endAction; - Success = success; - Disposed = false; - } - - public void Dispose() - { - if (!Disposed) - { - EndAction(); - Disposed = true; - } - } - } - - public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth) - { - internalWidth = BeginGroupPanel(name, width); - return new EndUnconditionally(EndGroupPanel, true); - } - private static Vector2 UnitCircle(float theta) { var (s, c) = MathF.SinCos(theta); @@ -168,6 +141,7 @@ private static Vector2 UnitCircle(float theta) return new Vector2(c, -s); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float Lerp(float a, float b, float t) => MathF.FusedMultiplyAdd(b - a, t, a); @@ -251,6 +225,72 @@ public static void ArcProgress(float value, float radiusInner, float radiusOuter Arc(MathF.PI / 2, MathF.PI / 2 - MathF.Tau * Math.Clamp(value, 0, 1), radiusInner, radiusOuter, backgroundColor, filledColor); } + public sealed class ViolinData + { + [StructLayout(LayoutKind.Sequential)] + public struct Point + { + public float X, Y, Y2; + + public Point(float x, float y, float y2) + { + X = x; + Y = y; + Y2 = y2; + } + } + + public ReadOnlySpan Data => (DataArray ?? Array.Empty()).AsSpan(); + private Point[]? DataArray { get; set; } + public readonly float Min; + public readonly float Max; + + public ViolinData(IEnumerable samples, float min, float max, int resolution, double bandwidth) + { + Min = min; + Max = max; + bandwidth *= Max - Min; + var samplesList = samples.AsParallel().Select(s => (double)s).ToArray(); + _ = Task.Run(() => { + var s = Stopwatch.StartNew(); + var data = ParallelEnumerable.Range(0, resolution) + .Select(n => Lerp(min, max, n / (float)resolution)) + .Select(n => (n, (float)KernelDensity.EstimateGaussian(n, bandwidth, samplesList))) + .Select(n => new Point(n.n, n.Item2, -n.Item2)); + DataArray = data.ToArray(); + s.Stop(); + Log.Debug($"Violin plot processing took {s.Elapsed.TotalMilliseconds:0.00}ms"); + }); + } + } + + public static void ViolinPlot(in ViolinData data, Vector2 size) + { + using var padding = ImRaii2.PushStyle(ImPlotStyleVar.PlotPadding, Vector2.Zero); + using var plotBg = ImRaii2.PushColor(ImPlotCol.PlotBg, Vector4.Zero); + using var frameBg = ImRaii2.PushColor(ImPlotCol.FrameBg, Vector4.Zero); + using var fill = ImRaii2.PushColor(ImPlotCol.Fill, Vector4.One.WithAlpha(.5f)); + + using var plot = ImRaii2.Plot("##violin", size, ImPlotFlags.CanvasOnly); + if (plot) + { + ImPlot.SetupAxes(null, null, ImPlotAxisFlags.NoDecorations, ImPlotAxisFlags.NoDecorations | ImPlotAxisFlags.AutoFit); + ImPlot.SetupAxisLimits(ImAxis.X1, data.Min, data.Max, ImPlotCond.Always); + ImPlot.SetupFinish(); + + if (data.Data is { } points && !points.IsEmpty) + { + unsafe { + var label_id = stackalloc byte[] { (byte)'\0' }; + fixed (ViolinData.Point* p = points) + { + ImPlotNative.ImPlot_PlotShaded_FloatPtrFloatPtrFloatPtr(label_id, &p->X, &p->Y, &p->Y2, points.Length, ImPlotShadedFlags.None, 0, sizeof(ViolinData.Point)); + } + } + } + } + } + private sealed class SearchableComboData where T : class { public readonly ImmutableArray items; diff --git a/Craftimizer/ImRaii2.cs b/Craftimizer/ImRaii2.cs new file mode 100644 index 0000000..f2499e8 --- /dev/null +++ b/Craftimizer/ImRaii2.cs @@ -0,0 +1,92 @@ +using Dalamud.Interface.Utility.Raii; +using ImPlotNET; +using System; +using System.Numerics; + +namespace Craftimizer.Plugin; + +public static class ImRaii2 +{ + private struct EndUnconditionally : ImRaii.IEndObject, IDisposable + { + private Action EndAction { get; } + + public bool Success { get; } + + public bool Disposed { get; private set; } + + public EndUnconditionally(Action endAction, bool success) + { + EndAction = endAction; + Success = success; + Disposed = false; + } + + public void Dispose() + { + if (!Disposed) + { + EndAction(); + Disposed = true; + } + } + } + + private struct EndConditionally : ImRaii.IEndObject, IDisposable + { + public bool Success { get; } + + public bool Disposed { get; private set; } + + private Action EndAction { get; } + + public EndConditionally(Action endAction, bool success) + { + EndAction = endAction; + Success = success; + Disposed = false; + } + + public void Dispose() + { + if (!Disposed) + { + if (Success) + { + EndAction(); + } + + Disposed = true; + } + } + } + + public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth) + { + internalWidth = ImGuiUtils.BeginGroupPanel(name, width); + return new EndUnconditionally(ImGuiUtils.EndGroupPanel, true); + } + + public static ImRaii.IEndObject Plot(string title_id, Vector2 size, ImPlotFlags flags) + { + return new EndConditionally(new Action(ImPlot.EndPlot), ImPlot.BeginPlot(title_id, size, flags)); + } + + public static ImRaii.IEndObject PushStyle(ImPlotStyleVar idx, Vector2 val) + { + ImPlot.PushStyleVar(idx, val); + return new EndUnconditionally(ImPlot.PopStyleVar, true); + } + + public static ImRaii.IEndObject PushStyle(ImPlotStyleVar idx, float val) + { + ImPlot.PushStyleVar(idx, val); + return new EndUnconditionally(ImPlot.PopStyleVar, true); + } + + public static ImRaii.IEndObject PushColor(ImPlotCol idx, Vector4 col) + { + ImPlot.PushStyleColor(idx, col); + return new EndUnconditionally(ImPlot.PopStyleColor, true); + } +} diff --git a/Craftimizer/Windows/MacroClipboard.cs b/Craftimizer/Windows/MacroClipboard.cs index a320b93..ee13d72 100644 --- a/Craftimizer/Windows/MacroClipboard.cs +++ b/Craftimizer/Windows/MacroClipboard.cs @@ -36,7 +36,7 @@ public override void Draw() private void DrawMacro(int idx, string macro) { using var id = ImRaii.PushId(idx); - using var panel = ImGuiUtils.GroupPanel($"Macro {idx + 1}", -1, out var availWidth); + using var panel = ImRaii2.GroupPanel($"Macro {idx + 1}", -1, out var availWidth); var cursor = ImGui.GetCursorPos(); diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs index 1e2380e..1983c8a 100644 --- a/Craftimizer/Windows/MacroEditor.cs +++ b/Craftimizer/Windows/MacroEditor.cs @@ -79,31 +79,48 @@ public CrafterBuffs(StatusList? statuses) private readonly record struct SimulationReliablity { - public record struct ParamReliability + public sealed class ParamReliability { + private List DataList { get; } + private ImGuiUtils.ViolinData? ViolinData { get; set; } + public int Max { get; private set; } public int Min { get; private set; } + public float Median { get; private set; } public float Average { get; private set; } public ParamReliability() { - Min = int.MaxValue; + DataList = new(); } public void Add(int value) { - Max = Math.Max(Max, value); - Min = Math.Min(Min, value); - Average += value; + DataList.Add(value); } - public void Finalize(int count) + public void FinalizeData() { - if (count == 0) - Average = Max = Min = 0; + if (DataList.Count == 0) + { + Average = Median = Max = Min = 0; + return; + } + + Max = DataList.Max(); + Min = DataList.Min(); + if (DataList.Count % 2 == 0) + Median = (float)DataList.Order().Skip(DataList.Count / 2 - 1).Take(2).Average(); else - Average /= count; + Median = DataList.Order().ElementAt(DataList.Count / 2); + Average = (float)DataList.Average(); } + + public ImGuiUtils.ViolinData? GetViolinData(float barMax, int resolution, double bandwidth) => + ViolinData ??= + Min != Max ? + new(DataList, 0, barMax, resolution, bandwidth) : + null; } public readonly ParamReliability Progress = new(); @@ -132,9 +149,9 @@ public SimulationReliablity(in SimulationState startState, IEnumerable actionSet, RecipeData recipeData) => - Reliability ??= new(initialState, actionSet, Service.Configuration.ReliabilitySimulationCount, recipeData); + Reliability ??= + new(initialState, actionSet, Service.Configuration.ReliabilitySimulationCount, recipeData); }; private List Macro { get; set; } = new(); @@ -195,7 +213,7 @@ public SimulationReliablity GetReliability(in SimulationState initialState, IEnu private CancellationTokenSource? popupImportUrlTokenSource; private MacroImport.RetrievedMacro? popupImportUrlMacro; - public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable actions, Action>? setter) : base("Craftimizer Macro Editor", WindowFlags, false) + public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable actions, Action>? setter) : base("Craftimizer Macro Editor", WindowFlags) { CharacterStats = characterStats; RecipeData = recipeData; @@ -1000,7 +1018,7 @@ private void DrawActionHotbars() continue; var actions = category.GetActions(); - using var panel = ImGuiUtils.GroupPanel(category.GetDisplayName(), -1, out var availSpace); + using var panel = ImRaii2.GroupPanel(category.GetDisplayName(), -1, out var availSpace); var itemsPerRow = (int)MathF.Floor((availSpace + spacing) / (imageSize + spacing)); var itemCount = actions.Count; var iterCount = (int)(Math.Ceiling((float)itemCount / itemsPerRow) * itemsPerRow); @@ -1076,7 +1094,7 @@ private void DrawMacroInfo() } } - using (var panel = ImGuiUtils.GroupPanel("Buffs", -1, out _)) + using (var panel = ImRaii2.GroupPanel("Buffs", -1, out _)) { using var _font = ImRaii.PushFont(AxisFont.ImFont); @@ -1134,7 +1152,7 @@ private void DrawBars(IEnumerable bars) var barSize = totalSize - textSize - spacing; foreach (var bar in bars) { - using var panel = ImGuiUtils.GroupPanel(bar.Name, totalSize, out _); + using var panel = ImRaii2.GroupPanel(bar.Name, totalSize, out _); if (bar.Condition is { } condition) { using (var g = ImRaii.Group()) @@ -1155,46 +1173,26 @@ private void DrawBars(IEnumerable bars) } else { - if (bar.Reliability is { } reliability) + var pos = ImGui.GetCursorPos(); + using (var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, bar.Color)) + ImGui.ProgressBar(Math.Clamp(bar.Value / bar.Max, 0, 1), new(barSize, ImGui.GetFrameHeight()), string.Empty); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped)) { - // blend rgb colors, assume alpha is always 1 - static Vector4 BlendColor(Vector4 A, Vector4 B) - { - return new( - A.X * (1 - B.W) + B.X * B.W, - A.Y * (1 - B.W) + B.Y * B.W, - A.Z * (1 - B.W) + B.Z * B.W, - 1); - } - var relBars = new (float Value, Vector4 Color)[] + if (bar.Reliability is { } reliability) { - (bar.Value, bar.Color), - (reliability.Average, BlendColor(bar.Color, new(.5f,.5f,.5f,.6f))), - (reliability.Min, BlendColor(bar.Color, new(0,0,0,.6f))), - (reliability.Max, BlendColor(bar.Color, new(1,1,1,.6f))), - }.DistinctBy(v => Math.Clamp(v.Value, 0, bar.Max)).OrderByDescending(v => v.Value); - var i = 0; - var pos = ImGui.GetCursorPos(); - foreach (var relBar in relBars) - { - ImGui.SetCursorPos(pos); - { - using var frameColor = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero, i != 0); - using var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, relBar.Color); - ImGui.ProgressBar(Math.Clamp(relBar.Value / bar.Max, 0, 1), new(barSize, ImGui.GetFrameHeight()), string.Empty); - } - if (i++ == 0) + if (reliability.GetViolinData(bar.Max, (int)(barSize / 2), 0.02) is { } violinData) { + ImGui.SetCursorPos(pos); + ImGuiUtils.ViolinPlot(violinData, new(barSize, ImGui.GetFrameHeight())); if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Min: {reliability.Min}\nAvg: {reliability.Average:0.##}\nMax: {reliability.Max}"); + ImGui.SetTooltip( + $"Min: {reliability.Min}\n" + + $"Med: {reliability.Median:0.##}\n" + + $"Avg: {reliability.Average:0.##}\n" + + $"Max: {reliability.Max}"); } } } - else - { - using var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, bar.Color); - ImGui.ProgressBar(Math.Clamp(bar.Value / bar.Max, 0, 1), new(barSize, ImGui.GetFrameHeight()), string.Empty); - } ImGui.SameLine(0, spacing); ImGui.AlignTextToFramePadding(); if (bar.Caption is { } caption) @@ -1217,7 +1215,7 @@ private void DrawMacro() var imageSize = ImGui.GetFrameHeight() * 2; var lastState = InitialState; - using var panel = ImGuiUtils.GroupPanel("Macro", -1, out var availSpace); + using var panel = ImRaii2.GroupPanel("Macro", -1, out var availSpace); ImGui.Dummy(new(0, imageSize)); ImGui.SameLine(0, 0); @@ -1412,7 +1410,7 @@ private void DrawImportPopup() { bool submittedText, submittedUrl; - using (var panel = ImGuiUtils.GroupPanel("##text", -1, out var availWidth)) + using (var panel = ImRaii2.GroupPanel("##text", -1, out var availWidth)) { ImGui.AlignTextToFramePadding(); ImGuiUtils.TextCentered("Paste your macro here"); @@ -1424,7 +1422,7 @@ private void DrawImportPopup() submittedText = ImGui.Button("Import", new(availWidth, 0)); } - using (var panel = ImGuiUtils.GroupPanel("##url", -1, out var availWidth)) + using (var panel = ImRaii2.GroupPanel("##url", -1, out var availWidth)) { var availOffset = ImGui.GetContentRegionAvail().X - availWidth; diff --git a/Craftimizer/Windows/MacroList.cs b/Craftimizer/Windows/MacroList.cs index 3601af6..6bc5b56 100644 --- a/Craftimizer/Windows/MacroList.cs +++ b/Craftimizer/Windows/MacroList.cs @@ -111,7 +111,7 @@ private void DrawMacro(Macro macro) var stateNullable = GetMacroState(macro); - using var panel = ImGuiUtils.GroupPanel(macro.Name, -1, out var availWidth); + using var panel = ImRaii2.GroupPanel(macro.Name, -1, out var availWidth); var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth; var spacing = ImGui.GetStyle().ItemSpacing.Y; var miniRowHeight = (windowHeight - spacing) / 2f; diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 5abd350..a660e85 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -216,7 +216,7 @@ public override void Draw() ImGui.Separator(); var panelWidth = availWidth - ImGui.GetStyle().ItemSpacing.X * 2; - using (var panel = ImGuiUtils.GroupPanel("Best Saved Macro", panelWidth, out _)) + using (var panel = ImRaii2.GroupPanel("Best Saved Macro", panelWidth, out _)) { var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth; if (BestSavedMacro is { } savedMacro) @@ -228,7 +228,7 @@ public override void Draw() DrawMacro(null, null, stepsPanelWidthOffset, true); } - using (var panel = ImGuiUtils.GroupPanel("Suggested Macro", panelWidth, out _)) + using (var panel = ImRaii2.GroupPanel("Suggested Macro", panelWidth, out _)) { var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth; if (BestSuggestedMacro is { } suggestedMacro) diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index a11ca3c..3f9a726 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -191,7 +191,7 @@ ref isDirty ImGui.SetTooltip("Disabled temporarily for testing"); DrawOption( - "Show Only One Macro Stat", + "Show Only One Macro Stat in Crafting Log", "Only one stat will be shown for a macro. If a craft will be finished, quality\n" + "is shown. Otherwise, progress is shown. Durability and remaining CP will be\n" + "hidden.", @@ -200,9 +200,21 @@ ref isDirty ref isDirty ); + DrawOption( + "Reliability Trial Count", + "When testing for reliability of a macro in the editor, this many trials will be\n" + + "run. You should set this value to at least 100 to get a reliable spread of data.\n" + + "If it's too low, you may not find an outlier, and the average might be skewed.", + Config.ReliabilitySimulationCount, + 5, + 5000, + v => Config.ReliabilitySimulationCount = v, + ref isDirty + ); + ImGuiHelpers.ScaledDummy(5); - using (var panel = ImGuiUtils.GroupPanel("Copying Settings", -1, out _)) + using (var panel = ImRaii2.GroupPanel("Copying Settings", -1, out _)) { DrawOption( "Macro Copy Method", @@ -380,7 +392,7 @@ private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig de var config = configRef; - using (var panel = ImGuiUtils.GroupPanel("General", -1, out _)) + using (var panel = ImRaii2.GroupPanel("General", -1, out _)) { if (ImGui.Button("Reset to Default", OptionButtonSize)) { @@ -501,7 +513,7 @@ ref isDirty ); } - using (var panel = ImGuiUtils.GroupPanel("Advanced", -1, out _)) + using (var panel = ImRaii2.GroupPanel("Advanced", -1, out _)) { DrawOption( "Score Storage Threshold", @@ -538,7 +550,7 @@ ref isDirty ); } - using (var panel = ImGuiUtils.GroupPanel("Score Weights (Advanced)", -1, out _)) + using (var panel = ImRaii2.GroupPanel("Score Weights (Advanced)", -1, out _)) { ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed."); ImGuiHelpers.ScaledDummy(10); diff --git a/Craftimizer/packages.lock.json b/Craftimizer/packages.lock.json index 2308529..fb17722 100644 --- a/Craftimizer/packages.lock.json +++ b/Craftimizer/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "2.1.12", "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" }, + "MathNet.Numerics": { + "type": "Direct", + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "pg1W2VwaEQMAiTpGK840hZgzavnqjlCMTVSbtVCXVyT+7AX4mc1o89SPv4TBlAjhgCOo9c1Y+jZ5m3ti2YgGgA==" + }, "Meziantou.Analyzer": { "type": "Direct", "requested": "[2.0.106, )",