Drop7 Research
approaches/fair-expectimax/transition-rewards/fair-clear-reward-confirmation.cppC++369 lines · 14.3 KB
#define DROP7_FAIR_PHASE_ENERGY_RELEASE_LIBRARY
#include "fair-phase-energy-release.cpp"
#undef DROP7_FAIR_PHASE_ENERGY_RELEASE_LIBRARY

// Fixed-candidate confirmation of the isolated +600 numbered-clear transition
// reward selected on 0x3de50000...003.  There is no phase term and no remaining
// candidate menu.  Each later range is read only after its preceding gate.
namespace drop7::fair_clear_reward_confirmation {

namespace phase = fair_phase_energy_release;
using Clock = std::chrono::steady_clock;

constexpr int kStockConfig = 0;
constexpr int kCandidateConfig = 1;
constexpr std::uint32_t kSelectionStart = 0x3de5'0000u;
constexpr int kSelectionGames = 4;
constexpr std::uint32_t kHeldoutStart = 0x3de6'0000u;
constexpr int kHeldoutGames = 8;
constexpr std::uint32_t kScreenStart = 0x3eb3'0000u;
constexpr int kScreenGames = 8;
constexpr std::uint32_t kConfirmationStart = 0x3eb4'0000u;
constexpr int kConfirmationGames = 16;
constexpr int kDefaultThreads = 8;
constexpr double kWallLimitSeconds = 35.0 * 60.0;
constexpr double kProjectionSafetyFactor = 1.50;
// Conservative upper bound inferred before this run from the resource-capped
// selection process: at most eight CPU-minutes per complete D4 game.
constexpr double kInitialSecondsPerGame = 8.0 * 60.0;

constexpr std::array<std::int64_t, kSelectionGames> kStockSelectionScores{{
    185'341, 51'625, 135'827, 200'405,
}};
constexpr std::array<int, kSelectionGames> kStockSelectionMoves{{
    105, 40, 85, 125,
}};
constexpr std::array<std::uint64_t, kSelectionGames> kStockSelectionClears{{
    204, 62, 158, 266,
}};
constexpr std::array<std::int64_t, kSelectionGames> kCandidateSelectionScores{{
    151'969, 154'863, 269'270, 400'644,
}};
constexpr std::array<int, kSelectionGames> kCandidateSelectionMoves{{
    100, 100, 175, 250,
}};
constexpr std::array<std::uint64_t, kSelectionGames>
    kCandidateSelectionClears{{195, 199, 376, 559}};

static_assert(phase::kMenu[kCandidateConfig].clear_reward == 600.0);
static_assert(phase::kMenu[kCandidateConfig].phase_strength == 0.0);
static_assert(phase::kDepth == 4 && phase::kChanceSamples == 5);
static_assert(phase::kMaximumMoves == 1'000);
static_assert(kSelectionStart + kSelectionGames < kHeldoutStart);
static_assert(kHeldoutStart + kHeldoutGames < kScreenStart);
static_assert(kScreenStart + kScreenGames < kConfirmationStart);
static_assert((kSelectionStart >> 24u) == 0x3du &&
              (kHeldoutStart >> 24u) == 0x3du &&
              (kScreenStart >> 24u) == 0x3eu &&
              (kConfirmationStart >> 24u) == 0x3eu);

struct PairedSummary {
  double mean_score_delta = 0.0;
  double mean_moves_delta = 0.0;
  int score_wins = 0;
  int move_wins = 0;
  int score_ties = 0;
  int move_ties = 0;
};

PairedSummary pairedSummary(const phase::MenuGames& games) {
  const auto& baseline = games[kStockConfig];
  const auto& candidate = games[kCandidateConfig];
  if (baseline.size() != candidate.size()) {
    throw std::runtime_error("unpaired clear-reward cohort");
  }
  PairedSummary result;
  for (std::size_t index = 0; index < baseline.size(); ++index) {
    const std::int64_t score_delta =
        candidate[index].score - baseline[index].score;
    const int move_delta = candidate[index].moves - baseline[index].moves;
    result.mean_score_delta += static_cast<double>(score_delta);
    result.mean_moves_delta += move_delta;
    if (score_delta > 0) ++result.score_wins;
    else if (score_delta == 0) ++result.score_ties;
    if (move_delta > 0) ++result.move_wins;
    else if (move_delta == 0) ++result.move_ties;
  }
  if (!baseline.empty()) {
    result.mean_score_delta /= baseline.size();
    result.mean_moves_delta /= baseline.size();
  }
  return result;
}

double measuredProjection(const phase::MenuGames& source, int source_games,
                          int target_games, int threads) {
  double cpu_seconds = 0.0;
  for (const int config : {kStockConfig, kCandidateConfig}) {
    for (const phase::GameResult& game : source[config]) {
      cpu_seconds += game.decision_seconds;
    }
  }
  if (source_games <= 0) return kWallLimitSeconds;
  return kProjectionSafetyFactor * cpu_seconds * target_games /
         (source_games * std::max(1, threads));
}

double initialHeldoutProjection(int threads) {
  return kProjectionSafetyFactor * kInitialSecondsPerGame *
         (2.0 * kHeldoutGames) / std::max(1, threads);
}

bool stageFits(const Clock::time_point& started, double projection) {
  const double elapsed =
      std::chrono::duration<double>(Clock::now() - started).count();
  return elapsed + projection <= kWallLimitSeconds;
}

bool noCensoring(const phase::Summary& baseline,
                 const phase::Summary& candidate) {
  return baseline.censored == 0 && candidate.censored == 0;
}

bool heldoutGate(const phase::MenuGames& games) {
  const phase::Summary baseline = phase::summarize(games[kStockConfig]);
  const phase::Summary candidate = phase::summarize(games[kCandidateConfig]);
  return noCensoring(baseline, candidate) &&
         candidate.mean_score > baseline.mean_score &&
         candidate.mean_moves > baseline.mean_moves &&
         candidate.clears_per_move >= baseline.clears_per_move &&
         candidate.reveals_per_move >= baseline.reveals_per_move;
}

bool screenGate(const phase::MenuGames& games) {
  const phase::Summary baseline = phase::summarize(games[kStockConfig]);
  const phase::Summary candidate = phase::summarize(games[kCandidateConfig]);
  return noCensoring(baseline, candidate) &&
         candidate.mean_score > baseline.mean_score &&
         candidate.mean_moves > baseline.mean_moves;
}

bool confirmationGate(const phase::MenuGames& games) {
  return heldoutGate(games);
}

void expect(bool condition, std::string_view message) {
  if (!condition) throw std::runtime_error(std::string(message));
}

void runSelfTests() {
  phase::runSelfTests();
  expect(phase::kMenu[kCandidateConfig].phase_strength == 0.0 &&
             phase::kMenu[kCandidateConfig].clear_reward == 600.0,
         "fixed candidate changed");
  for (int omitted = 0; omitted < kSelectionGames; ++omitted) {
    std::int64_t stock_score = 0;
    std::int64_t candidate_score = 0;
    int stock_moves = 0;
    int candidate_moves = 0;
    std::uint64_t stock_clears = 0;
    std::uint64_t candidate_clears = 0;
    for (int game = 0; game < kSelectionGames; ++game) {
      if (game == omitted) continue;
      stock_score += kStockSelectionScores[game];
      candidate_score += kCandidateSelectionScores[game];
      stock_moves += kStockSelectionMoves[game];
      candidate_moves += kCandidateSelectionMoves[game];
      stock_clears += kStockSelectionClears[game];
      candidate_clears += kCandidateSelectionClears[game];
    }
    expect(candidate_score > stock_score && candidate_moves > stock_moves &&
               static_cast<double>(candidate_clears) / candidate_moves >
                   static_cast<double>(stock_clears) / stock_moves,
           "selection evidence lost leave-one-out triple win");
  }
  expect(initialHeldoutProjection(kDefaultThreads) == 1'440.0,
         "initial resource projection changed");
}

void writePaired(std::ostream& output, const PairedSummary& summary) {
  output << "{\"meanScoreDelta\":" << summary.mean_score_delta
         << ",\"meanMovesDelta\":" << summary.mean_moves_delta
         << ",\"scoreWins\":" << summary.score_wins
         << ",\"moveWins\":" << summary.move_wins
         << ",\"scoreTies\":" << summary.score_ties
         << ",\"moveTies\":" << summary.move_ties << '}';
}

struct Options {
  std::string output = "/tmp/drop7-fair-clear-reward-confirmation.json";
  int threads = kDefaultThreads;
  bool self_test_only = false;
};

Options parseOptions(int argc, char** argv) {
  Options result;
  for (int index = 1; index < argc; ++index) {
    const std::string argument = argv[index];
    if (argument == "--self-test-only") {
      result.self_test_only = true;
      continue;
    }
    if (index + 1 >= argc) throw std::invalid_argument("missing option value");
    if (argument == "--output") {
      result.output = argv[++index];
    } else if (argument == "--threads") {
      result.threads = std::stoi(argv[++index]);
      if (result.threads < 1 || result.threads > 16) {
        throw std::invalid_argument("threads must be in [1,16]");
      }
    } else {
      throw std::invalid_argument("unknown option " + argument);
    }
  }
  return result;
}

int run(const Options& options) {
  const auto started = Clock::now();
  runSelfTests();
  std::cerr << "clear-reward confirmation self-tests passed (peak RSS "
            << phase::peakRssBytes() << " bytes)\n";
  if (options.self_test_only) return 0;
  const std::vector<int> configs{kStockConfig, kCandidateConfig};
  phase::MenuGames heldout;
  phase::MenuGames screen;
  phase::MenuGames confirmation;
  bool heldout_ran = false;
  bool heldout_passed = false;
  bool screen_ran = false;
  bool screen_passed = false;
  bool confirmation_ran = false;
  bool confirmation_passed = false;
  std::string resource_stop_stage;

  const double heldout_projection = initialHeldoutProjection(options.threads);
  if (!stageFits(started, heldout_projection)) {
    resource_stop_stage = "heldout";
  } else {
    heldout_ran = true;
    heldout = phase::runMenu(kHeldoutStart, kHeldoutGames, configs,
                             options.threads, "clear-reward-heldout");
    heldout_passed = heldoutGate(heldout);
  }

  const double screen_projection =
      heldout_ran ? measuredProjection(heldout, kHeldoutGames, kScreenGames,
                                       options.threads)
                  : kWallLimitSeconds;
  if (heldout_passed) {
    if (!stageFits(started, screen_projection)) {
      resource_stop_stage = "screen";
    } else {
      screen_ran = true;
      screen = phase::runMenu(kScreenStart, kScreenGames, configs,
                              options.threads, "clear-reward-screen");
      screen_passed = screenGate(screen);
    }
  }

  const double confirmation_projection =
      screen_ran ? measuredProjection(screen, kScreenGames, kConfirmationGames,
                                      options.threads)
                 : kWallLimitSeconds;
  if (screen_passed) {
    if (!stageFits(started, confirmation_projection)) {
      resource_stop_stage = "confirmation";
    } else {
      confirmation_ran = true;
      confirmation = phase::runMenu(
          kConfirmationStart, kConfirmationGames, configs, options.threads,
          "clear-reward-confirmation");
      confirmation_passed = confirmationGate(confirmation);
    }
  }

  const double wall_seconds =
      std::chrono::duration<double>(Clock::now() - started).count();
  std::ofstream output(options.output);
  if (!output) throw std::runtime_error("could not open confirmation artifact");
  output << std::setprecision(12)
         << "{\n  \"experiment\":\"fair-d4-fixed-clear-reward-confirmation\",\n"
         << "  \"preregistered\":true,\n"
         << "  \"publicStateOnly\":true,\n"
         << "  \"candidate\":{\"clearReward\":600,\"phaseStrength\":0},\n"
         << "  \"selectionEvidence\":{\"seedStart\":" << kSelectionStart
         << ",\"games\":" << kSelectionGames
         << ",\"meanScoreDelta\":100887,\"meanMovesDelta\":67.5,"
            "\"clearThroughputDelta\":0.18273802817,"
            "\"leaveOneOutTripleWins\":4},\n"
         << "  \"search\":{\"depth\":4,\"chanceSamples\":5,"
            "\"fullWidth\":true,\"maximumMoves\":1000,"
            "\"maximumWork\":3200000,\"worstCaseWork\":3134950,"
            "\"maximumCacheEntries\":60000,"
            "\"worstCaseCacheEntries\":45430},\n"
         << "  \"resourceProtocol\":{\"wallLimitSeconds\":"
         << kWallLimitSeconds << ",\"projectionSafetyFactor\":"
         << kProjectionSafetyFactor << ",\"initialSecondsPerGame\":"
         << kInitialSecondsPerGame << ",\"threads\":" << options.threads
         << ",\"heldoutProjection\":" << heldout_projection
         << ",\"screenProjection\":" << screen_projection
         << ",\"confirmationProjection\":" << confirmation_projection
         << ",\"stopStage\":";
  if (resource_stop_stage.empty()) output << "null";
  else output << '\"' << resource_stop_stage << '\"';
  output << "},\n  \"heldoutRan\":" << (heldout_ran ? "true" : "false")
         << ",\n  \"heldout\":";
  if (!heldout_ran) output << "null";
  else {
    output << "{\"cohort\":";
    phase::writeCohort(output, heldout, configs);
    output << ",\"paired\":";
    writePaired(output, pairedSummary(heldout));
    output << ",\"passed\":" << (heldout_passed ? "true" : "false")
           << '}';
  }
  output << ",\n  \"screenRan\":" << (screen_ran ? "true" : "false")
         << ",\n  \"screen\":";
  if (!screen_ran) output << "null";
  else {
    output << "{\"cohort\":";
    phase::writeCohort(output, screen, configs);
    output << ",\"paired\":";
    writePaired(output, pairedSummary(screen));
    output << ",\"passed\":" << (screen_passed ? "true" : "false")
           << '}';
  }
  output << ",\n  \"confirmationRan\":"
         << (confirmation_ran ? "true" : "false")
         << ",\n  \"confirmation\":";
  if (!confirmation_ran) output << "null";
  else {
    output << "{\"cohort\":";
    phase::writeCohort(output, confirmation, configs);
    output << ",\"paired\":";
    writePaired(output, pairedSummary(confirmation));
    output << ",\"passed\":"
           << (confirmation_passed ? "true" : "false") << '}';
  }
  output << ",\n  \"qualified\":"
         << (confirmation_ran && confirmation_passed ? "true" : "false")
         << ",\n  \"wallSeconds\":" << wall_seconds
         << ",\n  \"peakRssBytes\":" << phase::peakRssBytes() << "\n}\n";
  output.close();

  std::cout << "heldout=" << (heldout_passed ? "pass" : "not-pass")
            << " screen=" << (screen_passed ? "pass" : "not-pass")
            << " confirmation="
            << (confirmation_passed ? "pass" : "not-pass")
            << " resource-stop="
            << (resource_stop_stage.empty() ? "none" : resource_stop_stage)
            << " artifact=" << options.output << '\n';
  return confirmation_ran && confirmation_passed ? 0 : 2;
}

}  // namespace drop7::fair_clear_reward_confirmation

#ifndef DROP7_FAIR_CLEAR_REWARD_CONFIRMATION_LIBRARY
int main(int argc, char** argv) {
  try {
    const auto options =
        drop7::fair_clear_reward_confirmation::parseOptions(argc, argv);
    return drop7::fair_clear_reward_confirmation::run(options);
  } catch (const std::exception& error) {
    std::cerr << "drop7_fair_clear_reward_confirmation: " << error.what()
              << '\n';
    return 1;
  }
}
#endif