add instrumenting support to Bot, returns basic info about algorithm

This commit is contained in:
tali 2023-04-16 19:04:47 -04:00
parent 1784647146
commit 52d339747a
2 changed files with 83 additions and 14 deletions

View File

@ -2,10 +2,10 @@
//! anytime algorithm that will provide a suggestion for the next move. It may be
//! repeatedly polled by the `think` method in order to attempt to improve the suggestion.
use core::ops::Range;
use alloc::boxed::Box;
use alloc::collections::BinaryHeap;
use alloc::vec::Vec;
use core::ops::Range;
use mino::matrix::Mat;
use mino::srs::{Piece, Queue};
@ -14,18 +14,32 @@ mod trans;
use crate::bot::node::{Node, RawNodePtr};
use crate::bot::trans::TransTable;
use crate::eval::{features, Weights};
use crate::eval::{features, Features, Weights};
pub(crate) use bumpalo::Bump as Arena;
/// Encompasses an instance of the algorithm.
pub struct Bot {
iters: u32,
evaluator: Evaluator,
trans: TransTable,
algorithm: SegmentedAStar,
// IMPORTANT: `arena` must occur after `algorithm` so that it is dropped last.
arena: Arena,
iters: u32,
metrics: Option<Box<Metrics>>,
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Metrics {
pub start_features: Features,
pub start_heuristic: i32,
pub end_features: Features,
pub end_heuristic: i32,
pub end_rating: (bool, i32),
pub end_iteration: u32,
// TODO(?) memory usage metrics
// TODO(?) transposition table metrics
// TODO(?) A* metrics
}
impl Bot {
@ -37,11 +51,12 @@ impl Bot {
let trans = TransTable::new();
let algorithm = SegmentedAStar::new(root);
Self {
iters: 0,
evaluator,
trans,
algorithm,
arena,
iters: 0,
metrics: None,
}
}
@ -66,12 +81,19 @@ impl Bot {
&self.evaluator,
&mut self.iters,
);
if did_update {
tracing::debug!(
"new suggestion @ {}: {:?}",
self.iters,
self.algorithm.best().unwrap(),
self.algorithm.best(),
);
if let Some(metrics) = self.metrics.as_deref_mut() {
let weights = &self.evaluator.weights;
let start = self.algorithm.best();
metrics.updated_best(weights, start, self.iters);
}
}
}
}
@ -86,7 +108,20 @@ impl Bot {
/// - `think` has not been called enough times to provide an initial suggestion.
/// - there are no valid placements for the initial state
pub fn suggest(&self) -> Option<Piece> {
self.algorithm.best().and_then(|node| node.root_placement())
self.algorithm.best().root_placement()
}
/// Start collecting metrics about the search algorithm. Uses the current suggestion
/// as the "start" of the metrics data.
pub fn start_instrumenting(&mut self) {
let weights = &self.evaluator.weights;
let start = self.algorithm.best();
self.metrics = Some(Metrics::new(weights, start, self.iters).into());
}
/// Returns the current metrics, if enabled by `start_instrumenting`.
pub fn metrics(&self) -> Option<Metrics> {
self.metrics.as_deref().cloned()
}
}
@ -127,6 +162,33 @@ impl Evaluator {
}
}
impl Metrics {
fn new(weights: &Weights, start: &Node, iters: u32) -> Self {
let features = features(start.matrix(), 0);
let heuristic = features.evaluate(weights);
let rating = start.rating();
Self {
start_features: features,
start_heuristic: heuristic,
end_features: features,
end_heuristic: heuristic,
end_rating: rating,
end_iteration: iters,
}
}
#[cold]
fn updated_best(&mut self, weights: &Weights, node: &Node, iters: u32) {
let features = features(node.matrix(), 0);
let heuristic = features.evaluate(weights);
let rating = node.rating();
self.end_features = features;
self.end_heuristic = heuristic;
self.end_rating = rating;
self.end_iteration = iters;
}
}
// This implements an algorithm that is very similar to A* but has a slight
// modification. Rather than one big open set, there are separate sets at each depth of
// the search. After picking a node from one open set and expanding its children into the
@ -154,7 +216,7 @@ impl Evaluator {
struct SegmentedAStar {
open: Vec<BinaryHeap<AStarNode>>,
depth: usize,
best: Option<RawNodePtr>,
best: RawNodePtr,
}
impl SegmentedAStar {
@ -165,12 +227,12 @@ impl SegmentedAStar {
Self {
open,
depth: 0,
best: None,
best: root.into(),
}
}
fn best(&self) -> Option<&Node> {
self.best.map(|node| unsafe { node.as_node() })
fn best(&self) -> &Node {
unsafe { self.best.as_node() }
}
fn step(
@ -229,9 +291,10 @@ impl SegmentedAStar {
Ok(work)
}
#[cold]
fn backup(&mut self, cand: &Node) -> bool {
if self.best().map_or(true, |best| cand.is_better(best)) {
self.best = Some(cand.into());
if cand.is_better(self.best()) {
self.best = cand.into();
true
} else {
false

View File

@ -70,10 +70,16 @@ impl Node {
unsafe { self.queue.as_queue() }
}
pub fn rating(&self) -> (bool, i32) {
self.rating
}
// TODO: move this function to `bot.algorithm`
pub fn is_better(&self, other: &Node) -> bool {
self.rating > other.rating
}
// TODO: move this function to `bot.algorithm`
pub fn is_terminal(&self) -> bool {
self.queue().is_empty() || self.rating.0
}
@ -146,7 +152,7 @@ impl core::fmt::Debug for Node {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Node {{ ")?;
match self.rating {
(false, h) => write!(f, "heuristic: {}", h),
(false, h) => write!(f, "rating: {}", h),
(true, n) => write!(f, "solution: {}", -n),
}?;
if let Some(pc) = self.root_placement() {