Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions docs/internal-iterative-reduction.md
Original file line number Diff line number Diff line change
@@ -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`.
20 changes: 20 additions & 0 deletions include/ironphoenix/iir.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include "tuning.hpp"

#include <algorithm>

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
3 changes: 2 additions & 1 deletion include/ironphoenix/search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 20 additions & 17 deletions include/ironphoenix/tuning.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 10 additions & 1 deletion src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "ironphoenix/eval.hpp"
#include "ironphoenix/lmr.hpp"
#include "ironphoenix/iir.hpp"
#include "ironphoenix/tuning.hpp"

#include "ironphoenix/movegen.hpp"
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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<bool>(ttMove), tuning);
if (iir > 0)
depth -= iir;

MoveList moves;
generatePseudoLegalMoves(pos, moves);

Expand Down
57 changes: 34 additions & 23 deletions src/tuning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
117 changes: 117 additions & 0 deletions tests/iir.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#include "ironphoenix/geometry.hpp"
#include "ironphoenix/iir.hpp"
#include "ironphoenix/search.hpp"
#include "ironphoenix/tuning.hpp"

#include <iostream>
#include <memory>
#include <sstream>
#include <string>

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<SearchEngine>();
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;
}
Loading
Loading