diff --git a/src/Lynx.Cli/appsettings.Development.json b/src/Lynx.Cli/appsettings.Development.json index 94480bb12..1e5b5c5c7 100644 --- a/src/Lynx.Cli/appsettings.Development.json +++ b/src/Lynx.Cli/appsettings.Development.json @@ -1,4 +1,7 @@ { + "GeneralSettings": { + "EnableTuning": true + }, "EngineSettings": { "DefaultMaxDepth": 3, "TranspositionTableEnabled": true, diff --git a/src/Lynx.Cli/appsettings.json b/src/Lynx.Cli/appsettings.json index f1c9cf8eb..43045a9e7 100644 --- a/src/Lynx.Cli/appsettings.json +++ b/src/Lynx.Cli/appsettings.json @@ -1,7 +1,8 @@ { // Settings that affect the executable behavior "GeneralSettings": { - "EnableLogging": true // logging can be completely disablesd, both console and file, setting this to false. Alternatively, one or more "NLog.rules" can be removed/tweaked + "EnableLogging": true, // logging can be completely disablesd, both console and file, setting this to false. Alternatively, one or more "NLog.rules" can be removed/tweaked + "EnableTuning": false // Exposes search tunable values via UCI. Intended to be used for developer only purposes }, // Settings that affect the engine behavior diff --git a/src/Lynx/Configuration.cs b/src/Lynx/Configuration.cs index 4a281b274..0364442f7 100644 --- a/src/Lynx/Configuration.cs +++ b/src/Lynx/Configuration.cs @@ -72,6 +72,8 @@ public static int Hash public sealed class GeneralSettings { public bool EnableLogging { get; set; } = false; + + public bool EnableTuning { get; set; } = false; } public sealed class EngineSettings diff --git a/src/Lynx/SPSAAttribute.cs b/src/Lynx/SPSAAttribute.cs index 00e9e0833..bda9f2870 100644 --- a/src/Lynx/SPSAAttribute.cs +++ b/src/Lynx/SPSAAttribute.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using NLog; +using System.Numerics; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; @@ -47,7 +48,7 @@ public string ToOBString(PropertyInfo property) return $"{property.Name}, int, {val}, {MinValue}, {MaxValue}, {Step}, {Configuration.EngineSettings.SPSA_OB_R_end}"; } - private static T GetPropertyValue(PropertyInfo property) + internal static T GetPropertyValue(PropertyInfo property) { T val = (T)property.GetValue(Configuration.EngineSettings)!; @@ -83,3 +84,148 @@ public string ToOBPrettyString(PropertyInfo property) #pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code } } + +public static class SPSAAttributeHelpers +{ + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public static IEnumerable GenerateOpenBenchStrings() + { + foreach (var property in typeof(EngineSettings).GetProperties()) + { + var genericType = typeof(SPSAAttribute<>); + var spsaArray = property.GetCustomAttributes(genericType); + var count = spsaArray.Count(); + + if (count > 1) + { + _logger.Warn("Property {0} has more than one [{1}]", property.Name, genericType.Name); + } + + if (count == 0) + { + continue; + } + + var genericSpsa = spsaArray.First(); + if (genericSpsa is SPSAAttribute intSpsa) + { + yield return intSpsa.ToOBString(property); + } + else if (genericSpsa is SPSAAttribute doubleSpsa) + { + yield return doubleSpsa.ToOBString(property); + } + else + { + _logger.Error("Property {0} has a [{1}] defined with unsupported type <{2}>", property.Name, genericSpsa); + } + } + } + + public static IEnumerable GenerateOpenBenchPrettyStrings() + { + foreach (var property in typeof(EngineSettings).GetProperties()) + { + var genericType = typeof(SPSAAttribute<>); + var spsaArray = property.GetCustomAttributes(genericType); + var count = spsaArray.Count(); + + if (count > 1) + { + _logger.Warn("Property {0} has more than one [{1}]", property.Name, genericType.Name); + } + + if (count == 0) + { + continue; + } + + var genericSpsa = spsaArray.First(); + if (genericSpsa is SPSAAttribute intSpsa) + { + yield return intSpsa.ToOBPrettyString(property); + } + else if (genericSpsa is SPSAAttribute doubleSpsa) + { + yield return doubleSpsa.ToOBPrettyString(property); + } + else + { + _logger.Error("Property {0} has a [{1}] defined with unsupported type <{2}>", property.Name, genericSpsa); + } + } + } + + public static IEnumerable> GenerateWeatherFactoryStrings() + { + foreach (var property in typeof(EngineSettings).GetProperties()) + { + var genericType = typeof(SPSAAttribute<>); + var spsaArray = property.GetCustomAttributes(genericType); + var count = spsaArray.Count(); + + if (count > 1) + { + _logger.Warn("Property {0} has more than one [{1}]", property.Name, genericType.Name); + } + + if (count == 0) + { + continue; + } + + var genericSpsa = spsaArray.First(); + if (genericSpsa is SPSAAttribute intSpsa) + { + yield return intSpsa.ToWeatherFactoryString(property); + } + else if (genericSpsa is SPSAAttribute doubleSpsa) + { + yield return doubleSpsa.ToWeatherFactoryString(property); + } + else + { + _logger.Error("Property {0} has a [{1}] defined with unsupported type <{2}>", property.Name, genericSpsa); + } + } + } + + public static IEnumerable GenerateOptionStrings() + { + foreach (var property in typeof(EngineSettings).GetProperties()) + { + var genericType = typeof(SPSAAttribute<>); + var spsaArray = property.GetCustomAttributes(genericType); + var count = spsaArray.Count(); + + if (count > 1) + { + _logger.Warn("Property {0} has more than one [{1}]", property.Name, genericType.Name); + } + + if (count == 0) + { + continue; + } + + var genericSpsa = spsaArray.First(); + if (genericSpsa is SPSAAttribute intSpsa) + { + var val = SPSAAttribute.GetPropertyValue(property); + + yield return $"option name {property.Name} type spin default {val} min {intSpsa.MinValue} max {intSpsa.MaxValue}"; + } + else if (genericSpsa is SPSAAttribute doubleSpsa) + { + var val = SPSAAttribute.GetPropertyValue(property); + + yield return $"option name {property.Name} type spin default {val} min {doubleSpsa.MinValue} max {doubleSpsa.MaxValue}"; + } + else + { + _logger.Error("Property {0} has a [{1}] defined with unsupported type <{2}>", property.Name, genericSpsa); + } + } + } +} diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 5bbb710fd..4426cb9d2 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -97,30 +97,6 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM staticEval = ttScore; } - // 🔍 Null Move Pruning (NMP) - our position is so good that we can potentially afford giving our opponent a double move and still remain ahead of beta - if (depth >= Configuration.EngineSettings.NMP_MinDepth - && staticEval >= beta - && !parentWasNullMove - && phase > 2 // Zugzwang risk reduction: pieces other than pawn presents - && (ttElementType != NodeType.Alpha || ttEvaluation >= beta)) // TT suggests NMP will fail: entry must not be a fail-low entry with a score below beta - Stormphrax and Ethereal - { - var nmpReduction = Configuration.EngineSettings.NMP_BaseDepthReduction + ((depth + Configuration.EngineSettings.NMP_DepthIncrement) / Configuration.EngineSettings.NMP_DepthDivisor); // Clarity - - // TODO more advanced adaptative reduction, similar to what Ethereal and Stormphrax are doing - //var nmpReduction = Math.Min( - // depth, - // 3 + (depth / 3) + Math.Min((staticEval - beta) / 200, 3)); - - var gameState = position.MakeNullMove(); - var evaluation = -NegaMax(depth - 1 - nmpReduction, ply + 1, -beta, -beta + 1, parentWasNullMove: true); - position.UnMakeNullMove(gameState); - - if (evaluation >= beta) - { - return evaluation; - } - } - if (depth <= Configuration.EngineSettings.RFP_MaxDepth) { // 🔍 Reverse Futility Pruning (RFP) - https://www.chessprogramming.org/Reverse_Futility_Pruning @@ -163,6 +139,30 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM } } } + + // 🔍 Null Move Pruning (NMP) - our position is so good that we can potentially afford giving our opponent a double move and still remain ahead of beta + if (depth >= Configuration.EngineSettings.NMP_MinDepth + && staticEval >= beta + && !parentWasNullMove + && phase > 2 // Zugzwang risk reduction: pieces other than pawn presents + && (ttElementType != NodeType.Alpha || ttEvaluation >= beta)) // TT suggests NMP will fail: entry must not be a fail-low entry with a score below beta - Stormphrax and Ethereal + { + var nmpReduction = Configuration.EngineSettings.NMP_BaseDepthReduction + ((depth + Configuration.EngineSettings.NMP_DepthIncrement) / Configuration.EngineSettings.NMP_DepthDivisor); // Clarity + + // TODO more advanced adaptative reduction, similar to what Ethereal and Stormphrax are doing + //var nmpReduction = Math.Min( + // depth, + // 3 + (depth / 3) + Math.Min((staticEval - beta) / 200, 3)); + + var gameState = position.MakeNullMove(); + var evaluation = -NegaMax(depth - 1 - nmpReduction, ply + 1, -beta, -beta + 1, parentWasNullMove: true); + position.UnMakeNullMove(gameState); + + if (evaluation >= beta) + { + return evaluation; + } + } } Span moves = stackalloc Move[Constants.MaxNumberOfPossibleMovesInAPosition]; diff --git a/src/Lynx/UCI/Commands/Engine/OptionCommand.cs b/src/Lynx/UCI/Commands/Engine/OptionCommand.cs index d8abe89b3..14e484d18 100644 --- a/src/Lynx/UCI/Commands/Engine/OptionCommand.cs +++ b/src/Lynx/UCI/Commands/Engine/OptionCommand.cs @@ -124,42 +124,15 @@ public sealed class OptionCommand : EngineBaseCommand public const string Id = "option"; public static readonly ImmutableArray AvailableOptions = - [ - "option name UCI_Opponent type string", - $"option name UCI_EngineAbout type string default {IdCommand.EngineName} by {IdCommand.EngineAuthor}, see https://github.com/lynx-chess/Lynx", - $"option name UCI_ShowWDL type check default {Configuration.EngineSettings.ShowWDL}", - $"option name Hash type spin default {Configuration.EngineSettings.TranspositionTableSize} min {Constants.AbsoluteMinTTSize} max {Constants.AbsoluteMaxTTSize}", - $"option name OnlineTablebaseInRootPositions type check default {Configuration.EngineSettings.UseOnlineTablebaseInRootPositions}", - "option name Threads type spin default 1 min 1 max 1", - - #region Search tuning - - $"option name {nameof(Configuration.EngineSettings.LMR_MinDepth)} type spin default {Configuration.EngineSettings.LMR_MinDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.LMR_MinFullDepthSearchedMoves)} type spin default {Configuration.EngineSettings.LMR_MinFullDepthSearchedMoves} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.LMR_Base)} type spin default {100 * Configuration.EngineSettings.LMR_Base} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.LMR_Divisor)} type spin default {100 * Configuration.EngineSettings.LMR_Divisor} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.NMP_MinDepth)} type spin default {Configuration.EngineSettings.NMP_MinDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.NMP_BaseDepthReduction)} type spin default {Configuration.EngineSettings.NMP_BaseDepthReduction} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.NMP_DepthIncrement)} type spin default {Configuration.EngineSettings.NMP_DepthIncrement} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.NMP_DepthDivisor)} type spin default {Configuration.EngineSettings.NMP_DepthDivisor} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.AspirationWindow_Delta)} type spin default {Configuration.EngineSettings.AspirationWindow_Delta} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.AspirationWindow_MinDepth)} type spin default {Configuration.EngineSettings.AspirationWindow_MinDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.RFP_MaxDepth)} type spin default {Configuration.EngineSettings.RFP_MaxDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.RFP_DepthScalingFactor)} type spin default {Configuration.EngineSettings.RFP_DepthScalingFactor} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.Razoring_MaxDepth)} type spin default {Configuration.EngineSettings.Razoring_MaxDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.Razoring_Depth1Bonus)} type spin default {Configuration.EngineSettings.Razoring_Depth1Bonus} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.Razoring_NotDepth1Bonus)} type spin default {Configuration.EngineSettings.Razoring_NotDepth1Bonus} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.IIR_MinDepth)} type spin default {Configuration.EngineSettings.IIR_MinDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.LMP_MaxDepth)} type spin default {Configuration.EngineSettings.LMP_MaxDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.LMP_BaseMovesToTry)} type spin default {Configuration.EngineSettings.LMP_BaseMovesToTry} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.LMP_MovesDepthMultiplier)} type spin default {Configuration.EngineSettings.LMP_MovesDepthMultiplier} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.SEE_BadCaptureReduction)} type spin default {Configuration.EngineSettings.SEE_BadCaptureReduction} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.FP_MaxDepth)} type spin default {Configuration.EngineSettings.FP_MaxDepth} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.FP_DepthScalingFactor)} type spin default {Configuration.EngineSettings.FP_DepthScalingFactor} min 0 max 1024", - $"option name {nameof(Configuration.EngineSettings.FP_Margin)} type spin default {Configuration.EngineSettings.FP_Margin} min 0 max 1024", - - #endregion - ]; + [ + "option name UCI_Opponent type string", + $"option name UCI_EngineAbout type string default {IdCommand.EngineName} by {IdCommand.EngineAuthor}, see https://github.com/lynx-chess/Lynx", + $"option name UCI_ShowWDL type check default {Configuration.EngineSettings.ShowWDL}", + $"option name Hash type spin default {Configuration.EngineSettings.TranspositionTableSize} min {Constants.AbsoluteMinTTSize} max {Constants.AbsoluteMaxTTSize}", + $"option name OnlineTablebaseInRootPositions type check default {Configuration.EngineSettings.UseOnlineTablebaseInRootPositions}", + "option name Threads type spin default 1 min 1 max 1", + .. Configuration.GeneralSettings.EnableTuning ? SPSAAttributeHelpers.GenerateOptionStrings() : [] + ]; //"option name UCI_AnalyseMode type check", //"option name NalimovPath type string default C:/...", diff --git a/src/Lynx/UCIHandler.cs b/src/Lynx/UCIHandler.cs index d73ffad3b..9d0d7eac6 100644 --- a/src/Lynx/UCIHandler.cs +++ b/src/Lynx/UCIHandler.cs @@ -608,35 +608,9 @@ private async Task HandleFEN(CancellationToken cancellationToken) private async ValueTask HandleOpenBenchSPSA(CancellationToken cancellationToken) { - foreach (var property in typeof(EngineSettings).GetProperties()) + foreach(var tunableValue in SPSAAttributeHelpers.GenerateOpenBenchStrings()) { - var genericType = typeof(SPSAAttribute<>); - var spsaArray = property.GetCustomAttributes(genericType); - var count = spsaArray.Count(); - - if (count > 1) - { - _logger.Warn("Property {0} has more than one [{1}]", property.Name, genericType.Name); - } - - if (count == 0) - { - continue; - } - - var genericSpsa = spsaArray.First(); - if (genericSpsa is SPSAAttribute intSpsa) - { - await SendCommand(intSpsa.ToOBString(property), cancellationToken); - } - else if (genericSpsa is SPSAAttribute doubleSpsa) - { - await SendCommand(doubleSpsa.ToOBString(property), cancellationToken); - } - else - { - _logger.Error("Property {0} has a [{1}] defined with unsupported type <{2}>", property.Name, genericSpsa); - } + await SendCommand(tunableValue, cancellationToken); } } @@ -648,75 +622,17 @@ await SendCommand( + "-----------------------------------------------------------------------", cancellationToken); - foreach (var property in typeof(EngineSettings).GetProperties()) + foreach (var tunableValue in SPSAAttributeHelpers.GenerateOpenBenchPrettyStrings()) { - var genericType = typeof(SPSAAttribute<>); - var spsaArray = property.GetCustomAttributes(genericType); - var count = spsaArray.Count(); - - if (count > 1) - { - _logger.Warn("Property {0} has more than one [{1}]", property.Name, genericType.Name); - } - - if (count == 0) - { - continue; - } - - var genericSpsa = spsaArray.First(); - if (genericSpsa is SPSAAttribute intSpsa) - { - await SendCommand(intSpsa.ToOBPrettyString(property), cancellationToken); - } - else if (genericSpsa is SPSAAttribute doubleSpsa) - { - await SendCommand(doubleSpsa.ToOBPrettyString(property), cancellationToken); - } - else - { - _logger.Error("Property {0} has a [{1}] defined with unsupported type <{2}>", property.Name, genericSpsa); - } + await SendCommand(tunableValue, cancellationToken); } } private async ValueTask HandleWeatherFactorySPSA(CancellationToken cancellationToken) { - var properties = typeof(EngineSettings).GetProperties(); - List> parameters = new(properties.Length); - - foreach (var property in properties) - { - var genericType = typeof(SPSAAttribute<>); - var spsaArray = property.GetCustomAttributes(genericType); - var count = spsaArray.Count(); - - if (count > 1) - { - _logger.Warn("Property {0} has more than one [{1}]", property.Name, genericType.Name); - } - - if (count == 0) - { - continue; - } - - var genericSpsa = spsaArray.First(); - if (genericSpsa is SPSAAttribute intSpsa) - { - parameters.Add(intSpsa.ToWeatherFactoryString(property)); - } - else if (genericSpsa is SPSAAttribute doubleSpsa) - { - parameters.Add(doubleSpsa.ToWeatherFactoryString(property)); - } - else - { - _logger.Error("Property {0} has a [{1}] defined with unsupported type <{2}>", property.Name, genericSpsa); - } - } + var tunableValues = SPSAAttributeHelpers.GenerateWeatherFactoryStrings(); - await SendCommand(new JsonObject(parameters).ToString(), cancellationToken); + await SendCommand(new JsonObject(tunableValues).ToString(), cancellationToken); } #endregion