From 8f5f199682a079374abc80fdea807cbf96e03fc2 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 14 Nov 2023 02:21:04 -0800 Subject: [PATCH] Add solver progress bar --- Craftimizer/Windows/MacroEditor.cs | 19 ++++++++- Solver/MCTS.cs | 16 +++++--- Solver/Solver.cs | 64 +++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs index 89fe2b7..68220bf 100644 --- a/Craftimizer/Windows/MacroEditor.cs +++ b/Craftimizer/Windows/MacroEditor.cs @@ -196,6 +196,8 @@ public sealed class MacroEditor : Window, IDisposable private int? SolverStartStepCount { get; set; } private object? SolverQueueLock { get; set; } private List? SolverQueuedSteps { get; set; } + private int solverProgress; + private int maxSolverProgress; private bool SolverRunning => SolverTokenSource != null; private IDalamudTextureWrap ExpertBadge { get; } @@ -1269,7 +1271,7 @@ public sealed class MacroEditor : Window, IDisposable ImGui.Dummy(new(0, imageSize)); ImGui.SameLine(0, 0); - var macroActionsHeight = ImGui.GetFrameHeightWithSpacing(); + var macroActionsHeight = ImGui.GetFrameHeightWithSpacing() * (1 + (SolverRunning ? 1 : 0)); var childHeight = ImGui.GetContentRegionAvail().Y - ImGui.GetStyle().ItemSpacing.Y * 2 - ImGui.GetStyle().CellPadding.Y - macroActionsHeight - ImGui.GetStyle().ItemSpacing.Y * 2; using (var child = ImRaii.Child("##macroActions", new(availSpace, childHeight))) @@ -1308,6 +1310,19 @@ public sealed class MacroEditor : Window, IDisposable ImGui.Dummy(default); ImGui.GetWindowDrawList().AddLine(pos, pos + new Vector2(availSpace, 0), ImGui.GetColorU32(ImGuiCol.Border)); ImGui.Dummy(default); + if (SolverRunning) + { + var percentWidth = ImGui.CalcTextSize("100%").X; + var progressWidth = availSpace - percentWidth - spacing; + var fraction = Math.Clamp((float)solverProgress / maxSolverProgress, 0, 1); + using (var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, ImGuiColors.DalamudGrey3)) + ImGui.ProgressBar(fraction, new(progressWidth, ImGui.GetFrameHeight()), string.Empty); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Solver Progress: {solverProgress} / {maxSolverProgress}"); + ImGui.SameLine(0, spacing); + ImGui.AlignTextToFramePadding(); + ImGuiUtils.TextRight($"{fraction * 100:0}%", percentWidth); + } DrawMacroActions(availSpace); } @@ -1602,6 +1617,7 @@ public sealed class MacroEditor : Window, IDisposable } SolverQueueLock = new(); SolverQueuedSteps ??= new(); + solverProgress = 0; RevertPreviousMacro(); @@ -1648,6 +1664,7 @@ public sealed class MacroEditor : Window, IDisposable var solver = new Solver.Solver(config, state) { Token = token }; solver.OnLog += Log.Debug; solver.OnNewAction += QueueSolverStep; + solver.OnProgress += (s, p, m) => { Interlocked.Exchange(ref solverProgress, p); Interlocked.Exchange(ref maxSolverProgress, m); }; solver.Start(); _ = solver.GetTask().GetAwaiter().GetResult(); diff --git a/Solver/MCTS.cs b/Solver/MCTS.cs index 99e6147..35df291 100644 --- a/Solver/MCTS.cs +++ b/Solver/MCTS.cs @@ -15,6 +15,9 @@ public sealed class MCTS private readonly Node rootNode; private readonly RootScores rootScores; + public const int ProgressUpdateFrequency = 1 << 10; + private const int StaleProgressThreshold = 1 << 12; + public float MaxScore => rootScores.MaxScore; public MCTS(in MCTSConfig config, in SimulationState state) @@ -278,11 +281,11 @@ public sealed class MCTS } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Search(int iterations, CancellationToken token) + public void Search(int iterations, CancellationToken token, Action? progressCallback) { Simulator simulator = new(config.MaxStepCount); var random = rootNode.State.State.Input.Random; - var n = 0; + var staleCounter = 0; for (var i = 0; i < iterations || MaxScore == 0; i++) { token.ThrowIfCancellationRequested(); @@ -293,9 +296,9 @@ public sealed class MCTS { if (endNode == selectedNode) { - if (n++ > 5000) + if (staleCounter++ >= StaleProgressThreshold) { - n = 0; + staleCounter = 0; if (AllNodesComplete()) { //Console.WriteLine("All nodes solved for. Can't find a valid solution."); @@ -305,10 +308,13 @@ public sealed class MCTS } } else - n = 0; + staleCounter = 0; } Backpropagate(endNode, score); + + if ((i & (ProgressUpdateFrequency - 1)) == ProgressUpdateFrequency - 1) + progressCallback?.Invoke(); } } diff --git a/Solver/Solver.cs b/Solver/Solver.cs index add8bd1..b144f6c 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -19,8 +19,13 @@ public sealed class Solver : IDisposable private MCTSConfig MCTSConfig => new(Config); private Task? CompletionTask { get; set; } + private int progressSequence; + private int progress; + private int maxProgress; + public delegate void LogDelegate(string text); public delegate void WorkerProgressDelegate(SolverSolution solution, float score); + public delegate void ProgressDelegate(int sequence, int value, int maxValue); public delegate void NewActionDelegate(ActionType action); public delegate void SolutionDelegate(SolverSolution solution); @@ -32,6 +37,11 @@ public sealed class Solver : IDisposable // For example, to get to the terminal state, execute all OnNewAction actions, then execute all Solution actions. public event WorkerProgressDelegate? OnWorkerProgress; + // Always called in some form in every algorithm. + // In iterative algorithms, the sequence can increment and reset the value back to 0. + // In other algorithms, the sequence is always 0 and the value increases monotonically. + public event ProgressDelegate? OnProgress; + // Always called when a new step is generated. public event NewActionDelegate? OnNewAction; @@ -63,6 +73,7 @@ public sealed class Solver : IDisposable { Token.ThrowIfCancellationRequested(); + progressSequence = progress = 0; Solution = await SearchFunc().ConfigureAwait(false); } @@ -110,8 +121,35 @@ public sealed class Solver : IDisposable OnNewAction?.Invoke(sanitizedAction); } + private void IncrementProgress() => + IncrementProgressBy(MCTS.ProgressUpdateFrequency); + + private void IncrementRemainingProgress(int iterations) => + IncrementProgressBy(iterations & MCTS.ProgressUpdateFrequency); + + private void IncrementProgressBy(int value) + { + OnProgress?.Invoke(progressSequence, Interlocked.Add(ref progress, value), maxProgress); + } + + private void IncrementProgressSequence() + { + Interlocked.Exchange(ref progress, 0); + progressSequence++; + OnProgress?.Invoke(progressSequence, 0, maxProgress); + } + + private void SearchWithIncrement(MCTS mcts, int iterations) + { + mcts.Search(iterations, Token, IncrementProgress); + IncrementRemainingProgress(iterations); + } + private async Task SearchStepwiseFurcated() { + var iterCount = Config.Iterations / Config.ForkCount; + maxProgress = iterCount * Config.ForkCount; + var definiteActionCount = 0; var bestSims = new List<(float Score, SolverSolution Result)>(); @@ -136,7 +174,7 @@ public sealed class Solver : IDisposable await semaphore.WaitAsync(Token).ConfigureAwait(false); try { - solver.Search(Config.Iterations / Config.ForkCount, Token); + SearchWithIncrement(solver, iterCount); } finally { @@ -223,6 +261,8 @@ public sealed class Solver : IDisposable } } + IncrementProgressSequence(); + activeStates = newStates; } @@ -238,6 +278,9 @@ public sealed class Solver : IDisposable private async Task SearchStepwiseForked() { + var iterCount = Config.Iterations / Config.ForkCount; + maxProgress = iterCount * Config.ForkCount; + var actions = new List(); var state = State; var sim = new Simulator(Config.MaxStepCount) { State = state }; @@ -258,7 +301,7 @@ public sealed class Solver : IDisposable await semaphore.WaitAsync(Token).ConfigureAwait(false); try { - solver.Search(Config.Iterations / Config.ForkCount, Token); + SearchWithIncrement(solver, iterCount); } finally { @@ -294,6 +337,8 @@ public sealed class Solver : IDisposable (_, state) = sim.Execute(state, chosenAction); actions.Add(chosenAction); + + IncrementProgressSequence(); } return new(actions, state); @@ -301,6 +346,8 @@ public sealed class Solver : IDisposable private Task SearchStepwise() { + maxProgress = Config.Iterations; + var actions = new List(); var state = State; var sim = new Simulator(Config.MaxStepCount) { State = state }; @@ -314,7 +361,7 @@ public sealed class Solver : IDisposable var solver = new MCTS(MCTSConfig, state); var s = Stopwatch.StartNew(); - solver.Search(Config.Iterations, Token); + SearchWithIncrement(solver, Config.Iterations); s.Stop(); OnLog?.Invoke($"{s.Elapsed.TotalMilliseconds:0.00}ms {Config.Iterations / s.Elapsed.TotalSeconds / 1000:0.00} kI/s"); @@ -333,6 +380,8 @@ public sealed class Solver : IDisposable (_, state) = sim.Execute(state, chosenAction); actions.Add(chosenAction); + + IncrementProgressSequence(); } return Task.FromResult(new SolverSolution(actions, state)); @@ -340,6 +389,9 @@ public sealed class Solver : IDisposable private async Task SearchOneshotForked() { + var iterCount = Config.Iterations / Config.ForkCount; + maxProgress = iterCount * Config.ForkCount; + using var semaphore = new SemaphoreSlim(0, Config.MaxThreadCount); var tasks = new Task<(float MaxScore, SolverSolution Solution)>[Config.ForkCount]; for (var i = 0; i < Config.ForkCount; ++i) @@ -349,7 +401,7 @@ public sealed class Solver : IDisposable await semaphore.WaitAsync(Token).ConfigureAwait(false); try { - solver.Search(Config.Iterations / Config.ForkCount, Token); + SearchWithIncrement(solver, iterCount); } finally { @@ -375,8 +427,10 @@ public sealed class Solver : IDisposable private Task SearchOneshot() { + maxProgress = Config.Iterations; + var solver = new MCTS(MCTSConfig, State); - solver.Search(Config.Iterations, Token); + SearchWithIncrement(solver, Config.Iterations); var solution = solver.Solution(); foreach (var action in solution.Actions) InvokeNewAction(action);