diff --git a/CMakeLists.txt b/CMakeLists.txt index d03a6a9..40f71d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(ironphoenix_core STATIC src/uci.cpp include/ironphoenix/eval.hpp include/ironphoenix/bishop_eval.hpp + include/ironphoenix/iir.hpp include/ironphoenix/search_stack.hpp include/ironphoenix/lmr.hpp include/ironphoenix/nnue.hpp @@ -211,6 +212,12 @@ if(IRONPHOENIX_BUILD_TESTS) target_compile_features(ironphoenix_knight_outpost PRIVATE cxx_std_20) add_test(NAME ironphoenix_knight_outpost COMMAND ironphoenix_knight_outpost) + add_executable(ironphoenix_iir tests/iir.cpp) + target_link_libraries(ironphoenix_iir PRIVATE IronPhoenix::Core) + target_compile_features(ironphoenix_iir PRIVATE cxx_std_20) + add_test(NAME ironphoenix_iir COMMAND ironphoenix_iir) + set_target_properties(ironphoenix_iir PROPERTIES FOLDER "Tests") + add_executable(ironphoenix_bishop_features tests/bishop_features.cpp) target_link_libraries(ironphoenix_bishop_features PRIVATE IronPhoenix::Core) target_compile_features(ironphoenix_bishop_features PRIVATE cxx_std_20) diff --git a/docs/internal-iterative-reduction.md b/docs/internal-iterative-reduction.md new file mode 100644 index 0000000..1abbddc --- /dev/null +++ b/docs/internal-iterative-reduction.md @@ -0,0 +1,27 @@ +# Internal Iterative Reduction (4PC Teams) + +IIR is now allowed at **every `negamax` node**, not just PV nodes. +There is no PV/cut/all-node, king-check, partner-check, or root exclusion. +With the current defaults, the rule is equivalent to: + +```cpp +if (depth >= tuning.iirMinDepth && !ttMove) + depth -= std::min(tuning.iirReduction, depth - 1); +``` + +`SearchIIRMinDepth` and `SearchIIRReduction` are the only two UCI knobs. +The old `SearchIIRCutNode` and `SearchIIRAllNode` toggles were removed +because IIR no longer depends on node types; cut/all propagation was +also removed from `negamax`. + +| UCI option | Default | Valid range | Purpose | +|---|---:|---:|---| +| `SearchIIRMinDepth` | 6 | 2–32 | Minimum depth to apply IIR | +| `SearchIIRReduction` | 1 | 0–4 | Depth reduction; 0 disables | + +All other tuned search and evaluation values are unchanged. A safety clamp +keeps the remaining depth >= 1, even when tuning a large reduction. + +**Important:** Applying IIR during checks, at the root and at non-PV +nodes can reduce tactical accuracy and playing strength. Run independent +paired SPRT before merging this aggressive variant into `main`. diff --git a/include/ironphoenix/iir.hpp b/include/ironphoenix/iir.hpp new file mode 100644 index 0000000..ae0ca5d --- /dev/null +++ b/include/ironphoenix/iir.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "tuning.hpp" + +#include + +namespace ironphoenix::IIR { + +// Internal Iterative Reduction for every negamax node without a TT move. +// The minimum depth and reduction size are independently UCI-tunable. +[[nodiscard]] inline int reduction(int depth, bool hasTTMove, + const Tuning::SearchParameters& tune) noexcept { + if (depth < tune.iirMinDepth || hasTTMove || tune.iirReduction <= 0) + return 0; + // Even when the configured reduction exceeds this node's depth, keep + // at least one search ply instead of feeding a negative depth to move gen. + return std::min(tune.iirReduction, depth - 1); +} + +} // namespace ironphoenix::IIR diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 84da1c4..6a9a22d 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -108,7 +108,8 @@ namespace ironphoenix { void run(Position position, std::ostream& out); void runMultiPV(Position position, int multiPV, std::ostream& out); - int negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull = true); + int negamax(Position& pos, int depth, int alpha, int beta, int ply, + bool pvNode, bool allowNull = true); int qsearch(Position& pos, int alpha, int beta, int ply, bool pvNode); int evaluate(const Position& pos) const noexcept; void recordStackEvaluation(int ply, int score) noexcept; diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index 68dc088..64684f1 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -10,27 +10,30 @@ namespace ironphoenix { namespace Tuning { struct SearchParameters final { - int aspirationStartDepth = 4; - int aspirationInitialDelta = 50; + int aspirationStartDepth = 2; + int aspirationInitialDelta = 54; int seePruneMaxDepth = 4; - int seePruneMarginPerDepth = 80; - int historyBonusScale = 64; - int historyBonusMax = 2048; - int counterMoveBonus = 32'000; - int checkMoveBonus = 10'000'000; - int lmrMinDepth = 3; - int lmrFullMoves = 4; - int lmrBaseX100 = 78; - int lmrDivisorX100 = 286; - int lmpHistoryThreshold = 1918; + int seePruneMarginPerDepth = 87; + int historyBonusScale = 63; + int historyBonusMax = 2044; + int counterMoveBonus = 31'995; + int checkMoveBonus = 9'999'994; + int lmrMinDepth = 1; + int lmrFullMoves = 6; + int lmrBaseX100 = 79; + int lmrDivisorX100 = 300; + int lmpHistoryThreshold = 1927; int hmpMaxDepth = 4; - int hmpMarginPerDepth = 1519; - int nullMoveMinDepth = 2; - int nullMoveDivisor = 192; + int hmpMarginPerDepth = 1522; + int nullMoveMinDepth = 3; + int nullMoveDivisor = 194; int rfpMaxDepth = 6; int rfpMarginPerDepth = 141; - int rfpMinMargin = 88; - int rfpMaxImprovingDiscount = 78; + int rfpMinMargin = 93; + int rfpMaxImprovingDiscount = 82; + // Applies to every search node without a TT move at depth >= 6. + int iirMinDepth = 6; + int iirReduction = 1; }; struct EvalParameters final { diff --git a/src/search.cpp b/src/search.cpp index 770f84c..a7a9796 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -2,6 +2,7 @@ #include "ironphoenix/eval.hpp" #include "ironphoenix/lmr.hpp" +#include "ironphoenix/iir.hpp" #include "ironphoenix/tuning.hpp" #include "ironphoenix/movegen.hpp" @@ -687,7 +688,8 @@ namespace ironphoenix { return false; } - int SearchEngine::negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull) { + int SearchEngine::negamax(Position& pos, int depth, int alpha, int beta, int ply, + bool pvNode, bool allowNull) { if (ply >= MAX_PLY - 1) return evaluate(pos); if (shouldStop()) @@ -804,6 +806,13 @@ namespace ironphoenix { } } + // IIR applies at any search node (PV, cut, all, root, or check) + // once the configured depth is reached without a TT ordering move. + // The helper clamps the reduction to keep depth >= 1. + const int iir = IIR::reduction(depth, static_cast(ttMove), tuning); + if (iir > 0) + depth -= iir; + MoveList moves; generatePseudoLegalMoves(pos, moves); diff --git a/src/tuning.cpp b/src/tuning.cpp index 749b483..255ebb8 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -55,7 +55,9 @@ namespace ironphoenix::Tuning { || name == "SearchRFPMaxDepth" || name == "SearchRFPMarginPerDepth" || name == "SearchRFPMinMargin" - || name == "SearchRFPMaxImprovingDiscount"; + || name == "SearchRFPMaxImprovingDiscount" + || name == "SearchIIRMinDepth" + || name == "SearchIIRReduction"; } bool isEvalParameter(std::string_view name) noexcept { @@ -164,6 +166,10 @@ namespace ironphoenix::Tuning { p.rfpMinMargin = value; else if (name == "SearchRFPMaxImprovingDiscount" && inRange(value, 0, 5000)) p.rfpMaxImprovingDiscount = value; + else if (name == "SearchIIRMinDepth" && inRange(value, 2, 32)) + p.iirMinDepth = value; + else if (name == "SearchIIRReduction" && inRange(value, 0, 4)) + p.iirReduction = value; else changed = false; @@ -270,28 +276,31 @@ namespace ironphoenix::Tuning { } void printUciOptions(std::ostream& out) { + const SearchParameters searchDefaults{}; const EvalParameters defaults{}; - out << "option name SearchAspirationStartDepth type spin default 4 min 1 max 32\n" - << "option name SearchAspirationDelta type spin default 50 min 1 max 2000\n" - << "option name SearchSEEPruneMaxDepth type spin default 4 min 0 max 32\n" - << "option name SearchSEEPruneMargin type spin default 80 min 0 max 2000\n" - << "option name SearchHistoryBonusScale type spin default 64 min 0 max 2048\n" - << "option name SearchHistoryBonusMax type spin default 2048 min 1 max 32767\n" - << "option name SearchCounterMoveBonus type spin default 32000 min 0 max 50000000\n" - << "option name SearchCheckMoveBonus type spin default 10000000 min 0 max 50000000\n" - << "option name SearchLMRMinDepth type spin default 3 min 1 max 32\n" - << "option name SearchLMRFullMoves type spin default 3 min 0 max 128\n" - << "option name SearchLMRBaseX100 type spin default 78 min 0 max 1000\n" - << "option name SearchLMRDivisorX100 type spin default 286 min 25 max 5000\n" - << "option name SearchLMPHistoryThreshold type spin default 2000 min 0 max 20000\n" - << "option name SearchHMPMaxDepth type spin default 4 min 0 max 32\n" - << "option name SearchHMPMarginPerDepth type spin default 1500 min 0 max 10000\n" - << "option name SearchNullMoveMinDepth type spin default 2 min 1 max 32\n" - << "option name SearchNullMoveDivisor type spin default 192 min 1 max 5000\n" - << "option name SearchRFPMaxDepth type spin default 6 min 0 max 32\n" - << "option name SearchRFPMarginPerDepth type spin default 141 min 0 max 5000\n" - << "option name SearchRFPMinMargin type spin default 88 min 0 max 5000\n" - << "option name SearchRFPMaxImprovingDiscount type spin default 78 min 0 max 5000\n" + out << "option name SearchAspirationStartDepth type spin default " << searchDefaults.aspirationStartDepth << " min 1 max 32\n" + << "option name SearchAspirationDelta type spin default " << searchDefaults.aspirationInitialDelta << " min 1 max 2000\n" + << "option name SearchSEEPruneMaxDepth type spin default " << searchDefaults.seePruneMaxDepth << " min 0 max 32\n" + << "option name SearchSEEPruneMargin type spin default " << searchDefaults.seePruneMarginPerDepth << " min 0 max 2000\n" + << "option name SearchHistoryBonusScale type spin default " << searchDefaults.historyBonusScale << " min 0 max 2048\n" + << "option name SearchHistoryBonusMax type spin default " << searchDefaults.historyBonusMax << " min 1 max 32767\n" + << "option name SearchCounterMoveBonus type spin default " << searchDefaults.counterMoveBonus << " min 0 max 50000000\n" + << "option name SearchCheckMoveBonus type spin default " << searchDefaults.checkMoveBonus << " min 0 max 50000000\n" + << "option name SearchLMRMinDepth type spin default " << searchDefaults.lmrMinDepth << " min 1 max 32\n" + << "option name SearchLMRFullMoves type spin default " << searchDefaults.lmrFullMoves << " min 0 max 128\n" + << "option name SearchLMRBaseX100 type spin default " << searchDefaults.lmrBaseX100 << " min 0 max 1000\n" + << "option name SearchLMRDivisorX100 type spin default " << searchDefaults.lmrDivisorX100 << " min 25 max 5000\n" + << "option name SearchLMPHistoryThreshold type spin default " << searchDefaults.lmpHistoryThreshold << " min 0 max 20000\n" + << "option name SearchHMPMaxDepth type spin default " << searchDefaults.hmpMaxDepth << " min 0 max 32\n" + << "option name SearchHMPMarginPerDepth type spin default " << searchDefaults.hmpMarginPerDepth << " min 0 max 10000\n" + << "option name SearchNullMoveMinDepth type spin default " << searchDefaults.nullMoveMinDepth << " min 1 max 32\n" + << "option name SearchNullMoveDivisor type spin default " << searchDefaults.nullMoveDivisor << " min 1 max 5000\n" + << "option name SearchRFPMaxDepth type spin default " << searchDefaults.rfpMaxDepth << " min 0 max 32\n" + << "option name SearchRFPMarginPerDepth type spin default " << searchDefaults.rfpMarginPerDepth << " min 0 max 5000\n" + << "option name SearchRFPMinMargin type spin default " << searchDefaults.rfpMinMargin << " min 0 max 5000\n" + << "option name SearchRFPMaxImprovingDiscount type spin default " << searchDefaults.rfpMaxImprovingDiscount << " min 0 max 5000\n" + << "option name SearchIIRMinDepth type spin default " << searchDefaults.iirMinDepth << " min 2 max 32\n" + << "option name SearchIIRReduction type spin default " << searchDefaults.iirReduction << " min 0 max 4\n" << "option name EvalPawnValue type spin default " << defaults.pawnValue << " min 0 max 5000\n" << "option name EvalKnightValue type spin default " << defaults.knightValue << " min 0 max 5000\n" << "option name EvalBishopValue type spin default " << defaults.bishopValue << " min 0 max 5000\n" @@ -340,7 +349,9 @@ namespace ironphoenix::Tuning { << " SearchRFPMaxDepth=" << s.rfpMaxDepth << " SearchRFPMarginPerDepth=" << s.rfpMarginPerDepth << " SearchRFPMinMargin=" << s.rfpMinMargin - << " SearchRFPMaxImprovingDiscount=" << s.rfpMaxImprovingDiscount << '\n'; + << " SearchRFPMaxImprovingDiscount=" << s.rfpMaxImprovingDiscount + << " SearchIIRMinDepth=" << s.iirMinDepth + << " SearchIIRReduction=" << s.iirReduction << '\n'; out << "info string EvalPawnValue=" << e.pawnValue << " EvalKnightValue=" << e.knightValue diff --git a/tests/iir.cpp b/tests/iir.cpp new file mode 100644 index 0000000..9213cd7 --- /dev/null +++ b/tests/iir.cpp @@ -0,0 +1,117 @@ +#include "ironphoenix/geometry.hpp" +#include "ironphoenix/iir.hpp" +#include "ironphoenix/search.hpp" +#include "ironphoenix/tuning.hpp" + +#include +#include +#include +#include + +using namespace ironphoenix; + +namespace { +int failures = 0; +void expect(bool passed, const char* name) { + if (!passed) { std::cerr << "FAIL: " << name << '\n'; ++failures; } +} + +int reduce(int depth, bool hasTTMove) { + return IIR::reduction(depth, hasTTMove, Tuning::search()); +} +} // namespace + +int main() { + Geometry::init(); + Tuning::resetSearch(); + const Tuning::SearchParameters defaults{}; + + struct Expected { const char* name; int Tuning::SearchParameters::*field; int value; }; + const Expected tuned[] = { + {"SearchAspirationStartDepth", &Tuning::SearchParameters::aspirationStartDepth, 2}, + {"SearchAspirationDelta", &Tuning::SearchParameters::aspirationInitialDelta, 54}, + {"SearchSEEPruneMaxDepth", &Tuning::SearchParameters::seePruneMaxDepth, 4}, + {"SearchSEEPruneMargin", &Tuning::SearchParameters::seePruneMarginPerDepth, 87}, + {"SearchHistoryBonusScale", &Tuning::SearchParameters::historyBonusScale, 63}, + {"SearchHistoryBonusMax", &Tuning::SearchParameters::historyBonusMax, 2044}, + {"SearchCounterMoveBonus", &Tuning::SearchParameters::counterMoveBonus, 31995}, + {"SearchCheckMoveBonus", &Tuning::SearchParameters::checkMoveBonus, 9999994}, + {"SearchLMRMinDepth", &Tuning::SearchParameters::lmrMinDepth, 1}, + {"SearchLMRFullMoves", &Tuning::SearchParameters::lmrFullMoves, 6}, + {"SearchLMRBaseX100", &Tuning::SearchParameters::lmrBaseX100, 79}, + {"SearchLMRDivisorX100", &Tuning::SearchParameters::lmrDivisorX100, 300}, + {"SearchLMPHistoryThreshold", &Tuning::SearchParameters::lmpHistoryThreshold, 1927}, + {"SearchHMPMaxDepth", &Tuning::SearchParameters::hmpMaxDepth, 4}, + {"SearchHMPMarginPerDepth", &Tuning::SearchParameters::hmpMarginPerDepth, 1522}, + {"SearchNullMoveMinDepth", &Tuning::SearchParameters::nullMoveMinDepth, 3}, + {"SearchNullMoveDivisor", &Tuning::SearchParameters::nullMoveDivisor, 194}, + {"SearchRFPMaxDepth", &Tuning::SearchParameters::rfpMaxDepth, 6}, + {"SearchRFPMarginPerDepth", &Tuning::SearchParameters::rfpMarginPerDepth, 141}, + {"SearchRFPMinMargin", &Tuning::SearchParameters::rfpMinMargin, 93}, + {"SearchRFPMaxImprovingDiscount", &Tuning::SearchParameters::rfpMaxImprovingDiscount, 82}, + {"SearchIIRMinDepth", &Tuning::SearchParameters::iirMinDepth, 6}, + {"SearchIIRReduction", &Tuning::SearchParameters::iirReduction, 1}, + }; + std::ostringstream defaultsUci; + Tuning::printUciOptions(defaultsUci); + for (const auto& row : tuned) { + expect(Tuning::search().*(row.field) == row.value, + "SPSA search default matches requested value"); + expect(defaultsUci.str().find(std::string("option name ") + row.name + + " type spin default " + std::to_string(row.value) + " min ") + != std::string::npos, + "UCI advertises current SPSA default"); + } + expect(defaults.iirMinDepth == 6, "IIR default depth six"); + expect(defaults.iirReduction == 1, "IIR default reduction one"); + + // IIR no longer restricts PV/cut/all nodes, the root, or check status. + // Once the depth threshold is met, absence of a TT move is sufficient. + expect(reduce(5, false) == 0, "depth below threshold"); + expect(reduce(6, false) == 1, "eligible depth six"); + expect(reduce(9, false) == 1, "deep node qualifies"); + expect(reduce(9, true) == 0, "TT move still suppresses IIR"); + + expect(Tuning::setSearchParameter("SearchIIRMinDepth", 4), "set min depth"); + expect(Tuning::setSearchParameter("SearchIIRReduction", 3), "set reduction"); + expect(reduce(4, false) == 3, "configurable reduction"); + expect(reduce(4, true) == 0, "TT move suppresses tuned reduction"); + + expect(Tuning::setSearchParameter("SearchIIRMinDepth", 2), "accept min depth 2"); + expect(reduce(2, false) == 1, "clamp keeps at least one depth"); + expect(Tuning::setSearchParameter("SearchIIRReduction", 0), "disable IIR"); + expect(reduce(9, false) == 0, "zero reduction disables feature"); + + expect(!Tuning::setSearchParameter("SearchIIRMinDepth", 1), "reject min depth 1"); + expect(!Tuning::setSearchParameter("SearchIIRMinDepth", 33), "reject min depth 33"); + expect(!Tuning::setSearchParameter("SearchIIRReduction", -1), "reject negative reduction"); + expect(!Tuning::setSearchParameter("SearchIIRReduction", 5), "reject oversized reduction"); + expect(!Tuning::setSearchParameter("SearchIIRCutNode", 1), "obsolete cut knob rejected"); + expect(!Tuning::setSearchParameter("SearchIIRAllNode", 1), "obsolete all knob rejected"); + + auto engine = std::make_unique(); + std::ostringstream out; + expect(Tuning::handleSetOption("name SearchIIRMinDepth value 7", *engine, out), + "UCI setoption recognized"); + expect(Tuning::search().iirMinDepth == 7, "UCI changes IIR minimum depth"); + Tuning::printUciOptions(out); + Tuning::printCurrent(out); + expect(out.str().find("option name SearchIIRMinDepth type spin default 6 min 2 max 32") + != std::string::npos, "min-depth UCI option advertised"); + expect(out.str().find("option name SearchIIRReduction type spin default 1 min 0 max 4") + != std::string::npos, "reduction UCI option advertised"); + expect(out.str().find("option name SearchIIRCutNode type spin") == std::string::npos, + "cut switch not advertised"); + expect(out.str().find("option name SearchIIRAllNode type spin") == std::string::npos, + "all switch not advertised"); + expect(out.str().find("SearchIIRMinDepth=7") != std::string::npos, + "tuneparams reports active depth"); + + Tuning::resetSearch(); + expect(Tuning::search().iirMinDepth == defaults.iirMinDepth + && Tuning::search().iirReduction == defaults.iirReduction, + "reset restores both IIR defaults"); + + if (!failures) std::cout << "Internal iterative reduction tuning: PASS\n"; + return failures ? 1 : 0; +} diff --git a/tests/mate_stop.cpp b/tests/mate_stop.cpp index 86e1b32..afcc79b 100644 --- a/tests/mate_stop.cpp +++ b/tests/mate_stop.cpp @@ -85,6 +85,8 @@ void testRootMate(bool multiPV) { expect(completed, multiPV ? "MultiPV returned promptly on mate" : "root search returned promptly on mate"); expect(text.find("score mate 1") != std::string::npos, "engine reports winning mate in one"); expect(text.find("bestmove d7a7") != std::string::npos, "engine returns king-capturing move"); + expect(text.find("info string iir reductions ") == std::string::npos, + "search does not emit IIR diagnostic reporting"); expect(text.find("info depth 2 ") == std::string::npos, "no further deepening after winning mate"); if (multiPV) { expect(text.find("multipv 1 ") != std::string::npos, "MultiPV top line emitted");