Drop7 Research
approaches/lifetime-objective/leaf-evolution/weighted-leaf.hppC++188 lines · 7.2 KB
#pragma once
// The fast fair leaf with its weights as run-time data.
//
// EQUIVALENCE CONTRACT.  weightedFairLeaf is drop7::fast::fastFairLeaf with
// each `leafweights::kXWeight` constant replaced by `weights.w[i]`, in the same
// order, with the same operand types (double * int or double * double), the
// same accumulator and no re-association.  At the frozen vector the two
// functions therefore produce the same uint64 bit pattern on every board, and
// gate.cpp --leaf-bits checks that claim on the boards a real search visits.
//
// Roughness is absent for the reason fast-leaf.hpp states at L7: its frozen
// weight is exactly 0.0 and the fast leaf no longer computes the feature.  The
// search space here is therefore eighteen coordinates, not nineteen.  Anyone
// who wants to evolve a roughness term must reinstate the feature first.
//
// The terminal-board constant (kFairTerminalUtility) is not a coordinate: the
// terminal utility was shown saturated in finding-04 and changing it is a
// different experiment.
//
// This header modifies no existing repository file.

#include "../fast-engine/fast-leaf.hpp"

#include <cmath>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>

namespace drop7::fastw {

constexpr int kLeafTerms = 18;

// fast-leaf.hpp:fastFairLeaf accumulation order, minus roughness.
constexpr const char* kLeafNames[kLeafTerms] = {
    "open_columns",           "height_load",
    "solid_cells",            "cracked_cells",
    "numbered_cells",         "high_low_numbers",
    "direct_potential",       "latent_chain_potential",
    "cracked_exposure",       "solid_exposure",
    "adjacent_ones",          "triple_twos",
    "dead_low_numbers",       "covered_height_risk",
    "low_number_height_risk", "danger_height_squared",
    "rise_pressure",          "next_disc_vertical_options",
};

constexpr double kFrozenWeights[kLeafTerms] = {
    fast::leafweights::kOpenColumnsWeight,
    fast::leafweights::kHeightLoadWeight,
    fast::leafweights::kSolidCellsWeight,
    fast::leafweights::kCrackedCellsWeight,
    fast::leafweights::kNumberedCellsWeight,
    fast::leafweights::kHighLowNumbersWeight,
    fast::leafweights::kDirectPotentialWeight,
    fast::leafweights::kLatentChainPotentialWeight,
    fast::leafweights::kCrackedExposureWeight,
    fast::leafweights::kSolidExposureWeight,
    fast::leafweights::kAdjacentOnesWeight,
    fast::leafweights::kTripleTwosWeight,
    fast::leafweights::kDeadLowNumbersWeight,
    fast::leafweights::kCoveredHeightRiskWeight,
    fast::leafweights::kLowNumberHeightRiskWeight,
    fast::leafweights::kDangerHeightSquaredWeight,
    fast::leafweights::kRisePressureWeight,
    fast::leafweights::kNextDiscVerticalOptionsWeight,
};

// Guard against the frozen constants moving underneath this experiment.
static_assert(kFrozenWeights[0] == 180.0);
static_assert(kFrozenWeights[4] == -18.0);
static_assert(kFrozenWeights[6] == 1'600.0);
static_assert(kFrozenWeights[15] == -1'250.0);
static_assert(kFrozenWeights[16] == -35.0);
static_assert(kFrozenWeights[17] == 220.0);

struct LeafWeights {
  double w[kLeafTerms] = {
      kFrozenWeights[0],  kFrozenWeights[1],  kFrozenWeights[2],
      kFrozenWeights[3],  kFrozenWeights[4],  kFrozenWeights[5],
      kFrozenWeights[6],  kFrozenWeights[7],  kFrozenWeights[8],
      kFrozenWeights[9],  kFrozenWeights[10], kFrozenWeights[11],
      kFrozenWeights[12], kFrozenWeights[13], kFrozenWeights[14],
      kFrozenWeights[15], kFrozenWeights[16], kFrozenWeights[17]};

  bool isFrozen() const {
    for (int i = 0; i < kLeafTerms; ++i) {
      std::uint64_t a = 0, b = 0;
      std::memcpy(&a, &w[i], sizeof a);
      std::memcpy(&b, &kFrozenWeights[i], sizeof b);
      if (a != b) return false;
    }
    return true;
  }
};

inline int leafIndexOf(const std::string& name) {
  for (int i = 0; i < kLeafTerms; ++i) {
    if (name == kLeafNames[i]) return i;
  }
  return -1;
}

// Bit-identical to fast::fastFairLeaf when `weights.isFrozen()`.
inline double weightedFairLeaf(const State& state, fast::LeafScratch& scratch,
                               const LeafWeights& weights) {
  if (state.game_over) return fast::leafweights::kFairTerminalUtility;
  fast::FastLeafFeatures f;
  fast::extractFastLeafFeatures<6>(state, scratch, f);
  // Preserve the frozen dot-product order for parity.
  double result = 0.0;
  result += weights.w[0] * f.open_columns;
  result += weights.w[1] * f.height_load;
  result += weights.w[2] * f.solid_cells;
  result += weights.w[3] * f.cracked_cells;
  result += weights.w[4] * f.numbered_cells;
  result += weights.w[5] * f.high_low_numbers;
  result += weights.w[6] * f.direct_potential;
  result += weights.w[7] * f.latent_chain_potential;
  result += weights.w[8] * f.cracked_exposure;
  result += weights.w[9] * f.solid_exposure;
  result += weights.w[10] * f.adjacent_ones;
  result += weights.w[11] * f.triple_twos;
  result += weights.w[12] * f.dead_low_numbers;
  result += weights.w[13] * f.covered_height_risk;
  result += weights.w[14] * f.low_number_height_risk;
  result += weights.w[15] * f.danger_height_squared;
  // (roughness: absent, see the header comment and fast-leaf.hpp L7)
  result += weights.w[16] * f.rise_pressure;
  result += weights.w[17] * f.next_disc_vertical_options;
  return result;
}

// "name value" lines, '#' comments, in any order; every term must be present
// exactly once so a truncated file cannot silently mean "frozen elsewhere".
inline LeafWeights readWeightsFile(const std::string& path) {
  std::ifstream file(path);
  if (!file) throw std::runtime_error("cannot open weights file " + path);
  LeafWeights weights;
  bool seen[kLeafTerms] = {};
  std::string line;
  while (std::getline(file, line)) {
    const auto hash = line.find('#');
    if (hash != std::string::npos) line.erase(hash);
    std::istringstream in(line);
    std::string name;
    if (!(in >> name)) continue;
    double value = 0.0;
    if (!(in >> value) || !std::isfinite(value)) {
      throw std::runtime_error("weights file: bad value for " + name);
    }
    const int index = leafIndexOf(name);
    if (index < 0) throw std::runtime_error("unknown leaf term " + name);
    if (seen[index]) throw std::runtime_error("duplicate leaf term " + name);
    seen[index] = true;
    weights.w[index] = value;
  }
  for (int i = 0; i < kLeafTerms; ++i) {
    if (!seen[i]) {
      throw std::runtime_error(std::string("weights file missing term ") +
                               kLeafNames[i]);
    }
  }
  return weights;
}

// One line of a population file: "<name> w0 w1 ... w17".
inline bool parsePopulationLine(const std::string& line, std::string& name,
                                LeafWeights& weights) {
  std::istringstream in(line);
  if (!(in >> name)) return false;
  if (name[0] == '#') return false;
  for (int i = 0; i < kLeafTerms; ++i) {
    if (!(in >> weights.w[i]) || !std::isfinite(weights.w[i])) {
      throw std::runtime_error("population line for " + name +
                               " has fewer than 18 finite weights");
    }
  }
  double extra = 0.0;
  if (in >> extra) {
    throw std::runtime_error("population line for " + name +
                             " has more than 18 weights");
  }
  return true;
}

}  // namespace drop7::fastw