diff --git a/src/evaluate.cpp b/src/evaluate.cpp index f4d18d8e4e0..c7adf50946f 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -92,11 +92,11 @@ std::string Eval::trace(Position& pos, const Eval::NNUE::Networks& networks) { Value v = networks.big.evaluate(pos, false); v = pos.side_to_move() == WHITE ? v : -v; - ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v) << " (white side)\n"; + ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v, pos) << " (white side)\n"; v = evaluate(networks, pos, VALUE_ZERO); v = pos.side_to_move() == WHITE ? v : -v; - ss << "Final evaluation " << 0.01 * UCI::to_cp(v) << " (white side)"; + ss << "Final evaluation " << 0.01 * UCI::to_cp(v, pos) << " (white side)"; ss << " [with scaled NNUE, ...]"; ss << "\n"; diff --git a/src/nnue/nnue_misc.cpp b/src/nnue/nnue_misc.cpp index 7005a61025e..725d90d27d6 100644 --- a/src/nnue/nnue_misc.cpp +++ b/src/nnue/nnue_misc.cpp @@ -54,11 +54,11 @@ void hint_common_parent_position(const Position& pos, const Networks& networks) namespace { // Converts a Value into (centi)pawns and writes it in a buffer. // The buffer must have capacity for at least 5 chars. -void format_cp_compact(Value v, char* buffer) { +void format_cp_compact(Value v, char* buffer, const Position& pos) { buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' '); - int cp = std::abs(UCI::to_cp(v)); + int cp = std::abs(UCI::to_cp(v, pos)); if (cp >= 10000) { buffer[1] = '0' + cp / 10000; @@ -90,9 +90,9 @@ void format_cp_compact(Value v, char* buffer) { // Converts a Value into pawns, always keeping two decimals -void format_cp_aligned_dot(Value v, std::stringstream& stream) { +void format_cp_aligned_dot(Value v, std::stringstream& stream, const Position& pos) { - const double pawns = std::abs(0.01 * UCI::to_cp(v)); + const double pawns = std::abs(0.01 * UCI::to_cp(v, pos)); stream << (v < 0 ? '-' : v > 0 ? '+' @@ -114,7 +114,7 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { board[row][8 * 8 + 1] = '\0'; // A lambda to output one box of the board - auto writeSquare = [&board](File file, Rank rank, Piece pc, Value value) { + auto writeSquare = [&board, &pos](File file, Rank rank, Piece pc, Value value) { const int x = int(file) * 8; const int y = (7 - int(rank)) * 3; for (int i = 1; i < 8; ++i) @@ -125,7 +125,7 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { if (pc != NO_PIECE) board[y + 1][x + 4] = PieceToChar[pc]; if (value != VALUE_NONE) - format_cp_compact(value, &board[y + 2][x + 2]); + format_cp_compact(value, &board[y + 2][x + 2], pos); }; // We estimate the value of each piece by doing a differential evaluation from @@ -180,13 +180,13 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) { { ss << "| " << bucket << " "; ss << " | "; - format_cp_aligned_dot(t.psqt[bucket], ss); + format_cp_aligned_dot(t.psqt[bucket], ss, pos); ss << " " << " | "; - format_cp_aligned_dot(t.positional[bucket], ss); + format_cp_aligned_dot(t.positional[bucket], ss, pos); ss << " " << " | "; - format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], ss); + format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], ss, pos); ss << " " << " |"; if (bucket == t.correctBucket) diff --git a/src/search.cpp b/src/search.cpp index fc92d1a9c32..9929ec27ed2 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -155,7 +155,8 @@ void Search::Worker::start_searching() { { rootMoves.emplace_back(Move::none()); sync_cout << "info depth 0 score " - << UCI::value(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW) << sync_endl; + << UCI::to_score(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos) + << sync_endl; } else { @@ -1898,10 +1899,10 @@ std::string SearchManager::pv(const Search::Worker& worker, ss << "info" << " depth " << d << " seldepth " << rootMoves[i].selDepth << " multipv " << i + 1 - << " score " << UCI::value(v); + << " score " << UCI::to_score(v, pos); if (worker.options["UCI_ShowWDL"]) - ss << UCI::wdl(v, pos.game_ply()); + ss << UCI::wdl(v, pos); if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact ss << (rootMoves[i].scoreLowerbound diff --git a/src/uci.cpp b/src/uci.cpp index cf0e3f09cc7..cc03005ffc0 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "benchmark.h" @@ -44,9 +45,8 @@ namespace Stockfish { -constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; -constexpr int NormalizeToPawnValue = 356; -constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; +constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; namespace NN = Eval::NNUE; @@ -338,15 +338,43 @@ void UCI::position(Position& pos, std::istringstream& is, StateListPtr& states) } } -int UCI::to_cp(Value v) { return 100 * v / NormalizeToPawnValue; } +namespace { +std::pair win_rate_params(const Position& pos) { + + int material = pos.count() + 3 * pos.count() + 3 * pos.count() + + 5 * pos.count() + 9 * pos.count(); + + // The fitted model only uses data for material counts in [10, 78], and is anchored at count 58. + double m = std::clamp(material, 10, 78) / 58.0; + + // Return a = p_a(material) and b = p_b(material), see github.com/official-stockfish/WDL_model + constexpr double as[] = {-185.71965483, 504.85014385, -438.58295743, 474.04604627}; + constexpr double bs[] = {89.23542728, -137.02141296, 73.28669021, 47.53376190}; + + double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3]; + double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3]; -std::string UCI::value(Value v) { + return {a, b}; +} + +// The win rate model is 1 / (1 + exp((a - eval) / b)), where a = p_a(material) and b = p_b(material). +// It fits the LTC fishtest statistics rather accurately. +int win_rate_model(Value v, const Position& pos) { + + auto [a, b] = win_rate_params(pos); + + // Return the win rate in per mille units, rounded to the nearest integer. + return int(0.5 + 1000 / (1 + std::exp((a - double(v)) / b))); +} +} + +std::string UCI::to_score(Value v, const Position& pos) { assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); std::stringstream ss; if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY) - ss << "cp " << to_cp(v); + ss << "cp " << to_cp(v, pos); else if (std::abs(v) <= VALUE_TB) { const int ply = VALUE_TB - std::abs(v); // recompute ss->ply @@ -358,6 +386,30 @@ std::string UCI::value(Value v) { return ss.str(); } +// Turns a Value to an integer centipawn number, +// without treatment of mate and similar special scores. +int UCI::to_cp(Value v, const Position& pos) { + + // In general, the score can be defined via the the WDL as + // (log(1/L - 1) - log(1/W - 1)) / ((log(1/L - 1) + log(1/W - 1)) + // Based on our win_rate_model, this simply yields v / a. + + auto [a, b] = win_rate_params(pos); + + return std::round(100 * int(v) / a); +} + +std::string UCI::wdl(Value v, const Position& pos) { + std::stringstream ss; + + int wdl_w = win_rate_model(v, pos); + int wdl_l = win_rate_model(-v, pos); + int wdl_d = 1000 - wdl_w - wdl_l; + ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l; + + return ss.str(); +} + std::string UCI::square(Square s) { return std::string{char('a' + file_of(s)), char('1' + rank_of(s))}; } @@ -383,41 +435,6 @@ std::string UCI::move(Move m, bool chess960) { return move; } -namespace { -// The win rate model returns the probability of winning (in per mille units) given an -// eval and a game ply. It fits the LTC fishtest statistics rather accurately. -int win_rate_model(Value v, int ply) { - - // The fitted model only uses data for moves in [8, 120], and is anchored at move 32. - double m = std::clamp(ply / 2 + 1, 8, 120) / 32.0; - - // The coefficients of a third-order polynomial fit is based on the fishtest data - // for two parameters that need to transform eval to the argument of a logistic - // function. - constexpr double as[] = {-1.06249702, 7.42016937, 0.89425629, 348.60356174}; - constexpr double bs[] = {-5.33122190, 39.57831533, -90.84473771, 123.40620748}; - - // Enforce that NormalizeToPawnValue corresponds to a 50% win rate at move 32. - static_assert(NormalizeToPawnValue == int(0.5 + as[0] + as[1] + as[2] + as[3])); - - double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3]; - double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3]; - - // Return the win rate in per mille units, rounded to the nearest integer. - return int(0.5 + 1000 / (1 + std::exp((a - double(v)) / b))); -} -} - -std::string UCI::wdl(Value v, int ply) { - std::stringstream ss; - - int wdl_w = win_rate_model(v, ply); - int wdl_l = win_rate_model(-v, ply); - int wdl_d = 1000 - wdl_w - wdl_l; - ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l; - - return ss.str(); -} Move UCI::to_move(const Position& pos, std::string& str) { if (str.length() == 5) diff --git a/src/uci.h b/src/uci.h index dd55862ad17..237928d9abc 100644 --- a/src/uci.h +++ b/src/uci.h @@ -42,11 +42,11 @@ class UCI { void loop(); - static int to_cp(Value v); - static std::string value(Value v); + static int to_cp(Value v, const Position& pos); + static std::string to_score(Value v, const Position& pos); static std::string square(Square s); static std::string move(Move m, bool chess960); - static std::string wdl(Value v, int ply); + static std::string wdl(Value v, const Position& pos); static Move to_move(const Position& pos, std::string& str); static Search::LimitsType parse_limits(const Position& pos, std::istream& is);