/// <summary> /// Worker method that actually computes and updates the statistic. /// </summary> private void DoSetEarlyStopMoveSecondaryFlags() { if (Manager.Root.NumChildrenExpanded == 0) { return; } float aggressiveness = Context.ParamsSearch.MoveFutilityPruningAggressiveness; if (aggressiveness > 1.0f) { throw new Exception("Ceres configuration error: maximum value of EarlyStopMoveSecondaryAggressiveness is 1.0."); } float MIN_BEST_N_FRAC_REQUIRED = ManagerChooseRootMove.MIN_FRAC_N_REQUIRED_MIN(Context); // Calibrate aggressiveness such that : // - at maximum value of 1.0 we assume 50% visits go to second best move(s) // - at reasonable default value 0.5 we assume 75% of visits to go second best move(s) float aggressivenessMultiplier = 1.0f / (1.0f - aggressiveness * 0.5f); int?numRemainingSteps = Manager.EstimatedNumVisitsRemaining(); // Can't make any determiniation if we can't estimate how many steps left if (numRemainingSteps is null) { return; } int numStepsTotal = (int)(numRemainingSteps / Manager.FractionSearchRemaining); MCTSNode[] nodesSortedN = Root.ChildrenSorted(n => - n.N); MCTSNode bestNNode = nodesSortedN[0]; int minN = Root.N / 5;// (int)(bestQNode.N * MIN_BEST_N_FRAC_REQUIRED); MCTSNode[] nodesSortedQ = Root.ChildrenSorted(n => n.N < minN ? int.MaxValue : (float)n.Q); MCTSNode bestQNode = nodesSortedQ[0]; float bestQ = (float)bestQNode.Q; ManagerChooseRootMove bestMoveChoser = new(Context.Root, false, Context.ParamsSearch.MLHBonusFactor); Span <MCTSNodeStructChild> children = Root.Ref.Children; for (int i = 0; i < Root.Ref.NumChildrenExpanded; i++) { // Never shut down second best move unless the whole search is eligible to shut down if (nodesSortedN.Length > 1) { MCTSNode secondBestMove = Context.ParamsSearch.BestMoveMode == ParamsSearch.BestMoveModeEnum.TopN ? nodesSortedN[1] : nodesSortedQ[1]; bool isSecondBestMove = children[i].Move == secondBestMove.PriorMove; if (isSecondBestMove && !Context.ParamsSearch.FutilityPruningStopSearchEnabled) { Context.RootMovesArePruned[i] = false; continue; } } float earlyStopGapRaw; if (Context.ParamsSearch.BestMoveMode == ParamsSearch.BestMoveModeEnum.TopN) { earlyStopGapRaw = nodesSortedN[0].N - children[i].N; } else { int minNRequired = (int)(bestQNode.N * MIN_BEST_N_FRAC_REQUIRED); earlyStopGapRaw = minNRequired - children[i].N; } float earlyStopGapAjusted = earlyStopGapRaw * aggressivenessMultiplier; bool earlyStopSimple = earlyStopGapRaw > numRemainingSteps; if (MCTSDiagnostics.DumpSearchFutilityShutdown && earlyStopSimple && !Context.RootMovesArePruned[i] && NumberOfNotShutdownChildren() < 3) { Console.WriteLine(); Console.WriteLine($"\r\nShutdown {children[i].Move} [{children[i].N}] at root N {Context.Root.N} with remaning {numRemainingSteps}" + $" due to raw gapN {earlyStopGapRaw} adusted to {earlyStopGapAjusted} in mode {Context.ParamsSearch.BestMoveMode} aggmult {aggressivenessMultiplier}"); DumpDiagnosticsMoveShutdown(); } Context.RootMovesArePruned[i] = earlyStopSimple; // Console.WriteLine(i + $" EarlyStopMoveSecondary(simple) gap={gapToBest} adjustedGap={inflatedGap} remaining={numRemainingSteps} "); } // TODO: log this //Console.WriteLine($"{Context.RemainingTime,5:F2}sec remains at N={Root.N}, setting minN to {minN} number still considered {count} " + // $"using EstimatedNPS {Context.EstimatedNPS} with nodes remaining {Context.EstimatedNumStepsRemaining()} " + // statsStr); }
/// <summary> /// Worker method that actually computes and updates the statistic. /// </summary> private void DoSetEarlyStopMoveSecondaryFlags() { if (Manager.Root.NumChildrenExpanded == 0) { return; } float aggressiveness = Context.ParamsSearch.MoveFutilityPruningAggressiveness; if (aggressiveness >= 1.5f) { throw new Exception("Ceres configuration error: maximum value of EarlyStopMoveSecondaryAggressiveness is 1.5."); } float MIN_BEST_N_FRAC_REQUIRED = ManagerChooseRootMove.MIN_FRAC_N_REQUIRED_MIN; // Calibrate aggressiveness such that : // - at maximum value of 1.0 we assume 50% visits go to second best move(s) // - at reasonable default value 0.5 we assume 75% of visits to go second best move(s) float aggressivenessMultiplier = 1.0f / (1.0f - aggressiveness * 0.5f); int?numRemainingSteps = Manager.EstimatedNumVisitsRemaining(); // Can't make any determiniation if we can't estimate how many steps left if (numRemainingSteps is null) { return; } int numStepsTotal = (int)(numRemainingSteps / Manager.FractionSearchRemaining); MCTSNode[] nodesSortedN = Root.ChildrenSorted(n => - n.N); MCTSNode bestNNode = nodesSortedN[0]; int minN = Root.N / 5;// (int)(bestQNode.N * MIN_BEST_N_FRAC_REQUIRED); MCTSNode[] nodesSortedQ = Root.ChildrenSorted(n => n.N < minN ? int.MaxValue : (float)n.Q); MCTSNode bestQNode = nodesSortedQ[0]; float bestQ = (float)bestQNode.Q; ManagerChooseRootMove bestMoveChoser = new(Context.Root, false, Context.ParamsSearch.MLHBonusFactor); Span <MCTSNodeStructChild> children = Root.Ref.Children; for (int i = 0; i < Root.Ref.NumChildrenExpanded; i++) { // Never shut down second best move unless the whole search is eligible to shut down if (nodesSortedN.Length > 1) { MCTSNode secondBestMove = Context.ParamsSearch.BestMoveMode == ParamsSearch.BestMoveModeEnum.TopN ? nodesSortedN[1] : nodesSortedQ[1]; bool isSecondBestMove = children[i].Move == secondBestMove.PriorMove; if (isSecondBestMove && !Context.ParamsSearch.FutilityPruningStopSearchEnabled && Context.RootMovesPruningStatus[i] != MCTSFutilityPruningStatus.PrunedDueToSearchMoves) { Context.RootMovesPruningStatus[i] = MCTSFutilityPruningStatus.NotPruned; continue; } } float earlyStopGapRaw; if (Context.ParamsSearch.BestMoveMode == ParamsSearch.BestMoveModeEnum.TopN) { earlyStopGapRaw = nodesSortedN[0].N - children[i].N; } else { int minNRequired = (int)(bestQNode.N * MIN_BEST_N_FRAC_REQUIRED); earlyStopGapRaw = minNRequired - children[i].N; } float earlyStopGapAdjusted = earlyStopGapRaw * aggressivenessMultiplier; bool earlyStop = earlyStopGapAdjusted > numRemainingSteps; if (Context.RootMovesPruningStatus[i] == MCTSFutilityPruningStatus.NotPruned && earlyStop) { #if NOT_HELPFUL // Never shutdown nodes getting large fraction of all visits float fracVisitsThisMoveRunningAverage = Context.RootMoveTracker != null ? Context.RootMoveTracker.RunningFractionVisits[i] : 0; const float THRESHOLD_VISIT_FRACTION_DO_NOT_SHUTDOWN_CHILD = 0.20f; bool shouldVeto = (fracVisitsThisMoveRunningAverage > THRESHOLD_VISIT_FRACTION_DO_NOT_SHUTDOWN_CHILD); if (Context.ParamsSearch.TestFlag && nodesSortedN[0] != Context.Root.ChildAtIndex(i) && NumberOfNotShutdownChildren() > 1 && shouldVeto) { MCTSEventSource.TestCounter1++; continue; } else #endif Context.RootMovesPruningStatus[i] = MCTSFutilityPruningStatus.PrunedDueToFutility; if (MCTSDiagnostics.DumpSearchFutilityShutdown) { Console.WriteLine(); Console.WriteLine($"\r\nShutdown {children[i].Move} [{children[i].N}] at root N {Context.Root.N} with remaning {numRemainingSteps}" + $" due to raw gapN {earlyStopGapRaw} adusted to {earlyStopGapAdjusted} in mode {Context.ParamsSearch.BestMoveMode} aggmult {aggressivenessMultiplier}"); DumpDiagnosticsMoveShutdown(); } } // Console.WriteLine(i + $" EarlyStopMoveSecondary(simple) gap={gapToBest} adjustedGap={inflatedGap} remaining={numRemainingSteps} "); } // TODO: log this //Console.WriteLine($"{Context.RemainingTime,5:F2}sec remains at N={Root.N}, setting minN to {minN} number still considered {count} " + // $"using EstimatedNPS {Context.EstimatedNPS} with nodes remaining {Context.EstimatedNumStepsRemaining()} " + // statsStr); }