Drop7 Research
approaches/value-policy-learning/denoised-value/denoised-guided-veto.cppC++843 lines · 33.7 KB
#define main drop7_denoised_stochastic_value_frozen_main
#include "denoised-stochastic-value.cpp"
#undef main

#define main drop7_nnue_guided_frozen_main
#include "../../tree-search/nnue-guided/nnue-guided-search.cpp"
#undef main

#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <mutex>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

// Combines the fixed denoised value model and guided K3 ensemble in isolation.
// The denoised value model cannot nominate an action: it may only veto the
// three-member guided K3 ensemble in favor of a fixed exact-d3
// fallback.  Both alternatives are compared on the same five root strata.
namespace drop7::denoised_guided_veto {

namespace denoised = drop7::denoised_stochastic_value;
namespace guided = drop7::nnue_guided;

constexpr std::uint32_t kScreenSeedStart = 0x3e84'0000u;
constexpr std::uint32_t kConfirmationSeedStart = 0x3e85'0000u;
constexpr int kScreenGames = 4;
constexpr int kConfirmationGames = 8;
constexpr int kMaximumMoves = 200;
constexpr int kParallelism = 4;
constexpr int kGuidedDepth = 5;
constexpr int kGuidedTopK = 3;
constexpr std::uint64_t kGuidedMaximumWork = 250'000;
constexpr std::size_t kGuidedMaximumCacheEntries = 40'000;
constexpr int kExactDepth = 3;
constexpr int kExactChanceSamples = 5;
constexpr std::uint64_t kExactMaximumWork = 1'000'000;
constexpr std::size_t kExactMaximumCacheEntries = 40'000;
constexpr double kVetoMargin = 2.744151;
constexpr std::uint64_t kMultiplyAddsPerInference =
    2u * (denoised::kActiveCategories * denoised::kHidden1 +
          denoised::kMetricCount * denoised::kHidden1 +
          denoised::kHidden1 * denoised::kHidden2 +
          denoised::kHeads * denoised::kHidden2);

static_assert(kMultiplyAddsPerInference == 12'864);
static_assert(kScreenSeedStart + kScreenGames < kConfirmationSeedStart);

std::mutex progress_mutex;

struct Options {
  std::string guided_model = "/tmp/drop7-phase-q-student-scale.bin";
  std::string value_model =
      "artifacts/models/denoised-value/v1.bin";
  std::string output = "/tmp/drop7-denoised-guided-veto.json";
};

guided::SearchOptions guidedOptions() {
  guided::SearchOptions result;
  result.maximum_depth = kGuidedDepth;
  result.top_k = kGuidedTopK;
  result.guided = true;
  result.safety_union = true;
  result.maximum_work = kGuidedMaximumWork;
  result.maximum_cache_entries = kGuidedMaximumCacheEntries;
  result.policy_seed = guided::kEnsemblePolicySeeds[0];
  return result;
}

cfpi::BehaviorOptions exactOptions() {
  cfpi::BehaviorOptions result;
  result.max_depth = kExactDepth;
  result.chance_samples = kExactChanceSamples;
  result.max_work = kExactMaximumWork;
  result.max_cache_entries = kExactMaximumCacheEntries;
  return result;
}

std::uint64_t fileBytes(const std::string& path) {
  std::ifstream input(path, std::ios::binary | std::ios::ate);
  if (!input) throw std::runtime_error("could not open model artifact " + path);
  const std::streamoff length = input.tellg();
  if (length < 0) throw std::runtime_error("could not size model artifact");
  return static_cast<std::uint64_t>(length);
}

struct ValueEstimate {
  double mean_lifetime = 0.0;
  std::uint64_t transitions = 0;
  std::uint64_t inferences = 0;
};

ValueEstimate estimateCanonicalAction(
    const State& canonical, int canonical_action,
    const denoised::ModelBundle& model) {
  if (!isLegal(canonical.board, canonical_action)) {
    throw std::invalid_argument("cannot value an illegal canonical action");
  }
  ValueEstimate result;
  for (int sample = 0; sample < denoised::kRootStrata; ++sample) {
    const denoised::RootSuccessor successor =
        denoised::rootSuccessor(canonical, canonical_action, sample);
    ++result.transitions;
    if (successor.terminal) continue;
    result.mean_lifetime +=
        denoised::predict(model.network, model.normalizer, model.calibrator,
                           successor.state)
            .lifetime /
        denoised::kRootStrata;
    ++result.inferences;
  }
  return result;
}

ValueEstimate estimateAction(const State& source, int action,
                             const denoised::ModelBundle& model) {
  bool mirrored = false;
  const State canonical = cfpi::detail::canonicalState(source, mirrored);
  const int canonical_action = mirrored ? kBoardSize - 1 - action : action;
  return estimateCanonicalAction(canonical, canonical_action, model);
}

struct VetoSelection {
  int action = -1;
  bool vetoed = false;
};

VetoSelection selectVeto(int ensemble_action, int exact_action,
                         double exact_advantage) {
  const bool veto = ensemble_action != exact_action &&
                    exact_advantage >= kVetoMargin;
  return {veto ? exact_action : ensemble_action, veto};
}

struct MoveDecision {
  int action = -1;
  int ensemble_action = -1;
  int exact_action = -1;
  bool disagreed = false;
  bool vetoed = false;
  double exact_advantage = 0.0;
  std::uint64_t guided_work = 0;
  std::uint64_t exact_work = 0;
  std::uint64_t value_transitions = 0;
  std::uint64_t value_inferences = 0;
  std::size_t peak_cache_entries = 0;
};

MoveDecision chooseCandidateAction(const State& state,
                                   const guided::SearchOptions& options,
                                   const guided::QModel& q_model,
                                   const denoised::ModelBundle& value_model) {
  MoveDecision result;
  const guided::SearchDecision ensemble =
      guided::chooseEnsembleAction(state, options, q_model);
  result.ensemble_action = ensemble.action;
  result.guided_work = ensemble.work;
  result.peak_cache_entries = ensemble.peak_cache_entries;
  if (!isLegal(state.board, result.ensemble_action)) {
    throw std::runtime_error("guided ensemble produced an illegal action");
  }

  cfpi::BehaviorMetrics exact_metrics;
  result.exact_action =
      cfpi::chooseBehaviorAction(state, exactOptions(), &exact_metrics);
  result.exact_work = exact_metrics.work;
  if (!exact_metrics.complete || exact_metrics.completed_depth != kExactDepth) {
    throw std::runtime_error("exact fallback did not complete full depth three");
  }
  if (!isLegal(state.board, result.exact_action)) {
    throw std::runtime_error("exact fallback produced an illegal action");
  }

  result.disagreed = result.ensemble_action != result.exact_action;
  if (result.disagreed) {
    bool mirrored = false;
    const State canonical = cfpi::detail::canonicalState(state, mirrored);
    const int ensemble_canonical =
        mirrored ? kBoardSize - 1 - result.ensemble_action
                 : result.ensemble_action;
    const int exact_canonical =
        mirrored ? kBoardSize - 1 - result.exact_action : result.exact_action;
    const ValueEstimate ensemble_value =
        estimateCanonicalAction(canonical, ensemble_canonical, value_model);
    const ValueEstimate exact_value =
        estimateCanonicalAction(canonical, exact_canonical, value_model);
    result.exact_advantage =
        exact_value.mean_lifetime - ensemble_value.mean_lifetime;
    result.value_transitions =
        ensemble_value.transitions + exact_value.transitions;
    result.value_inferences =
        ensemble_value.inferences + exact_value.inferences;
  }
  const VetoSelection selection = selectVeto(
      result.ensemble_action, result.exact_action, result.exact_advantage);
  result.action = selection.action;
  result.vetoed = selection.vetoed;
  if (result.action != result.ensemble_action &&
      result.action != result.exact_action) {
    throw std::logic_error("veto policy nominated a third action");
  }
  return result;
}

struct GameResult {
  std::uint32_t seed = 0;
  std::int64_t score = 0;
  int moves = 0;
  bool censored = false;
  std::uint64_t decisions = 0;
  std::uint64_t disagreements = 0;
  std::uint64_t vetoes = 0;
  double disagreement_advantage_sum = 0.0;
  double veto_advantage_sum = 0.0;
  std::uint64_t guided_work = 0;
  std::uint64_t exact_work = 0;
  std::uint64_t value_transitions = 0;
  std::uint64_t value_inferences = 0;
  std::size_t peak_cache_entries = 0;
  std::uint64_t peak_rss_bytes = 0;
  double elapsed_seconds = 0.0;
};

void reportGame(std::string_view label, const GameResult& result) {
  const std::lock_guard<std::mutex> lock(progress_mutex);
  std::cerr << label << " seed 0x" << std::hex << result.seed << std::dec
            << ' ' << result.score << " (" << result.moves << " moves, "
            << result.vetoes << '/' << result.disagreements << " vetoes, "
            << "guided work " << result.guided_work << ", exact work "
            << result.exact_work << ")\n";
}

GameResult runBaselineGame(std::uint32_t seed,
                           const guided::SearchOptions& options,
                           const guided::QModel& model,
                           std::string_view label) {
  const auto started = std::chrono::steady_clock::now();
  State state = initialHeadlessState(seed);
  GameResult result;
  result.seed = seed;
  while (!state.game_over && state.moves_played < kMaximumMoves) {
    const guided::SearchDecision decision =
        guided::chooseEnsembleAction(state, options, model);
    if (!isLegal(state.board, decision.action)) {
      throw std::runtime_error("baseline ensemble produced an illegal action");
    }
    ++result.decisions;
    result.guided_work += decision.work;
    result.peak_cache_entries =
        std::max(result.peak_cache_entries, decision.peak_cache_entries);
    MoveResult move;
    if (!playHeadlessMove(state, seed, decision.action, move)) {
      throw std::runtime_error("baseline headless transition failed");
    }
  }
  result.score = state.score;
  result.moves = state.moves_played;
  result.censored = !state.game_over;
  result.peak_rss_bytes = guided::peakRssBytes();
  result.elapsed_seconds = std::chrono::duration<double>(
                               std::chrono::steady_clock::now() - started)
                               .count();
  reportGame(label, result);
  return result;
}

GameResult runCandidateGame(std::uint32_t seed,
                            const guided::SearchOptions& options,
                            const guided::QModel& q_model,
                            const denoised::ModelBundle& value_model,
                            std::string_view label) {
  const auto started = std::chrono::steady_clock::now();
  State state = initialHeadlessState(seed);
  GameResult result;
  result.seed = seed;
  while (!state.game_over && state.moves_played < kMaximumMoves) {
    const MoveDecision decision =
        chooseCandidateAction(state, options, q_model, value_model);
    ++result.decisions;
    result.disagreements += decision.disagreed;
    result.vetoes += decision.vetoed;
    if (decision.disagreed) {
      result.disagreement_advantage_sum += decision.exact_advantage;
    }
    if (decision.vetoed) {
      result.veto_advantage_sum += decision.exact_advantage;
    }
    result.guided_work += decision.guided_work;
    result.exact_work += decision.exact_work;
    result.value_transitions += decision.value_transitions;
    result.value_inferences += decision.value_inferences;
    result.peak_cache_entries =
        std::max(result.peak_cache_entries, decision.peak_cache_entries);
    MoveResult move;
    if (!playHeadlessMove(state, seed, decision.action, move)) {
      throw std::runtime_error("candidate headless transition failed");
    }
  }
  result.score = state.score;
  result.moves = state.moves_played;
  result.censored = !state.game_over;
  result.peak_rss_bytes = guided::peakRssBytes();
  result.elapsed_seconds = std::chrono::duration<double>(
                               std::chrono::steady_clock::now() - started)
                               .count();
  reportGame(label, result);
  return result;
}

struct Cohort {
  std::vector<GameResult> baseline;
  std::vector<GameResult> candidate;
  double wall_seconds = 0.0;
};

Cohort runCohort(std::uint32_t seed_start, int games,
                 const guided::QModel& q_model,
                 const denoised::ModelBundle& value_model,
                 std::string_view phase) {
  const auto started = std::chrono::steady_clock::now();
  Cohort result;
  result.baseline.resize(games);
  result.candidate.resize(games);
  const guided::SearchOptions options = guidedOptions();
  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, options, q_model, std::string(phase) + "-ensemble");
        result.candidate[game] = runCandidateGame(
            seed, options, q_model, value_model,
            std::string(phase) + "-veto");
      }
    }));
  }
  for (auto& worker : workers) worker.get();
  result.wall_seconds = std::chrono::duration<double>(
                            std::chrono::steady_clock::now() - started)
                            .count();
  return result;
}

struct Summary {
  int games = 0;
  double mean_score = 0.0;
  double mean_moves = 0.0;
  int censored = 0;
  std::uint64_t decisions = 0;
  std::uint64_t disagreements = 0;
  std::uint64_t vetoes = 0;
  double disagreement_rate = 0.0;
  double veto_rate = 0.0;
  double veto_rate_on_disagreements = 0.0;
  double mean_exact_advantage_on_disagreements = 0.0;
  double mean_exact_advantage_on_vetoes = 0.0;
  std::uint64_t guided_work = 0;
  std::uint64_t exact_work = 0;
  std::uint64_t value_transitions = 0;
  std::uint64_t value_inferences = 0;
  std::uint64_t value_multiply_adds = 0;
  double guided_work_per_move = 0.0;
  double exact_work_per_move = 0.0;
  double aggregate_game_seconds = 0.0;
  std::size_t peak_cache_entries = 0;
  std::uint64_t peak_rss_bytes = 0;
};

Summary summarize(const std::vector<GameResult>& games) {
  if (games.empty()) throw std::invalid_argument("cannot summarize no games");
  Summary result;
  result.games = static_cast<int>(games.size());
  double disagreement_advantage_sum = 0.0;
  double veto_advantage_sum = 0.0;
  std::uint64_t moves = 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.decisions += game.decisions;
    result.disagreements += game.disagreements;
    result.vetoes += game.vetoes;
    disagreement_advantage_sum += game.disagreement_advantage_sum;
    veto_advantage_sum += game.veto_advantage_sum;
    result.guided_work += game.guided_work;
    result.exact_work += game.exact_work;
    result.value_transitions += game.value_transitions;
    result.value_inferences += game.value_inferences;
    result.aggregate_game_seconds += game.elapsed_seconds;
    result.peak_cache_entries =
        std::max(result.peak_cache_entries, game.peak_cache_entries);
    result.peak_rss_bytes =
        std::max(result.peak_rss_bytes, game.peak_rss_bytes);
    moves += static_cast<std::uint64_t>(game.moves);
  }
  const double decision_count =
      static_cast<double>(std::max<std::uint64_t>(1, result.decisions));
  const double disagreement_count =
      static_cast<double>(std::max<std::uint64_t>(1, result.disagreements));
  const double veto_count =
      static_cast<double>(std::max<std::uint64_t>(1, result.vetoes));
  const double move_count =
      static_cast<double>(std::max<std::uint64_t>(1, moves));
  result.disagreement_rate = result.disagreements / decision_count;
  result.veto_rate = result.vetoes / decision_count;
  result.veto_rate_on_disagreements = result.vetoes / disagreement_count;
  result.mean_exact_advantage_on_disagreements =
      disagreement_advantage_sum / disagreement_count;
  result.mean_exact_advantage_on_vetoes = veto_advantage_sum / veto_count;
  result.value_multiply_adds =
      result.value_inferences * kMultiplyAddsPerInference;
  result.guided_work_per_move = result.guided_work / move_count;
  result.exact_work_per_move = result.exact_work / move_count;
  return result;
}

struct DifferenceStats {
  double mean = 0.0;
  double lower_95 = 0.0;
  int wins = 0;
  int ties = 0;
  int losses = 0;
};

DifferenceStats differences(const std::vector<double>& values) {
  if (values.empty()) throw std::invalid_argument("empty paired differences");
  DifferenceStats result;
  for (const double value : values) {
    result.mean += value / values.size();
    result.wins += value > 0.0;
    result.ties += value == 0.0;
    result.losses += value < 0.0;
  }
  double squares = 0.0;
  for (const double value : values) {
    squares += (value - result.mean) * (value - result.mean);
  }
  const double deviation = values.size() > 1
                               ? std::sqrt(squares / (values.size() - 1))
                               : 0.0;
  result.lower_95 =
      result.mean - 1.96 * deviation / std::sqrt(values.size());
  return result;
}

struct PairedSummary {
  DifferenceStats score;
  DifferenceStats moves;
};

PairedSummary pairedSummary(const std::vector<GameResult>& baseline,
                            const std::vector<GameResult>& candidate) {
  if (baseline.size() != candidate.size() || baseline.empty()) {
    throw std::invalid_argument("invalid paired cohort");
  }
  std::vector<double> scores;
  std::vector<double> moves;
  scores.reserve(baseline.size());
  moves.reserve(baseline.size());
  for (std::size_t game = 0; game < baseline.size(); ++game) {
    scores.push_back(static_cast<double>(candidate[game].score -
                                         baseline[game].score));
    moves.push_back(static_cast<double>(candidate[game].moves -
                                        baseline[game].moves));
  }
  return {differences(scores), differences(moves)};
}

bool improvesBothMeans(const Summary& baseline, const Summary& candidate) {
  return candidate.mean_score > baseline.mean_score &&
         candidate.mean_moves > baseline.mean_moves;
}

void writeSummary(std::ostream& output, const Summary& summary) {
  output << "{\"games\":" << summary.games
         << ",\"meanScore\":" << summary.mean_score
         << ",\"meanMoves\":" << summary.mean_moves
         << ",\"censored\":" << summary.censored
         << ",\"decisions\":" << summary.decisions
         << ",\"disagreements\":" << summary.disagreements
         << ",\"vetoes\":" << summary.vetoes
         << ",\"disagreementRate\":" << summary.disagreement_rate
         << ",\"vetoRate\":" << summary.veto_rate
         << ",\"vetoRateOnDisagreements\":"
         << summary.veto_rate_on_disagreements
         << ",\"meanExactAdvantageOnDisagreements\":"
         << summary.mean_exact_advantage_on_disagreements
         << ",\"meanExactAdvantageOnVetoes\":"
         << summary.mean_exact_advantage_on_vetoes
         << ",\"guidedWork\":" << summary.guided_work
         << ",\"exactWork\":" << summary.exact_work
         << ",\"guidedWorkPerMove\":" << summary.guided_work_per_move
         << ",\"exactWorkPerMove\":" << summary.exact_work_per_move
         << ",\"valueTransitions\":" << summary.value_transitions
         << ",\"valueInferences\":" << summary.value_inferences
         << ",\"valueMultiplyAdds\":" << summary.value_multiply_adds
         << ",\"aggregateGameSeconds\":"
         << summary.aggregate_game_seconds
         << ",\"peakCacheEntries\":" << summary.peak_cache_entries
         << ",\"peakRssBytes\":" << summary.peak_rss_bytes << '}';
}

void writeDifference(std::ostream& output, const DifferenceStats& summary) {
  output << "{\"mean\":" << summary.mean
         << ",\"lower95\":" << summary.lower_95
         << ",\"wins\":" << summary.wins
         << ",\"ties\":" << summary.ties
         << ",\"losses\":" << summary.losses << '}';
}

void writePaired(std::ostream& output, const PairedSummary& summary) {
  output << "{\"score\":";
  writeDifference(output, summary.score);
  output << ",\"moves\":";
  writeDifference(output, summary.moves);
  output << '}';
}

void writeGames(std::ostream& output, const std::vector<GameResult>& games) {
  output << '[';
  for (std::size_t index = 0; index < games.size(); ++index) {
    if (index != 0) output << ',';
    const GameResult& game = games[index];
    output << "{\"seed\":" << game.seed << ",\"score\":" << game.score
           << ",\"moves\":" << game.moves
           << ",\"censored\":" << (game.censored ? "true" : "false")
           << ",\"disagreements\":" << game.disagreements
           << ",\"vetoes\":" << game.vetoes
           << ",\"guidedWork\":" << game.guided_work
           << ",\"exactWork\":" << game.exact_work
           << ",\"valueTransitions\":" << game.value_transitions
           << ",\"valueInferences\":" << game.value_inferences << '}';
  }
  output << ']';
}

void writeCohort(std::ostream& output, std::uint32_t seed_start,
                 const Cohort& cohort, const Summary& baseline,
                 const Summary& candidate, const PairedSummary& paired,
                 bool passed) {
  output << "{\"seedStart\":" << seed_start << ",\"baseline\":";
  writeSummary(output, baseline);
  output << ",\"candidate\":";
  writeSummary(output, candidate);
  output << ",\"paired\":";
  writePaired(output, paired);
  output << ",\"wallSeconds\":" << cohort.wall_seconds
         << ",\"passed\":" << (passed ? "true" : "false")
         << ",\"baselineGames\":";
  writeGames(output, cohort.baseline);
  output << ",\"candidateGames\":";
  writeGames(output, cohort.candidate);
  output << '}';
}

void writeArtifact(const Options& options, std::uint64_t guided_bytes,
                   std::uint64_t value_bytes, const Cohort& screen,
                   const Summary& screen_baseline,
                   const Summary& screen_candidate,
                   const PairedSummary& screen_paired, bool screen_passed,
                   const Cohort* confirmation,
                   const Summary* confirmation_baseline,
                   const Summary* confirmation_candidate,
                   const PairedSummary* confirmation_paired,
                   bool confirmation_passed, double total_wall) {
  std::ofstream output(options.output);
  if (!output) throw std::runtime_error("could not open veto result artifact");
  output << std::setprecision(10)
         << "{\n  \"experiment\":\"denoised-guided-exact-veto\",\n"
         << "  \"preregistered\":true,\n"
         << "  \"scoring\":{\"levelBonus\":7000},\n"
         << "  \"policy\":{\"baseline\":\"three-member-guided-K3-root-Q-ensemble\","
            "\"fallback\":\"verified-full-width-exact-d3-s5\","
            "\"valueUse\":\"exact-only-veto-common-five-strata\","
            "\"thirdActionAllowed\":false,\"vetoMarginMoves\":"
         << kVetoMargin << "},\n"
         << "  \"limits\":{\"maximumMoves\":" << kMaximumMoves
         << ",\"parallelism\":" << kParallelism
         << ",\"guidedDepth\":" << kGuidedDepth
         << ",\"guidedTopK\":" << kGuidedTopK
         << ",\"guidedMaximumWorkPerMember\":" << kGuidedMaximumWork
         << ",\"guidedMaximumCacheEntries\":"
         << kGuidedMaximumCacheEntries
         << ",\"exactDepth\":" << kExactDepth
         << ",\"exactChanceSamples\":" << kExactChanceSamples
         << ",\"exactMaximumWork\":" << kExactMaximumWork
         << ",\"exactMaximumCacheEntries\":"
         << kExactMaximumCacheEntries << "},\n"
         << "  \"models\":{\"guidedPath\":\"" << options.guided_model
         << "\",\"guidedBytes\":" << guided_bytes
         << ",\"denoisedPath\":\"" << options.value_model
         << "\",\"denoisedBytes\":" << value_bytes
         << ",\"combinedBytes\":" << guided_bytes + value_bytes
         << ",\"denoisedPayloadChecksum\":"
         << denoised::checkpointChecksum(
                denoised::readCheckpointBytes(options.value_model),
                denoised::kCheckpointHeaderBytes)
         << "},\n  \"screen\":";
  writeCohort(output, kScreenSeedStart, screen, screen_baseline,
              screen_candidate, screen_paired, screen_passed);
  output << ",\n  \"confirmation\":";
  if (confirmation == nullptr) {
    output << "null";
  } else {
    writeCohort(output, kConfirmationSeedStart, *confirmation,
                *confirmation_baseline, *confirmation_candidate,
                *confirmation_paired, confirmation_passed);
  }
  output << ",\n  \"screenPassed\":"
         << (screen_passed ? "true" : "false")
         << ",\n  \"confirmationRan\":"
         << (confirmation != nullptr ? "true" : "false")
         << ",\n  \"confirmationPassed\":"
         << (confirmation_passed ? "true" : "false")
         << ",\n  \"qualified\":"
         << (screen_passed && confirmation_passed ? "true" : "false")
         << ",\n  \"totalWallSeconds\":" << total_wall
         << ",\n  \"peakRssBytes\":" << guided::peakRssBytes() << "\n}\n";
}

bool selfTest(const Options& options, std::ostream& output) {
  const bool denoised_test = denoised::selfTest(output);
  const bool guided_test = guided::selfTest(options.guided_model, output);
  const denoised::ModelBundle value_model =
      denoised::loadModel(options.value_model);
  guided::QModel q_model;
  q_model.load(options.guided_model);

  State state;
  state.board = initialBoard();
  state.board[indexOf(5, 0)] = 3;
  state.board[indexOf(5, 1)] = 5;
  state.board[indexOf(4, 4)] = kCracked;
  state.board[indexOf(5, 4)] = 4;
  state.next_disc = 6;
  state.moves_remaining = 3;
  State reflected = state;
  reflected.board = cfpi::detail::mirrorBoard(state.board);
  State metadata_changed = state;
  metadata_changed.score = 9'876'543;
  metadata_changed.level = 87;
  metadata_changed.moves_played = 321;

  const ValueEstimate first = estimateAction(state, 1, value_model);
  const ValueEstimate mirrored =
      estimateAction(reflected, kBoardSize - 1 - 1, value_model);
  const ValueEstimate metadata =
      estimateAction(metadata_changed, 1, value_model);
  const bool value_reflection_safe =
      first.mean_lifetime == mirrored.mean_lifetime &&
      first.transitions == mirrored.transitions &&
      first.inferences == mirrored.inferences;
  const bool public_state_only =
      first.mean_lifetime == metadata.mean_lifetime &&
      first.transitions == metadata.transitions &&
      first.inferences == metadata.inferences;
  const VetoSelection below = selectVeto(
      1, 4, std::nextafter(kVetoMargin, 0.0));
  const VetoSelection boundary = selectVeto(1, 4, kVetoMargin);
  const VetoSelection same =
      selectVeto(1, 1, std::numeric_limits<double>::infinity());
  const bool threshold_safe = below.action == 1 && !below.vetoed &&
                              boundary.action == 4 && boundary.vetoed &&
                              same.action == 1 && !same.vetoed;

  const guided::SearchDecision ensemble =
      guided::chooseEnsembleAction(state, guidedOptions(), q_model);
  cfpi::BehaviorMetrics exact_metrics;
  const int exact_action =
      cfpi::chooseBehaviorAction(state, exactOptions(), &exact_metrics);
  const bool exact_verified = exact_metrics.complete &&
                              exact_metrics.completed_depth == kExactDepth &&
                              isLegal(state.board, exact_action);
  const bool ensemble_legal = isLegal(state.board, ensemble.action);
  const MoveDecision combined = chooseCandidateAction(
      state, guidedOptions(), q_model, value_model);
  const bool no_third_action = combined.action == combined.ensemble_action ||
                               combined.action == combined.exact_action;
  const bool fixed_protocol =
      denoised::kRootStrata == 5 && kGuidedDepth == 5 &&
      kGuidedTopK == 3 && kExactDepth == 3 && kExactChanceSamples == 5 &&
      kVetoMargin == 2.744151 && kMaximumMoves == 200 &&
      kScreenSeedStart == 0x3e84'0000u &&
      kConfirmationSeedStart == 0x3e85'0000u;
  const bool model_sizes =
      fileBytes(options.guided_model) == 92'056 &&
      fileBytes(options.value_model) == 141'780;
  const bool passed = denoised_test && guided_test && value_reflection_safe &&
                      public_state_only && threshold_safe && exact_verified &&
                      ensemble_legal && no_third_action && fixed_protocol &&
                      model_sizes;
  output << std::setprecision(10)
         << "DENOISED_GUIDED_VETO_SELF_TEST {\"passed\":"
         << (passed ? "true" : "false")
         << ",\"denoisedSelfTest\":"
         << (denoised_test ? "true" : "false")
         << ",\"guidedSelfTest\":" << (guided_test ? "true" : "false")
         << ",\"publicStateOnly\":"
         << (public_state_only ? "true" : "false")
         << ",\"reflectionSafe\":"
         << (value_reflection_safe ? "true" : "false")
         << ",\"thresholdSafe\":"
         << (threshold_safe ? "true" : "false")
         << ",\"exactFallbackVerified\":"
         << (exact_verified ? "true" : "false")
         << ",\"ensembleLegal\":"
         << (ensemble_legal ? "true" : "false")
         << ",\"noThirdAction\":"
         << (no_third_action ? "true" : "false")
         << ",\"commonStrata\":" << denoised::kRootStrata
         << ",\"vetoMarginMoves\":" << kVetoMargin
         << ",\"guidedModelBytes\":" << fileBytes(options.guided_model)
         << ",\"denoisedModelBytes\":" << fileBytes(options.value_model)
         << ",\"combinedModelBytes\":"
         << fileBytes(options.guided_model) + fileBytes(options.value_model)
         << "}\n";
  return passed;
}

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 value for veto study argument");
    }
    const std::string argument = argv[index];
    const std::string value = argv[index + 1];
    if (argument == "--guided-model") {
      result.guided_model = value;
    } else if (argument == "--value-model") {
      result.value_model = value;
    } else if (argument == "--output") {
      result.output = value;
    } else {
      throw std::invalid_argument("unknown veto study argument " + argument);
    }
  }
  return result;
}

int run(const Options& options, std::ostream& output) {
  const auto started = std::chrono::steady_clock::now();
  guided::QModel q_model;
  q_model.load(options.guided_model);
  const denoised::ModelBundle value_model =
      denoised::loadModel(options.value_model);
  const std::uint64_t guided_bytes = fileBytes(options.guided_model);
  const std::uint64_t value_bytes = fileBytes(options.value_model);

  const Cohort screen = runCohort(kScreenSeedStart, kScreenGames, q_model,
                                  value_model, "screen");
  const Summary screen_baseline = summarize(screen.baseline);
  const Summary screen_candidate = summarize(screen.candidate);
  const PairedSummary screen_paired =
      pairedSummary(screen.baseline, screen.candidate);
  const bool screen_passed =
      improvesBothMeans(screen_baseline, screen_candidate);

  Cohort confirmation;
  Summary confirmation_baseline;
  Summary confirmation_candidate;
  PairedSummary confirmation_paired;
  bool confirmation_passed = false;
  if (screen_passed) {
    confirmation = runCohort(kConfirmationSeedStart, kConfirmationGames,
                             q_model, value_model, "confirmation");
    confirmation_baseline = summarize(confirmation.baseline);
    confirmation_candidate = summarize(confirmation.candidate);
    confirmation_paired =
        pairedSummary(confirmation.baseline, confirmation.candidate);
    confirmation_passed =
        improvesBothMeans(confirmation_baseline, confirmation_candidate);
  }
  const double total_wall = std::chrono::duration<double>(
                                std::chrono::steady_clock::now() - started)
                                .count();
  writeArtifact(
      options, guided_bytes, value_bytes, screen, screen_baseline,
      screen_candidate, screen_paired, screen_passed,
      screen_passed ? &confirmation : nullptr,
      screen_passed ? &confirmation_baseline : nullptr,
      screen_passed ? &confirmation_candidate : nullptr,
      screen_passed ? &confirmation_paired : nullptr, confirmation_passed,
      total_wall);
  output << std::fixed << std::setprecision(3)
         << "DENOISED_GUIDED_VETO_RESULT {\"levelBonus\":7000"
         << ",\"screenBaselineScore\":" << screen_baseline.mean_score
         << ",\"screenBaselineMoves\":" << screen_baseline.mean_moves
         << ",\"screenCandidateScore\":" << screen_candidate.mean_score
         << ",\"screenCandidateMoves\":" << screen_candidate.mean_moves
         << ",\"screenVetoRate\":" << screen_candidate.veto_rate
         << ",\"screenPassed\":"
         << (screen_passed ? "true" : "false")
         << ",\"confirmationRan\":"
         << (screen_passed ? "true" : "false")
         << ",\"confirmationPassed\":"
         << (confirmation_passed ? "true" : "false")
         << ",\"combinedModelBytes\":" << guided_bytes + value_bytes
         << ",\"peakRssBytes\":" << guided::peakRssBytes()
         << ",\"totalWallSeconds\":" << total_wall
         << ",\"artifact\":\"" << options.output << "\"}\n";
  return 0;
}

}  // namespace drop7::denoised_guided_veto

int main(int argc, char** argv) {
  try {
    if (argc >= 2 && std::string_view(argv[1]) == "--self-test") {
      const auto options =
          drop7::denoised_guided_veto::parseOptions(argc, argv, 2);
      return drop7::denoised_guided_veto::selfTest(options, std::cout)
                 ? EXIT_SUCCESS
                 : EXIT_FAILURE;
    }
    if (argc >= 2 && std::string_view(argv[1]) == "--run") {
      const auto options =
          drop7::denoised_guided_veto::parseOptions(argc, argv, 2);
      return drop7::denoised_guided_veto::run(options, std::cout);
    }
    std::cerr << "usage: drop7_denoised_guided_veto --self-test|--run "
                 "[--guided-model PATH] [--value-model PATH] "
                 "[--output PATH]\n";
    return 2;
  } catch (const std::exception& error) {
    std::cerr << "error: " << error.what() << '\n';
    return 1;
  }
}