#define DROP7_FAIR_ONLY_DEPTH4_LIBRARY
#include "../../fair-expectimax/reference/fair-only-depth4.cpp"
#define DROP7_ORACLE_TOPOLOGY_LIBRARY
#include "oracle-topology-audit.cpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <list>
#include <map>
#include <mutex>
#include <numeric>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/resource.h>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>
// A leakage-controlled test of whether the observable topology of the
// privileged oracle can provide a useful residual for fair-only depth four.
// Oracle access is confined to the 0x3d fitting families. The learned model
// receives only a reflection-canonical board; nuisance variables are used to
// balance examples but never enter its input. Fresh gameplay is sealed behind
// both a prediction gate and a public-state policy-diagnostic gate.
namespace drop7::oracle_topology_residual {
namespace fair = drop7::fair_only_depth4;
namespace frozen = drop7::fair_only_horizon;
namespace oracle = drop7::oracle_topology;
using Clock = std::chrono::steady_clock;
constexpr std::uint32_t kTrainingSeedStart = 0x3d9c'0000u;
constexpr std::uint32_t kHeldoutSeedStart = 0x3d9d'0000u;
constexpr std::uint32_t kScreenSeedStart = 0x3ea9'0000u;
constexpr std::uint32_t kConfirmationSeedStart = 0x3eaa'0000u;
constexpr int kTrainingGames = 16;
constexpr int kHeldoutGames = 8;
constexpr int kTrainingMaximumMoves = 160;
constexpr int kGameplayMaximumMoves = 1'000;
constexpr int kScreenGames = 8;
constexpr int kConfirmationGames = 16;
constexpr int kOracleDepth = 4;
constexpr int kOracleBeam = 128;
constexpr int kParallelism = 4;
constexpr int kMoveBandWidth = 20;
constexpr int kMaximumPairsPerStratum = 16;
constexpr int kDiagnosticStates = 24;
constexpr int kCellKinds = 10;
constexpr int kInputCount = kCellCount * kCellKinds;
constexpr int kHidden = 8;
constexpr int kParameterCount =
kInputCount * kHidden + kHidden + kHidden + 1;
constexpr int kTrainingEpochs = 240;
constexpr int kBatchSize = 64;
constexpr double kLearningRate = 0.004;
constexpr double kL2 = 0.0002;
constexpr std::array<double, 6> kCoefficientGrid{{
250.0, 500.0, 1'000.0, 2'000.0, 4'000.0, 8'000.0,
}};
constexpr double kMinimumHeldoutAuc = 0.58;
constexpr double kMinimumHeldoutPairAccuracy = 0.56;
constexpr double kMinimumHalfPairAccuracy = 0.53;
constexpr double kMinimumDiagnosticSwitchRate = 0.04;
constexpr double kMaximumDiagnosticSwitchRate = 0.35;
static_assert(kLevelBonus == 7'000);
static_assert(kParameterCount == 3'937);
static_assert(kParameterCount < 4'000);
static_assert(kTrainingSeedStart + kTrainingGames < kHeldoutSeedStart);
static_assert(kHeldoutSeedStart + kHeldoutGames < 0x3e00'0000u);
static_assert(kScreenSeedStart + kScreenGames < kConfirmationSeedStart);
static_assert(kConfirmationSeedStart + kConfirmationGames < 0x3eab'0000u);
static_assert(fair::kCandidateDepth == 4);
static_assert(fair::kMaximumWork == 3'200'000);
static_assert(fair::kMaximumCacheEntries == 60'000);
std::mutex progress_mutex;
std::uint64_t peakRssBytes() {
rusage usage{};
if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
#if defined(__APPLE__)
return static_cast<std::uint64_t>(usage.ru_maxrss);
#else
return static_cast<std::uint64_t>(usage.ru_maxrss) * 1024u;
#endif
}
struct Stratum {
int move_band = 0;
int rise_phase = 0;
int occupied = 0;
int maximum_height = 0;
auto operator<=>(const Stratum&) const = default;
};
enum class Label : std::uint8_t { kFair = 0, kOracle = 1 };
struct RawRecord {
std::uint32_t seed = 0;
Stratum stratum{};
Board board{};
Label label = Label::kFair;
};
// This is the complete optimizer-facing example type. It intentionally has
// no seed, score, level, move index, history, next disc, or random-tape field.
struct LearningExample {
std::array<std::uint16_t, kCellCount> active{};
double label = 0.0;
};
static_assert(std::tuple_size_v<decltype(LearningExample::active)> ==
kCellCount);
struct MatchedPair {
LearningExample fair{};
LearningExample oracle{};
std::uint32_t diagnostic_seed = 0;
Stratum diagnostic_stratum{};
};
struct MatchedDataset {
std::vector<LearningExample> examples;
std::vector<MatchedPair> pairs;
int raw_fair = 0;
int raw_oracle = 0;
int strata = 0;
};
struct PublicDiagnosticState {
State state{};
Stratum stratum{};
};
// Independent training-only behavior-clone material. It is serialized after
// collection but never read by matching, fitting, prediction, or policy gates.
struct BehaviorLabel {
Board board{};
std::uint8_t next_disc = 1;
int moves_remaining = kMovesPerLevel;
int action = -1;
std::array<double, kBoardSize> root_values{};
};
struct CollectedSplit {
std::vector<RawRecord> fair;
std::vector<RawRecord> oracle;
std::vector<PublicDiagnosticState> fair_diagnostics;
std::vector<BehaviorLabel> behavior_labels;
double wall_seconds = 0.0;
};
int occupiedCells(const Board& board) {
return static_cast<int>(std::count_if(
board.begin(), board.end(), [](std::uint8_t cell) {
return cell != kEmpty;
}));
}
int maximumHeight(const Board& board) {
int result = 0;
for (const int height : cfpi::detail::columnHeights(board)) {
result = std::max(result, height);
}
return result;
}
Stratum stratumFor(const State& state) {
return {state.moves_played / kMoveBandWidth, state.moves_remaining,
occupiedCells(state.board), maximumHeight(state.board)};
}
State publicState(const State& source) {
State result;
result.board = source.board;
result.next_disc = source.next_disc;
result.moves_remaining = source.moves_remaining;
result.score = 0;
result.level = 1;
result.moves_played = 0;
result.game_over = false;
return result;
}
Board canonicalBoard(const Board& board) {
const Board reflected = cfpi::detail::mirrorBoard(board);
return std::lexicographical_compare(reflected.begin(), reflected.end(),
board.begin(), board.end())
? reflected
: board;
}
LearningExample makeLearningExample(const Board& source, Label label) {
const Board board = canonicalBoard(source);
LearningExample result;
result.label = label == Label::kOracle ? 1.0 : 0.0;
for (int cell = 0; cell < kCellCount; ++cell) {
const int kind = static_cast<int>(board[cell]);
if (kind < 0 || kind >= kCellKinds) {
throw std::logic_error("invalid Drop7 cell in NNUE input");
}
result.active[cell] = static_cast<std::uint16_t>(
cell * kCellKinds + kind);
}
return result;
}
RawRecord makeRawRecord(std::uint32_t seed, Label label,
const State& state) {
return {seed, stratumFor(state), state.board, label};
}
BehaviorLabel makeBehaviorLabel(const State& source,
const fair::SearchDecision& decision) {
const bool mirrored =
cfpi::detail::mirroredRepresentationIsSmaller(source.board);
BehaviorLabel result;
result.board = mirrored ? cfpi::detail::mirrorBoard(source.board)
: source.board;
result.next_disc = source.next_disc;
result.moves_remaining = source.moves_remaining;
result.action = mirrored ? kBoardSize - 1 - decision.action
: decision.action;
for (int canonical_column = 0; canonical_column < kBoardSize;
++canonical_column) {
const int source_column = mirrored
? kBoardSize - 1 - canonical_column
: canonical_column;
result.root_values[canonical_column] =
decision.root_values[source_column];
}
return result;
}
void reportCollection(std::string_view split, std::string_view policy,
std::uint32_t seed, int moves, std::int64_t score) {
const std::lock_guard<std::mutex> lock(progress_mutex);
std::cerr << "topology-residual " << split << '-' << policy << " seed 0x"
<< std::hex << seed << std::dec << ' ' << score << " (" << moves
<< " moves)\n";
}
std::vector<RawRecord> collectFairGame(std::uint32_t seed, int maximum_moves,
std::string_view split,
std::vector<PublicDiagnosticState>*
diagnostics,
std::vector<BehaviorLabel>* labels) {
State state = initialHeadlessState(seed);
std::vector<RawRecord> records;
records.reserve(maximum_moves);
while (!state.game_over && state.moves_played < maximum_moves) {
records.push_back(makeRawRecord(seed, Label::kFair, state));
if (diagnostics != nullptr) {
diagnostics->push_back({publicState(state), stratumFor(state)});
}
const fair::SearchDecision decision = fair::chooseDepth4Action(state);
if (!decision.complete || decision.completed_depth != 4 ||
!isLegal(state.board, decision.action)) {
throw std::runtime_error("fair D4 collection decision failed");
}
if (labels != nullptr) labels->push_back(makeBehaviorLabel(state, decision));
MoveResult move;
if (!playHeadlessMove(state, seed, decision.action, move)) {
throw std::runtime_error("fair D4 collection transition failed");
}
}
reportCollection(split, "fair-d4", seed, state.moves_played, state.score);
return records;
}
std::vector<RawRecord> collectOracleGame(std::uint32_t seed,
int maximum_moves,
std::string_view split) {
State state = initialHeadlessState(seed);
std::vector<RawRecord> records;
records.reserve(maximum_moves);
while (!state.game_over && state.moves_played < maximum_moves) {
records.push_back(makeRawRecord(seed, Label::kOracle, state));
const oracle::OraclePlan plan =
oracle::planOracleMove(state, seed, kOracleDepth, kOracleBeam);
if (!isLegal(state.board, plan.column)) {
throw std::runtime_error("oracle collection chose an illegal action");
}
MoveResult move;
if (!playHeadlessMove(state, seed, plan.column, move)) {
throw std::runtime_error("oracle collection transition failed");
}
}
reportCollection(split, "oracle", seed, state.moves_played, state.score);
return records;
}
CollectedSplit collectSplit(std::uint32_t seed_start, int games,
std::string_view split) {
const auto started = Clock::now();
std::vector<std::vector<RawRecord>> fair_by_game(games);
std::vector<std::vector<RawRecord>> oracle_by_game(games);
std::vector<std::vector<PublicDiagnosticState>> diagnostics_by_game(games);
std::vector<std::vector<BehaviorLabel>> labels_by_game(games);
std::atomic<int> next_game{0};
std::vector<std::future<void>> workers;
for (int worker = 0; worker < std::min(kParallelism, games); ++worker) {
workers.push_back(std::async(std::launch::async, [&] {
for (;;) {
const int game = next_game.fetch_add(1);
if (game >= games) return;
const std::uint32_t seed =
seed_start + static_cast<std::uint32_t>(game);
fair_by_game[game] = collectFairGame(
seed, kTrainingMaximumMoves, split, &diagnostics_by_game[game],
&labels_by_game[game]);
oracle_by_game[game] =
collectOracleGame(seed, kTrainingMaximumMoves, split);
}
}));
}
for (auto& worker : workers) worker.get();
CollectedSplit result;
for (int game = 0; game < games; ++game) {
result.fair.insert(result.fair.end(), fair_by_game[game].begin(),
fair_by_game[game].end());
result.oracle.insert(result.oracle.end(), oracle_by_game[game].begin(),
oracle_by_game[game].end());
result.fair_diagnostics.insert(result.fair_diagnostics.end(),
diagnostics_by_game[game].begin(),
diagnostics_by_game[game].end());
result.behavior_labels.insert(result.behavior_labels.end(),
labels_by_game[game].begin(),
labels_by_game[game].end());
}
result.wall_seconds =
std::chrono::duration<double>(Clock::now() - started).count();
return result;
}
std::uint32_t stratumHash(const Stratum& value) {
std::uint32_t hash = 0x811c'9dc5u;
for (const int item : {value.move_band, value.rise_phase, value.occupied,
value.maximum_height}) {
hash ^= static_cast<std::uint32_t>(item + 1);
hash *= 0x0100'0193u;
}
return mix32(hash);
}
template <typename Value>
void deterministicShuffle(std::vector<Value>& values, std::uint32_t seed) {
for (std::size_t remaining = values.size(); remaining > 1; --remaining) {
seed = mix32(seed + static_cast<std::uint32_t>(remaining));
const std::size_t selected = seed % remaining;
std::swap(values[remaining - 1], values[selected]);
}
}
MatchedDataset matchRecords(const std::vector<RawRecord>& fair_records,
const std::vector<RawRecord>& oracle_records) {
std::map<Stratum, std::vector<const RawRecord*>> fair;
std::map<Stratum, std::vector<const RawRecord*>> privileged;
for (const RawRecord& record : fair_records) fair[record.stratum].push_back(&record);
for (const RawRecord& record : oracle_records) {
privileged[record.stratum].push_back(&record);
}
MatchedDataset result;
result.raw_fair = static_cast<int>(fair_records.size());
result.raw_oracle = static_cast<int>(oracle_records.size());
for (auto& [stratum, fair_values] : fair) {
const auto found = privileged.find(stratum);
if (found == privileged.end()) continue;
std::vector<const RawRecord*> oracle_values = found->second;
deterministicShuffle(fair_values, stratumHash(stratum) ^ 0xfa17'0001u);
deterministicShuffle(oracle_values,
stratumHash(stratum) ^ 0x0ace'0002u);
const std::size_t count = std::min<std::size_t>(
{fair_values.size(), oracle_values.size(),
static_cast<std::size_t>(kMaximumPairsPerStratum)});
if (count == 0) continue;
++result.strata;
for (std::size_t index = 0; index < count; ++index) {
MatchedPair pair;
pair.fair = makeLearningExample(fair_values[index]->board, Label::kFair);
pair.oracle =
makeLearningExample(oracle_values[index]->board, Label::kOracle);
pair.diagnostic_seed = fair_values[index]->seed;
pair.diagnostic_stratum = stratum;
result.examples.push_back(pair.fair);
result.examples.push_back(pair.oracle);
result.pairs.push_back(std::move(pair));
}
}
deterministicShuffle(result.examples, 0x71a1'5eedu);
return result;
}
struct NnueModel {
std::array<double, kInputCount * kHidden> input{};
std::array<double, kHidden> hidden_bias{};
std::array<double, kHidden> output{};
double output_bias = 0.0;
};
struct Forward {
std::array<double, kHidden> preactivation{};
std::array<double, kHidden> hidden{};
double logit = 0.0;
};
Forward forward(const NnueModel& model, const LearningExample& example) {
Forward result;
result.preactivation = model.hidden_bias;
for (const std::uint16_t active : example.active) {
const std::size_t base = static_cast<std::size_t>(active) * kHidden;
for (int hidden = 0; hidden < kHidden; ++hidden) {
result.preactivation[hidden] += model.input[base + hidden];
}
}
result.logit = model.output_bias;
for (int hidden = 0; hidden < kHidden; ++hidden) {
result.hidden[hidden] = std::max(0.0, result.preactivation[hidden]);
result.logit += model.output[hidden] * result.hidden[hidden];
}
return result;
}
double boardLogit(const NnueModel& model, const Board& board) {
return forward(model, makeLearningExample(board, Label::kFair)).logit;
}
double sigmoid(double value) {
if (value >= 0.0) {
const double inverse = std::exp(-value);
return 1.0 / (1.0 + inverse);
}
const double exponential = std::exp(value);
return exponential / (1.0 + exponential);
}
struct PackedParameters {
std::array<double, kParameterCount> values{};
};
PackedParameters pack(const NnueModel& model) {
PackedParameters result;
std::size_t cursor = 0;
for (double value : model.input) result.values[cursor++] = value;
for (double value : model.hidden_bias) result.values[cursor++] = value;
for (double value : model.output) result.values[cursor++] = value;
result.values[cursor++] = model.output_bias;
if (cursor != result.values.size()) throw std::logic_error("NNUE pack failed");
return result;
}
NnueModel unpack(const PackedParameters& packed) {
NnueModel result;
std::size_t cursor = 0;
for (double& value : result.input) value = packed.values[cursor++];
for (double& value : result.hidden_bias) value = packed.values[cursor++];
for (double& value : result.output) value = packed.values[cursor++];
result.output_bias = packed.values[cursor++];
if (cursor != packed.values.size()) throw std::logic_error("NNUE unpack failed");
return result;
}
NnueModel initializedModel() {
NnueModel result;
std::uint32_t random = 0x4e4e'5545u;
for (double& value : result.input) {
random = mix32(random + 0x9e37'79b9u);
value = (static_cast<double>(random) / 4'294'967'296.0 - 0.5) * 0.02;
}
for (double& value : result.output) {
random = mix32(random + 0x9e37'79b9u);
value = (static_cast<double>(random) / 4'294'967'296.0 - 0.5) * 0.04;
}
return result;
}
struct TrainingResult {
NnueModel model{};
double initial_loss = 0.0;
double final_loss = 0.0;
};
double datasetLoss(const NnueModel& model,
const std::vector<LearningExample>& examples) {
if (examples.empty()) throw std::invalid_argument("empty NNUE dataset");
double result = 0.0;
for (const LearningExample& example : examples) {
const double logit = forward(model, example).logit;
result += std::max(logit, 0.0) - logit * example.label +
std::log1p(std::exp(-std::abs(logit)));
}
return result / examples.size();
}
TrainingResult trainModel(const std::vector<LearningExample>& source) {
if (source.size() < 64) {
throw std::invalid_argument("too few matched NNUE examples");
}
std::vector<LearningExample> examples = source;
NnueModel model = initializedModel();
TrainingResult result;
result.initial_loss = datasetLoss(model, examples);
PackedParameters packed = pack(model);
PackedParameters first_moment;
PackedParameters second_moment;
std::uint64_t step = 0;
for (int epoch = 0; epoch < kTrainingEpochs; ++epoch) {
deterministicShuffle(examples, 0xada0'0000u + static_cast<std::uint32_t>(epoch));
for (std::size_t begin = 0; begin < examples.size(); begin += kBatchSize) {
const std::size_t end = std::min(examples.size(), begin + kBatchSize);
PackedParameters gradient;
model = unpack(packed);
for (std::size_t index = begin; index < end; ++index) {
const LearningExample& example = examples[index];
const Forward pass = forward(model, example);
const double error = sigmoid(pass.logit) - example.label;
const std::size_t output_offset = kInputCount * kHidden + kHidden;
for (int hidden = 0; hidden < kHidden; ++hidden) {
gradient.values[output_offset + hidden] +=
error * pass.hidden[hidden];
if (pass.preactivation[hidden] <= 0.0) continue;
const double hidden_error = error * model.output[hidden];
gradient.values[kInputCount * kHidden + hidden] += hidden_error;
for (const std::uint16_t active : example.active) {
gradient.values[static_cast<std::size_t>(active) * kHidden + hidden] +=
hidden_error;
}
}
gradient.values.back() += error;
}
const double inverse_batch = 1.0 / static_cast<double>(end - begin);
++step;
const double first_correction = 1.0 - std::pow(0.9, static_cast<double>(step));
const double second_correction = 1.0 - std::pow(0.999, static_cast<double>(step));
for (std::size_t parameter = 0; parameter < packed.values.size(); ++parameter) {
double value = gradient.values[parameter] * inverse_batch;
const bool regularized = parameter < kInputCount * kHidden ||
(parameter >= kInputCount * kHidden + kHidden &&
parameter < kParameterCount - 1);
if (regularized) value += kL2 * packed.values[parameter];
first_moment.values[parameter] =
0.9 * first_moment.values[parameter] + 0.1 * value;
second_moment.values[parameter] =
0.999 * second_moment.values[parameter] + 0.001 * value * value;
const double corrected_first =
first_moment.values[parameter] / first_correction;
const double corrected_second =
second_moment.values[parameter] / second_correction;
packed.values[parameter] -=
kLearningRate * corrected_first / (std::sqrt(corrected_second) + 1.0e-8);
}
}
}
result.model = unpack(packed);
result.final_loss = datasetLoss(result.model, source);
return result;
}
struct PredictionMetrics {
int examples = 0;
int pairs = 0;
double loss = 0.0;
double auc = 0.0;
double pair_accuracy = 0.0;
double first_half_accuracy = 0.0;
double second_half_accuracy = 0.0;
};
double pairCredit(double positive, double negative) {
if (positive > negative) return 1.0;
if (positive == negative) return 0.5;
return 0.0;
}
PredictionMetrics predictionMetrics(const NnueModel& model,
const MatchedDataset& dataset,
std::uint32_t split_seed) {
PredictionMetrics result;
result.examples = static_cast<int>(dataset.examples.size());
result.pairs = static_cast<int>(dataset.pairs.size());
result.loss = datasetLoss(model, dataset.examples);
std::vector<double> positive;
std::vector<double> negative;
for (const LearningExample& example : dataset.examples) {
const double score = forward(model, example).logit;
(example.label > 0.5 ? positive : negative).push_back(score);
}
double auc_credit = 0.0;
for (const double oracle_score : positive) {
for (const double fair_score : negative) {
auc_credit += pairCredit(oracle_score, fair_score);
}
}
result.auc = auc_credit /
static_cast<double>(positive.size() * negative.size());
double all_credit = 0.0;
double first_credit = 0.0;
double second_credit = 0.0;
int first_count = 0;
int second_count = 0;
for (const MatchedPair& pair : dataset.pairs) {
const double credit = pairCredit(forward(model, pair.oracle).logit,
forward(model, pair.fair).logit);
all_credit += credit;
if (pair.diagnostic_seed < split_seed) {
first_credit += credit;
++first_count;
} else {
second_credit += credit;
++second_count;
}
}
result.pair_accuracy = all_credit / std::max(1, result.pairs);
result.first_half_accuracy = first_credit / std::max(1, first_count);
result.second_half_accuracy = second_credit / std::max(1, second_count);
return result;
}
std::uint64_t modelFingerprint(const NnueModel& model) {
const PackedParameters packed = pack(model);
std::uint64_t hash = 0xcbf2'9ce4'8422'2325ull;
for (const double value : packed.values) {
std::uint64_t bits = std::bit_cast<std::uint64_t>(value);
for (int byte = 0; byte < 8; ++byte) {
hash ^= bits & 0xffu;
hash *= 0x0000'0100'0000'01b3ull;
bits >>= 8;
}
}
return hash;
}
namespace residual_search {
class WorkLimitReached : public std::exception {};
struct CacheEntry {
double value = 0.0;
std::list<std::string>::iterator order;
};
struct Context {
Context(const NnueModel& source_model, double source_coefficient)
: model(source_model), coefficient(source_coefficient) {}
const NnueModel& model;
double coefficient = 0.0;
std::unordered_map<std::string, CacheEntry> cache;
std::list<std::string> order;
std::uint64_t nodes = 0;
std::uint64_t work = 0;
std::uint64_t cache_hits = 0;
};
void checkBudget(const Context& context) {
if (context.work >= fair::kMaximumWork) throw WorkLimitReached{};
}
void cacheValue(Context& context, std::string key, double value) {
const auto prior = context.cache.find(key);
if (prior != context.cache.end()) {
context.order.erase(prior->second.order);
context.cache.erase(prior);
}
while (context.cache.size() >= fair::kMaximumCacheEntries) {
const std::string& oldest = context.order.front();
context.cache.erase(oldest);
context.order.pop_front();
}
context.order.push_back(key);
const auto order = std::prev(context.order.end());
context.cache.emplace(std::move(key), CacheEntry{value, order});
}
double bestFutureValue(const State& state, int depth, Context& context);
struct ActionValue {
double value = 0.0;
double expected_score = 0.0;
};
ActionValue evaluateAction(const State& state, int column, int depth,
Context& context) {
const std::uint32_t state_seed = cfpi::detail::scenarioSeedForState(
state, frozen::kPolicySeed, depth);
ActionValue result;
for (int sample = 0; sample < fair::kChanceSamples; ++sample) {
checkBudget(context);
cfpi::detail::StratifiedRandom random{
state_seed, sample, fair::kChanceSamples, 0};
MoveResult move;
const bool played =
cfpi::detail::playMoveSampled(state, column, random, move);
++context.work;
if (!played) {
result.value += frozen::kTerminalUtility;
continue;
}
const double score_delta = static_cast<double>(move.score_delta);
result.expected_score += score_delta;
if (move.state.game_over) {
result.value += score_delta + frozen::kTerminalUtility;
continue;
}
move.state.score = 0;
move.state.next_disc = cfpi::detail::sampledNextDisc(
state_seed, sample, fair::kChanceSamples);
bool ignored = false;
const State next = cfpi::detail::canonicalState(move.state, ignored);
result.value +=
score_delta + bestFutureValue(next, depth - 1, context);
}
result.value /= fair::kChanceSamples;
result.expected_score /= fair::kChanceSamples;
return result;
}
double evaluateLeaf(const State& state, Context& context) {
checkBudget(context);
++context.work;
const double value = frozen::fairLeaf(state) +
context.coefficient * boardLogit(context.model,
state.board);
if (!std::isfinite(value)) {
throw std::runtime_error("topology residual leaf is non-finite");
}
return value;
}
double bestFutureValue(const State& state, int depth, Context& context) {
++context.nodes;
checkBudget(context);
if (state.game_over) return frozen::kTerminalUtility;
if (depth == 0) return evaluateLeaf(state, context);
const std::string key = cfpi::detail::dynamicStateKey(state, depth);
const auto cached = context.cache.find(key);
if (cached != context.cache.end()) {
++context.cache_hits;
const double value = cached->second.value;
context.order.splice(context.order.end(), context.order,
cached->second.order);
return value;
}
double best = -std::numeric_limits<double>::infinity();
for (const int column : cfpi::detail::kColumnOrder) {
if (!isLegal(state.board, column)) continue;
best = std::max(best, evaluateAction(state, column, depth, context).value);
}
if (!std::isfinite(best)) best = frozen::kTerminalUtility;
cacheValue(context, key, best);
return best;
}
struct RootEvaluation {
int action = -1;
double value = -std::numeric_limits<double>::infinity();
std::array<double, kBoardSize> values{};
};
RootEvaluation rootDecision(const State& state, int depth, Context& context) {
RootEvaluation result;
result.values.fill(-std::numeric_limits<double>::infinity());
for (const int column : cfpi::detail::kColumnOrder) {
if (!isLegal(state.board, column)) continue;
const ActionValue candidate = evaluateAction(state, column, depth, context);
result.values[column] = candidate.value;
if (candidate.value > result.value) {
result.value = candidate.value;
result.action = column;
}
}
return result;
}
struct Decision {
int action = -1;
int completed_depth = 0;
bool complete = false;
std::uint64_t work = 0;
std::uint64_t nodes = 0;
std::uint64_t cache_hits = 0;
std::size_t cache_entries = 0;
std::array<double, kBoardSize> root_values{};
};
Decision chooseAction(const State& source, const NnueModel& model,
double coefficient) {
if (source.game_over) return {};
bool mirrored = false;
const State state = cfpi::detail::canonicalState(source, mirrored);
Context context{model, coefficient};
RootEvaluation completed;
int completed_depth = 0;
for (int depth = 1; depth <= fair::kCandidateDepth; ++depth) {
try {
completed = rootDecision(state, depth, context);
if (completed.action < 0) break;
completed_depth = depth;
} catch (const WorkLimitReached&) {
break;
}
}
int action = completed.action;
if (action < 0) action = centerFirstMove(state.board);
Decision result;
result.action = mirrored ? kBoardSize - 1 - action : action;
result.completed_depth = completed_depth;
result.complete = completed_depth == fair::kCandidateDepth;
result.work = context.work;
result.nodes = context.nodes;
result.cache_hits = context.cache_hits;
result.cache_entries = context.cache.size();
result.root_values.fill(-std::numeric_limits<double>::infinity());
if (completed_depth > 0) {
for (int canonical_column = 0; canonical_column < kBoardSize;
++canonical_column) {
const int source_column = mirrored
? kBoardSize - 1 - canonical_column
: canonical_column;
result.root_values[source_column] = completed.values[canonical_column];
}
}
return result;
}
} // namespace residual_search
struct CoefficientDiagnostic {
double coefficient = 0.0;
int states = 0;
int switches = 0;
double switch_rate = 0.0;
bool all_complete = true;
bool all_legal = true;
std::uint64_t work = 0;
};
std::vector<State> selectDiagnosticStates(
const std::vector<PublicDiagnosticState>& source) {
if (source.empty()) return {};
std::map<std::pair<int, int>, std::vector<State>> by_phase;
for (const PublicDiagnosticState& item : source) {
by_phase[{item.stratum.move_band, item.stratum.rise_phase}].push_back(
item.state);
}
std::vector<State> result;
std::size_t round = 0;
while (result.size() < kDiagnosticStates) {
bool added = false;
for (const auto& [key, values] : by_phase) {
static_cast<void>(key);
if (round >= values.size()) continue;
result.push_back(values[round]);
added = true;
if (result.size() == kDiagnosticStates) break;
}
if (!added) break;
++round;
}
return result;
}
struct PolicyDiagnostic {
int states = 0;
std::vector<CoefficientDiagnostic> grid;
std::optional<double> selected_coefficient;
bool passed = false;
};
PolicyDiagnostic diagnosePolicy(const NnueModel& model,
const std::vector<State>& states) {
PolicyDiagnostic result;
result.states = static_cast<int>(states.size());
if (states.size() < static_cast<std::size_t>(kDiagnosticStates)) return result;
std::vector<fair::SearchDecision> baseline;
baseline.reserve(states.size());
for (const State& state : states) {
baseline.push_back(fair::chooseDepth4Action(state));
}
for (const double coefficient : kCoefficientGrid) {
CoefficientDiagnostic item;
item.coefficient = coefficient;
item.states = static_cast<int>(states.size());
for (std::size_t index = 0; index < states.size(); ++index) {
const residual_search::Decision candidate =
residual_search::chooseAction(states[index], model, coefficient);
item.all_complete = item.all_complete && candidate.complete &&
candidate.completed_depth == 4;
item.all_legal = item.all_legal &&
isLegal(states[index].board, candidate.action);
item.switches += candidate.action != baseline[index].action;
item.work += candidate.work;
}
item.switch_rate =
static_cast<double>(item.switches) / std::max(1, item.states);
result.grid.push_back(item);
}
double best_distance = std::numeric_limits<double>::infinity();
for (const CoefficientDiagnostic& item : result.grid) {
if (!item.all_complete || !item.all_legal ||
item.switch_rate < kMinimumDiagnosticSwitchRate ||
item.switch_rate > kMaximumDiagnosticSwitchRate) {
continue;
}
const double distance = std::abs(item.switch_rate - 0.12);
if (distance < best_distance) {
best_distance = distance;
result.selected_coefficient = item.coefficient;
}
}
result.passed = result.selected_coefficient.has_value();
return result;
}
struct GameResult {
std::uint32_t seed = 0;
std::int64_t score = 0;
int moves = 0;
bool censored = false;
std::uint64_t cleared = 0;
std::uint64_t revealed = 0;
int maximum_chain = 0;
std::uint64_t work = 0;
std::size_t peak_cache_entries = 0;
double elapsed_seconds = 0.0;
};
void observeMove(const MoveResult& move, GameResult& result) {
result.maximum_chain =
std::max(result.maximum_chain, static_cast<int>(move.waves.size()));
for (const Wave& wave : move.waves) {
result.cleared += wave.cleared;
result.revealed += wave.revealed;
}
}
void reportGame(std::string_view phase, std::string_view policy,
const GameResult& result) {
const std::lock_guard<std::mutex> lock(progress_mutex);
std::cerr << "topology-residual " << phase << '-' << policy << " seed 0x"
<< std::hex << result.seed << std::dec << ' ' << result.score
<< " (" << result.moves << " moves"
<< (result.censored ? ", capped" : "") << ")\n";
}
GameResult runBaselineGame(std::uint32_t seed, std::string_view phase) {
const auto started = Clock::now();
State state = initialHeadlessState(seed);
GameResult result;
result.seed = seed;
while (!state.game_over && state.moves_played < kGameplayMaximumMoves) {
const fair::SearchDecision decision = fair::chooseDepth4Action(state);
if (!decision.complete || decision.completed_depth != 4 ||
!isLegal(state.board, decision.action)) {
throw std::runtime_error("gameplay fair D4 decision failed");
}
result.work += decision.work;
result.peak_cache_entries =
std::max(result.peak_cache_entries, decision.cache_entries);
MoveResult move;
if (!playHeadlessMove(state, seed, decision.action, move)) {
throw std::runtime_error("gameplay fair D4 transition failed");
}
observeMove(move, result);
}
result.score = state.score;
result.moves = state.moves_played;
result.censored = !state.game_over;
result.elapsed_seconds =
std::chrono::duration<double>(Clock::now() - started).count();
reportGame(phase, "fair-d4", result);
return result;
}
GameResult runResidualGame(std::uint32_t seed, const NnueModel& model,
double coefficient, std::string_view phase) {
const auto started = Clock::now();
State state = initialHeadlessState(seed);
GameResult result;
result.seed = seed;
while (!state.game_over && state.moves_played < kGameplayMaximumMoves) {
const residual_search::Decision decision =
residual_search::chooseAction(state, model, coefficient);
if (!decision.complete || decision.completed_depth != 4 ||
!isLegal(state.board, decision.action)) {
throw std::runtime_error("gameplay residual D4 decision failed");
}
result.work += decision.work;
result.peak_cache_entries =
std::max(result.peak_cache_entries, decision.cache_entries);
MoveResult move;
if (!playHeadlessMove(state, seed, decision.action, move)) {
throw std::runtime_error("gameplay residual D4 transition failed");
}
observeMove(move, result);
}
result.score = state.score;
result.moves = state.moves_played;
result.censored = !state.game_over;
result.elapsed_seconds =
std::chrono::duration<double>(Clock::now() - started).count();
reportGame(phase, "residual-d4", result);
return result;
}
struct GameplayCohort {
std::vector<GameResult> baseline;
std::vector<GameResult> candidate;
double wall_seconds = 0.0;
};
GameplayCohort runGameplayCohort(std::uint32_t seed_start, int games,
const NnueModel& model, double coefficient,
std::string_view phase) {
const auto started = Clock::now();
GameplayCohort result;
result.baseline.resize(games);
result.candidate.resize(games);
std::atomic<int> next_game{0};
std::vector<std::future<void>> workers;
for (int worker = 0; worker < std::min(kParallelism, games); ++worker) {
workers.push_back(std::async(std::launch::async, [&] {
for (;;) {
const int game = next_game.fetch_add(1);
if (game >= games) return;
const std::uint32_t seed =
seed_start + static_cast<std::uint32_t>(game);
result.baseline[game] = runBaselineGame(seed, phase);
result.candidate[game] =
runResidualGame(seed, model, coefficient, phase);
}
}));
}
for (auto& worker : workers) worker.get();
result.wall_seconds =
std::chrono::duration<double>(Clock::now() - started).count();
return result;
}
struct GameSummary {
int games = 0;
double mean_score = 0.0;
double mean_moves = 0.0;
int censored = 0;
double clears_per_move = 0.0;
double reveals_per_move = 0.0;
double mean_maximum_chain = 0.0;
double work_per_move = 0.0;
double moves_per_second = 0.0;
std::size_t peak_cache_entries = 0;
};
GameSummary summarizeGames(const std::vector<GameResult>& games) {
if (games.empty()) throw std::invalid_argument("empty gameplay cohort");
GameSummary result;
result.games = static_cast<int>(games.size());
std::uint64_t moves = 0;
std::uint64_t cleared = 0;
std::uint64_t revealed = 0;
std::uint64_t work = 0;
double seconds = 0.0;
for (const GameResult& game : games) {
result.mean_score += static_cast<double>(game.score) / games.size();
result.mean_moves += static_cast<double>(game.moves) / games.size();
result.censored += game.censored;
result.mean_maximum_chain +=
static_cast<double>(game.maximum_chain) / games.size();
moves += game.moves;
cleared += game.cleared;
revealed += game.revealed;
work += game.work;
seconds += game.elapsed_seconds;
result.peak_cache_entries =
std::max(result.peak_cache_entries, game.peak_cache_entries);
}
const double move_count = static_cast<double>(std::max<std::uint64_t>(1, moves));
result.clears_per_move = cleared / move_count;
result.reveals_per_move = revealed / move_count;
result.work_per_move = work / move_count;
result.moves_per_second = move_count / std::max(1.0e-9, seconds);
return result;
}
struct Difference {
double mean = 0.0;
double lower_95 = 0.0;
int wins = 0;
int ties = 0;
int losses = 0;
};
Difference difference(const std::vector<double>& values) {
if (values.empty()) throw std::invalid_argument("empty paired difference");
Difference result;
result.mean = std::accumulate(values.begin(), values.end(), 0.0) /
values.size();
double squared = 0.0;
for (const double value : values) {
squared += (value - result.mean) * (value - result.mean);
result.wins += value > 0.0;
result.ties += value == 0.0;
result.losses += value < 0.0;
}
const double deviation = values.size() > 1
? std::sqrt(squared / (values.size() - 1))
: 0.0;
result.lower_95 =
result.mean - 1.96 * deviation / std::sqrt(values.size());
return result;
}
struct PairedGameplay {
Difference score;
Difference moves;
};
PairedGameplay pairedGameplay(const GameplayCohort& cohort) {
if (cohort.baseline.size() != cohort.candidate.size() ||
cohort.baseline.empty()) {
throw std::invalid_argument("invalid paired gameplay cohort");
}
std::vector<double> scores;
std::vector<double> moves;
for (std::size_t index = 0; index < cohort.baseline.size(); ++index) {
scores.push_back(static_cast<double>(cohort.candidate[index].score -
cohort.baseline[index].score));
moves.push_back(static_cast<double>(cohort.candidate[index].moves -
cohort.baseline[index].moves));
}
return {difference(scores), difference(moves)};
}
bool improvesBoth(const GameSummary& baseline, const GameSummary& candidate) {
return candidate.mean_score > baseline.mean_score &&
candidate.mean_moves > baseline.mean_moves;
}
void writePrediction(std::ostream& output, const PredictionMetrics& value) {
output << "{\"examples\":" << value.examples << ",\"pairs\":"
<< value.pairs << ",\"loss\":" << value.loss << ",\"auc\":"
<< value.auc << ",\"matchedPairAccuracy\":"
<< value.pair_accuracy << ",\"firstHalfPairAccuracy\":"
<< value.first_half_accuracy << ",\"secondHalfPairAccuracy\":"
<< value.second_half_accuracy << '}';
}
void writeDataset(std::ostream& output, const MatchedDataset& value) {
output << "{\"rawFair\":" << value.raw_fair << ",\"rawOracle\":"
<< value.raw_oracle << ",\"matchedStrata\":" << value.strata
<< ",\"matchedPairs\":" << value.pairs.size()
<< ",\"learningExamples\":" << value.examples.size() << '}';
}
void writePolicyDiagnostic(std::ostream& output,
const PolicyDiagnostic& diagnostic) {
output << "{\"states\":" << diagnostic.states
<< ",\"minimumSwitchRate\":" << kMinimumDiagnosticSwitchRate
<< ",\"maximumSwitchRate\":" << kMaximumDiagnosticSwitchRate
<< ",\"selectedCoefficient\":";
if (diagnostic.selected_coefficient.has_value()) {
output << *diagnostic.selected_coefficient;
} else {
output << "null";
}
output << ",\"passed\":" << (diagnostic.passed ? "true" : "false")
<< ",\"grid\":[";
for (std::size_t index = 0; index < diagnostic.grid.size(); ++index) {
if (index != 0) output << ',';
const CoefficientDiagnostic& item = diagnostic.grid[index];
output << "{\"coefficient\":" << item.coefficient
<< ",\"states\":" << item.states << ",\"switches\":"
<< item.switches << ",\"switchRate\":" << item.switch_rate
<< ",\"allComplete\":"
<< (item.all_complete ? "true" : "false")
<< ",\"allLegal\":" << (item.all_legal ? "true" : "false")
<< ",\"work\":" << item.work << '}';
}
output << "]}";
}
void writeGame(std::ostream& output, const GameResult& game) {
output << "{\"seed\":" << game.seed << ",\"score\":" << game.score
<< ",\"moves\":" << game.moves << ",\"censored\":"
<< (game.censored ? "true" : "false") << ",\"cleared\":"
<< game.cleared << ",\"revealed\":" << game.revealed
<< ",\"maximumChain\":" << game.maximum_chain
<< ",\"work\":" << game.work << ",\"peakCacheEntries\":"
<< game.peak_cache_entries << ",\"elapsedSeconds\":"
<< game.elapsed_seconds << '}';
}
void writeGameSummary(std::ostream& output, const GameSummary& value) {
output << "{\"games\":" << value.games << ",\"meanScore\":"
<< value.mean_score << ",\"meanMoves\":" << value.mean_moves
<< ",\"censored\":" << value.censored
<< ",\"clearsPerMove\":" << value.clears_per_move
<< ",\"revealsPerMove\":" << value.reveals_per_move
<< ",\"meanMaximumChain\":" << value.mean_maximum_chain
<< ",\"workPerMove\":" << value.work_per_move
<< ",\"movesPerSecond\":" << value.moves_per_second
<< ",\"peakCacheEntries\":" << value.peak_cache_entries << '}';
}
void writeDifference(std::ostream& output, const Difference& value) {
output << "{\"mean\":" << value.mean << ",\"lower95\":"
<< value.lower_95 << ",\"wins\":" << value.wins
<< ",\"ties\":" << value.ties << ",\"losses\":"
<< value.losses << '}';
}
void writeGameplay(std::ostream& output, std::uint32_t seed_start,
const GameplayCohort& cohort, const GameSummary& baseline,
const GameSummary& candidate,
const PairedGameplay& paired, bool passed) {
output << "{\"seedStart\":" << seed_start
<< ",\"maximumMoves\":" << kGameplayMaximumMoves
<< ",\"baseline\":";
writeGameSummary(output, baseline);
output << ",\"candidate\":";
writeGameSummary(output, candidate);
output << ",\"paired\":{\"score\":";
writeDifference(output, paired.score);
output << ",\"moves\":";
writeDifference(output, paired.moves);
output << "},\"wallSeconds\":" << cohort.wall_seconds
<< ",\"passed\":" << (passed ? "true" : "false")
<< ",\"pairs\":[";
for (std::size_t index = 0; index < cohort.baseline.size(); ++index) {
if (index != 0) output << ',';
output << "{\"seed\":" << cohort.baseline[index].seed
<< ",\"baseline\":";
writeGame(output, cohort.baseline[index]);
output << ",\"candidate\":";
writeGame(output, cohort.candidate[index]);
output << '}';
}
output << "]}";
}
void writeModel(const std::string& path, const NnueModel& model,
std::uint64_t fingerprint) {
std::ofstream output(path);
if (!output) throw std::runtime_error("could not write topology model");
const PackedParameters packed = pack(model);
output << std::setprecision(17)
<< "{\n \"format\":\"drop7-reflection-nnue-v1\",\n"
<< " \"input\":\"reflection-canonical-board-only\",\n"
<< " \"cellKinds\":10,\n \"cells\":49,\n \"hidden\":"
<< kHidden << ",\n \"parameterCount\":" << kParameterCount
<< ",\n \"fingerprintFnv1a64\":\"0x" << std::hex
<< fingerprint << std::dec << "\",\n \"parameters\":[";
for (std::size_t index = 0; index < packed.values.size(); ++index) {
if (index != 0) output << ',';
output << packed.values[index];
}
output << "]\n}\n";
}
void writeBehaviorLabels(const std::string& path,
const std::vector<BehaviorLabel>& training,
const std::vector<BehaviorLabel>& heldout) {
std::ofstream output(path);
if (!output) throw std::runtime_error("could not write D4 behavior labels");
output << std::setprecision(17)
<< "{\"format\":\"drop7-public-d4-root-labels-v1\","
"\"independentOfResidualExperiment\":true,"
"\"wholeSeedSplit\":true,\"trainingSeedStart\":"
<< kTrainingSeedStart << ",\"trainingGames\":" << kTrainingGames
<< ",\"trainingRecords\":" << training.size()
<< ",\"heldoutSeedStart\":" << kHeldoutSeedStart
<< ",\"heldoutGames\":" << kHeldoutGames
<< ",\"heldoutRecords\":" << heldout.size()
<< ",\"excluded\":[\"gameSeed\",\"score\",\"level\","
"\"moveIndex\",\"history\",\"futureTape\"]}\n";
const auto write_split = [&](std::string_view split,
const std::vector<BehaviorLabel>& labels) {
for (const BehaviorLabel& label : labels) {
output << "{\"split\":\"" << split << "\",\"board\":\"";
for (const std::uint8_t cell : label.board) {
output << static_cast<char>('0' + cell);
}
output << "\",\"nextDisc\":" << static_cast<int>(label.next_disc)
<< ",\"movesRemaining\":" << label.moves_remaining
<< ",\"action\":" << label.action << ",\"rootQ\":[";
for (int column = 0; column < kBoardSize; ++column) {
if (column != 0) output << ',';
if (std::isfinite(label.root_values[column])) {
output << label.root_values[column];
} else {
output << "null";
}
}
output << "]}\n";
}
};
write_split("training", training);
write_split("heldout", heldout);
}
struct Options {
std::string output = "/tmp/drop7-oracle-topology-residual.json";
std::string model = "/tmp/drop7-oracle-topology-residual-model.json";
std::string labels = "/tmp/drop7-d4-public-root-labels.jsonl";
};
Options parseOptions(int argc, char** argv, int begin) {
Options result;
for (int index = begin; index < argc; index += 2) {
if (index + 1 >= argc) {
throw std::invalid_argument("missing topology residual option value");
}
const std::string flag = argv[index];
if (flag == "--output") {
result.output = argv[index + 1];
} else if (flag == "--model") {
result.model = argv[index + 1];
} else if (flag == "--labels") {
result.labels = argv[index + 1];
} else {
throw std::invalid_argument("unknown topology residual option " + flag);
}
}
return result;
}
bool predictionGate(const PredictionMetrics& heldout) {
return heldout.examples >= 200 && heldout.pairs >= 100 &&
heldout.auc >= kMinimumHeldoutAuc &&
heldout.pair_accuracy >= kMinimumHeldoutPairAccuracy &&
heldout.first_half_accuracy >= kMinimumHalfPairAccuracy &&
heldout.second_half_accuracy >= kMinimumHalfPairAccuracy;
}
bool sameDecision(const residual_search::Decision& left,
const residual_search::Decision& right) {
if (left.action != right.action ||
left.completed_depth != right.completed_depth ||
left.complete != right.complete || left.work != right.work ||
left.nodes != right.nodes || left.cache_hits != right.cache_hits ||
left.cache_entries != right.cache_entries) {
return false;
}
for (int column = 0; column < kBoardSize; ++column) {
if (left.root_values[column] != right.root_values[column]) return false;
}
return true;
}
bool selfTest(std::ostream& output) {
const State fixture = frozen::fixtureState(frozen::kTypeScriptFixtures[1]);
const Board reflected_board = cfpi::detail::mirrorBoard(fixture.board);
const LearningExample original =
makeLearningExample(fixture.board, Label::kFair);
const LearningExample reflected =
makeLearningExample(reflected_board, Label::kFair);
const bool reflection_input = original.active == reflected.active;
std::vector<LearningExample> training;
training.reserve(64);
for (int index = 0; index < 64; ++index) {
Board board = fixture.board;
const int cell = index % kCellCount;
if (board[cell] == kEmpty) board[cell] = static_cast<std::uint8_t>(index % 7 + 1);
training.push_back(makeLearningExample(
board, index % 2 == 0 ? Label::kFair : Label::kOracle));
}
const TrainingResult first_training = trainModel(training);
const TrainingResult repeat_training = trainModel(training);
const bool training_deterministic =
modelFingerprint(first_training.model) ==
modelFingerprint(repeat_training.model);
const bool finite_training =
std::isfinite(first_training.initial_loss) &&
std::isfinite(first_training.final_loss) &&
first_training.final_loss < first_training.initial_loss;
const bool reflection_model =
boardLogit(first_training.model, fixture.board) ==
boardLogit(first_training.model, reflected_board);
const fair::SearchDecision baseline = fair::chooseDepth4Action(fixture);
const residual_search::Decision zero = residual_search::chooseAction(
fixture, first_training.model, 0.0);
bool zero_parity = baseline.action == zero.action && baseline.complete &&
zero.complete && baseline.completed_depth == 4 &&
zero.completed_depth == 4;
for (int column = 0; column < kBoardSize; ++column) {
zero_parity = zero_parity &&
baseline.root_values[column] == zero.root_values[column];
}
const residual_search::Decision candidate = residual_search::chooseAction(
fixture, first_training.model, 1'000.0);
const residual_search::Decision repeat = residual_search::chooseAction(
fixture, first_training.model, 1'000.0);
State reflected_state = fixture;
reflected_state.board = reflected_board;
const residual_search::Decision mirror = residual_search::chooseAction(
reflected_state, first_training.model, 1'000.0);
State metadata = fixture;
metadata.score = 987'654'321;
metadata.level = 777;
metadata.moves_played = 999;
const residual_search::Decision metadata_decision =
residual_search::chooseAction(metadata, first_training.model, 1'000.0);
const bool deterministic = sameDecision(candidate, repeat);
const bool legal = candidate.complete &&
isLegal(fixture.board, candidate.action);
const bool reflection_policy =
mirror.complete && mirror.action == kBoardSize - 1 - candidate.action;
const bool metadata_blind = sameDecision(candidate, metadata_decision);
const bool ranges = kTrainingSeedStart >= 0x3d00'0000u &&
kHeldoutSeedStart < 0x3e00'0000u &&
kScreenSeedStart >= 0x3e00'0000u &&
kScreenSeedStart != 0x3e9d'0000u;
const bool resource_bounds = kParameterCount < 4'000 &&
fair::kWorstCaseD4Work < fair::kMaximumWork &&
fair::kWorstCaseD4CacheEntries <
fair::kMaximumCacheEntries;
const bool passed = kLevelBonus == 7'000 && reflection_input &&
training_deterministic && finite_training &&
reflection_model && zero_parity && deterministic &&
legal && reflection_policy && metadata_blind && ranges &&
resource_bounds;
output << "ORACLE_TOPOLOGY_RESIDUAL_SELF_TEST {\"passed\":"
<< (passed ? "true" : "false")
<< ",\"reflectionInput\":"
<< (reflection_input ? "true" : "false")
<< ",\"trainingDeterministic\":"
<< (training_deterministic ? "true" : "false")
<< ",\"finiteTraining\":" << (finite_training ? "true" : "false")
<< ",\"reflectionModel\":"
<< (reflection_model ? "true" : "false")
<< ",\"zeroCoefficientParity\":"
<< (zero_parity ? "true" : "false")
<< ",\"searchDeterministic\":"
<< (deterministic ? "true" : "false")
<< ",\"legal\":" << (legal ? "true" : "false")
<< ",\"reflectionPolicy\":"
<< (reflection_policy ? "true" : "false")
<< ",\"metadataBlind\":"
<< (metadata_blind ? "true" : "false")
<< ",\"seedRangesSealed\":" << (ranges ? "true" : "false")
<< ",\"resourceBounds\":"
<< (resource_bounds ? "true" : "false")
<< ",\"parameterCount\":" << kParameterCount
<< ",\"maximumWork\":" << fair::kMaximumWork
<< ",\"maximumCacheEntries\":" << fair::kMaximumCacheEntries
<< "}\n";
return passed;
}
void writeArtifact(
const Options& options, const CollectedSplit& training_collection,
const CollectedSplit& heldout_collection,
const MatchedDataset& training_data,
const MatchedDataset& heldout_data,
const TrainingResult& training,
const PredictionMetrics& training_prediction,
const PredictionMetrics& heldout_prediction, bool prediction_passed,
std::uint64_t fingerprint, const PolicyDiagnostic& policy,
const GameplayCohort* screen, const GameSummary* screen_baseline,
const GameSummary* screen_candidate, const PairedGameplay* screen_paired,
bool screen_passed, const GameplayCohort* confirmation,
const GameSummary* confirmation_baseline,
const GameSummary* confirmation_candidate,
const PairedGameplay* confirmation_paired, bool confirmation_passed,
double wall_seconds) {
std::ofstream output(options.output);
if (!output) throw std::runtime_error("could not write topology artifact");
output << std::setprecision(10)
<< "{\n \"experiment\":\"oracle-observable-topology-residual\",\n"
<< " \"preregistered\":true,\n"
<< " \"warning\":\"oracle sees the future only while producing fitting labels; model input is board-only\",\n"
<< " \"dataProtocol\":{\"trainingSeedStart\":"
<< kTrainingSeedStart << ",\"trainingGames\":" << kTrainingGames
<< ",\"heldoutSeedStart\":" << kHeldoutSeedStart
<< ",\"heldoutGames\":" << kHeldoutGames
<< ",\"maximumMoves\":" << kTrainingMaximumMoves
<< ",\"oracleDepth\":" << kOracleDepth
<< ",\"oracleBeam\":" << kOracleBeam
<< ",\"wholeSeedSplit\":true,\"matchedOn\":[\"risePhase\",\"exactOccupancy\",\"exactMaximumHeight\",\"twentyMoveBand\"],"
"\"optimizerInputs\":[\"reflectionCanonicalBoardCells\"],"
"\"excludedFromOptimizer\":[\"gameSeed\",\"futureTape\",\"score\",\"level\",\"moveIndex\",\"history\",\"nextDisc\",\"risePhase\"],"
"\"independentBehaviorLabels\":{\"path\":\""
<< options.labels << "\",\"trainingRecords\":"
<< training_collection.behavior_labels.size()
<< ",\"heldoutRecords\":"
<< heldout_collection.behavior_labels.size()
<< ",\"usedByResidualExperiment\":false}},\n"
<< " \"collection\":{\"trainingWallSeconds\":"
<< training_collection.wall_seconds
<< ",\"heldoutWallSeconds\":" << heldout_collection.wall_seconds
<< ",\"training\":";
writeDataset(output, training_data);
output << ",\"heldout\":";
writeDataset(output, heldout_data);
output << "},\n \"model\":{\"kind\":\"sparse-reflection-invariant-nnue\","
"\"canonicalization\":\"lexicographically-smaller-horizontal-reflection\","
"\"inputCount\":"
<< kInputCount << ",\"activeInputsPerBoard\":" << kCellCount
<< ",\"hiddenRelu\":" << kHidden
<< ",\"parameterCount\":" << kParameterCount
<< ",\"epochs\":" << kTrainingEpochs << ",\"batchSize\":"
<< kBatchSize << ",\"learningRate\":" << kLearningRate
<< ",\"l2\":" << kL2 << ",\"initialLoss\":"
<< training.initial_loss << ",\"finalLoss\":"
<< training.final_loss << ",\"fingerprintFnv1a64\":\"0x"
<< std::hex << fingerprint << std::dec << "\",\"path\":\""
<< options.model << "\"},\n \"prediction\":{\"gate\":{"
"\"minimumHeldoutAuc\":"
<< kMinimumHeldoutAuc << ",\"minimumHeldoutPairAccuracy\":"
<< kMinimumHeldoutPairAccuracy
<< ",\"minimumHalfPairAccuracy\":"
<< kMinimumHalfPairAccuracy << ",\"minimumHeldoutExamples\":200,"
"\"minimumHeldoutPairs\":100},\"training\":";
writePrediction(output, training_prediction);
output << ",\"heldout\":";
writePrediction(output, heldout_prediction);
output << ",\"passed\":" << (prediction_passed ? "true" : "false")
<< "},\n \"policyDiagnostic\":";
writePolicyDiagnostic(output, policy);
output << ",\n \"search\":{\"baseline\":\"fair-only-full-width-depth4\","
"\"candidate\":\"same-search-plus-NNUE-at-leaves-only\","
"\"chanceSamples\":"
<< fair::kChanceSamples << ",\"maximumWork\":"
<< fair::kMaximumWork << ",\"maximumCacheEntries\":"
<< fair::kMaximumCacheEntries << ",\"maximumMoves\":"
<< kGameplayMaximumMoves << "},\n \"screen\":";
if (screen == nullptr) {
output << "null";
} else {
writeGameplay(output, kScreenSeedStart, *screen, *screen_baseline,
*screen_candidate, *screen_paired, screen_passed);
}
output << ",\n \"confirmation\":";
if (confirmation == nullptr) {
output << "null";
} else {
writeGameplay(output, kConfirmationSeedStart, *confirmation,
*confirmation_baseline, *confirmation_candidate,
*confirmation_paired, confirmation_passed);
}
output << ",\n \"screenRan\":" << (screen != nullptr ? "true" : "false")
<< ",\n \"screenPassed\":" << (screen_passed ? "true" : "false")
<< ",\n \"confirmationRan\":"
<< (confirmation != nullptr ? "true" : "false")
<< ",\n \"confirmationPassed\":"
<< (confirmation_passed ? "true" : "false")
<< ",\n \"qualified\":"
<< (prediction_passed && policy.passed && screen_passed &&
confirmation_passed
? "true"
: "false")
<< ",\n \"wallSeconds\":" << wall_seconds
<< ",\n \"peakRssBytes\":" << peakRssBytes() << "\n}\n";
}
int run(const Options& options, std::ostream& output) {
const auto started = Clock::now();
const CollectedSplit training_collection =
collectSplit(kTrainingSeedStart, kTrainingGames, "training");
const CollectedSplit heldout_collection =
collectSplit(kHeldoutSeedStart, kHeldoutGames, "heldout");
writeBehaviorLabels(options.labels, training_collection.behavior_labels,
heldout_collection.behavior_labels);
const MatchedDataset training_data =
matchRecords(training_collection.fair, training_collection.oracle);
const MatchedDataset heldout_data =
matchRecords(heldout_collection.fair, heldout_collection.oracle);
if (training_data.examples.size() < 64 || heldout_data.examples.empty()) {
throw std::runtime_error("insufficient matched topology data");
}
const TrainingResult training = trainModel(training_data.examples);
const PredictionMetrics training_prediction = predictionMetrics(
training.model, training_data,
kTrainingSeedStart + static_cast<std::uint32_t>(kTrainingGames / 2));
const PredictionMetrics heldout_prediction = predictionMetrics(
training.model, heldout_data,
kHeldoutSeedStart + static_cast<std::uint32_t>(kHeldoutGames / 2));
const bool prediction_passed = predictionGate(heldout_prediction);
const std::uint64_t fingerprint = modelFingerprint(training.model);
writeModel(options.model, training.model, fingerprint);
PolicyDiagnostic policy;
if (prediction_passed) {
policy = diagnosePolicy(
training.model,
selectDiagnosticStates(heldout_collection.fair_diagnostics));
}
GameplayCohort screen;
GameSummary screen_baseline;
GameSummary screen_candidate;
PairedGameplay screen_paired;
bool screen_passed = false;
if (prediction_passed && policy.passed) {
screen = runGameplayCohort(kScreenSeedStart, kScreenGames,
training.model,
*policy.selected_coefficient, "screen");
screen_baseline = summarizeGames(screen.baseline);
screen_candidate = summarizeGames(screen.candidate);
screen_paired = pairedGameplay(screen);
screen_passed = improvesBoth(screen_baseline, screen_candidate);
}
GameplayCohort confirmation;
GameSummary confirmation_baseline;
GameSummary confirmation_candidate;
PairedGameplay confirmation_paired;
bool confirmation_passed = false;
if (screen_passed) {
confirmation = runGameplayCohort(
kConfirmationSeedStart, kConfirmationGames, training.model,
*policy.selected_coefficient, "confirmation");
confirmation_baseline = summarizeGames(confirmation.baseline);
confirmation_candidate = summarizeGames(confirmation.candidate);
confirmation_paired = pairedGameplay(confirmation);
confirmation_passed =
improvesBoth(confirmation_baseline, confirmation_candidate);
}
const double wall_seconds =
std::chrono::duration<double>(Clock::now() - started).count();
writeArtifact(
options, training_collection, heldout_collection, training_data,
heldout_data, training, training_prediction, heldout_prediction,
prediction_passed, fingerprint, policy,
prediction_passed && policy.passed ? &screen : nullptr,
prediction_passed && policy.passed ? &screen_baseline : nullptr,
prediction_passed && policy.passed ? &screen_candidate : nullptr,
prediction_passed && policy.passed ? &screen_paired : nullptr,
screen_passed, screen_passed ? &confirmation : nullptr,
screen_passed ? &confirmation_baseline : nullptr,
screen_passed ? &confirmation_candidate : nullptr,
screen_passed ? &confirmation_paired : nullptr, confirmation_passed,
wall_seconds);
output << std::fixed << std::setprecision(4)
<< "ORACLE_TOPOLOGY_RESIDUAL_RESULT {\"trainingPairs\":"
<< training_data.pairs.size() << ",\"heldoutPairs\":"
<< heldout_data.pairs.size() << ",\"heldoutAuc\":"
<< heldout_prediction.auc << ",\"heldoutPairAccuracy\":"
<< heldout_prediction.pair_accuracy
<< ",\"firstHalfPairAccuracy\":"
<< heldout_prediction.first_half_accuracy
<< ",\"secondHalfPairAccuracy\":"
<< heldout_prediction.second_half_accuracy
<< ",\"predictionPassed\":"
<< (prediction_passed ? "true" : "false")
<< ",\"policyPassed\":" << (policy.passed ? "true" : "false")
<< ",\"selectedCoefficient\":";
if (policy.selected_coefficient.has_value()) {
output << *policy.selected_coefficient;
} else {
output << "null";
}
output << ",\"screenRan\":"
<< (prediction_passed && policy.passed ? "true" : "false")
<< ",\"screenPassed\":" << (screen_passed ? "true" : "false")
<< ",\"confirmationRan\":"
<< (screen_passed ? "true" : "false")
<< ",\"confirmationPassed\":"
<< (confirmation_passed ? "true" : "false")
<< ",\"fingerprint\":\"0x" << std::hex << fingerprint << std::dec
<< "\",\"wallSeconds\":" << wall_seconds
<< ",\"peakRssBytes\":" << peakRssBytes() << ",\"artifact\":\""
<< options.output << "\",\"model\":\"" << options.model
<< "\",\"behaviorLabels\":\"" << options.labels << "\"}\n";
return 0;
}
} // namespace drop7::oracle_topology_residual
#ifndef DROP7_ORACLE_TOPOLOGY_RESIDUAL_LIBRARY
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--self-test") {
return drop7::oracle_topology_residual::selfTest(std::cout)
? EXIT_SUCCESS
: EXIT_FAILURE;
}
if (argc >= 2 && std::string_view(argv[1]) == "--run") {
return drop7::oracle_topology_residual::run(
drop7::oracle_topology_residual::parseOptions(argc, argv, 2),
std::cout);
}
std::cerr << "usage: drop7_oracle_topology_residual --self-test | --run "
"[--output PATH] [--model PATH] [--labels PATH]\n";
return 2;
} catch (const std::exception& error) {
std::cerr << "drop7_oracle_topology_residual: " << error.what() << '\n';
return 1;
}
}
#endif