Skip to content

Commit

Permalink
Emit statistics from puzzle solver. (#17)
Browse files Browse the repository at this point in the history
The Puzzle type can optionally be configured to use a profiler by passing the `Profile` or `NoProfile` type parameter. If `NoProfile` is chosen, no runtime cost is incurred.

Currently, the following statistics are tracked:
* Maximum depth of tree.
* Number of nodes in the tree.

The unit tests for the puzzles also assert on these statistics to give an additional way of tracking changes in performance.

Closes #14.
  • Loading branch information
cameron-martin authored May 7, 2020
1 parent a14f00b commit 563aae0
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 25 deletions.
25 changes: 21 additions & 4 deletions benches/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use tsumego_solver::go::{BoardPosition, GoGame, Move};
use tsumego_solver::puzzle::NoProfile;
use tsumego_solver::puzzle::Puzzle;

fn playing_moves(c: &mut Criterion) {
Expand Down Expand Up @@ -71,23 +72,35 @@ fn solving_puzzles(c: &mut Criterion) {

simple.bench_function("1", |b| {
b.iter_batched(
|| Puzzle::from_sgf(include_str!("../src/test_sgfs/puzzles/true_simple1.sgf")),
|| {
Puzzle::<NoProfile>::from_sgf(include_str!(
"../src/test_sgfs/puzzles/true_simple1.sgf"
))
},
|mut puzzle| puzzle.solve(),
BatchSize::SmallInput,
)
});

simple.bench_function("2", |b| {
b.iter_batched(
|| Puzzle::from_sgf(include_str!("../src/test_sgfs/puzzles/true_simple2.sgf")),
|| {
Puzzle::<NoProfile>::from_sgf(include_str!(
"../src/test_sgfs/puzzles/true_simple2.sgf"
))
},
|mut puzzle| puzzle.solve(),
BatchSize::SmallInput,
)
});

simple.bench_function("3", |b| {
b.iter_batched(
|| Puzzle::from_sgf(include_str!("../src/test_sgfs/puzzles/true_simple3.sgf")),
|| {
Puzzle::<NoProfile>::from_sgf(include_str!(
"../src/test_sgfs/puzzles/true_simple3.sgf"
))
},
|mut puzzle| puzzle.solve(),
BatchSize::SmallInput,
)
Expand All @@ -100,7 +113,11 @@ fn solving_puzzles(c: &mut Criterion) {

medium.bench_function("1", |b| {
b.iter_batched(
|| Puzzle::from_sgf(include_str!("../src/test_sgfs/puzzles/true_medium1.sgf")),
|| {
Puzzle::<NoProfile>::from_sgf(include_str!(
"../src/test_sgfs/puzzles/true_medium1.sgf"
))
},
|mut puzzle| puzzle.solve(),
BatchSize::SmallInput,
)
Expand Down
21 changes: 14 additions & 7 deletions src/bin/cli/explore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ use std::fs;
use std::path::Path;
use std::rc::Rc;
use tsumego_solver::go::GoGame;
use tsumego_solver::puzzle::Puzzle;
use tsumego_solver::puzzle::{Profile, Puzzle};

fn load_puzzle(filename: &str) -> Puzzle {
fn load_puzzle(filename: &str) -> Puzzle<Profile> {
let game = GoGame::from_sgf(&fs::read_to_string(Path::new(filename)).unwrap());

Puzzle::new(game)
}

fn create_layer(puzzle_cell: Rc<RefCell<Puzzle>>) -> LinearLayout {
fn create_layer(puzzle_cell: Rc<RefCell<Puzzle<Profile>>>) -> LinearLayout {
let puzzle = puzzle_cell.borrow();
let edges = puzzle.tree.edges(puzzle.current_node_id);

let up_view = PaddedView::new(
Margins::lrtb(0, 0, 0, 2),
Margins::lrtb(0, 0, 1, 2),
Button::new("Up", {
let puzzle_cell = puzzle_cell.clone();
move |s| {
Expand Down Expand Up @@ -50,18 +50,25 @@ fn create_layer(puzzle_cell: Rc<RefCell<Puzzle>>) -> LinearLayout {
}

let node_display = PaddedView::new(
Margins::lrtb(0, 0, 0, 2),
Margins::lr(0, 2),
TextView::new(format!(
"{:?}\n\n{}",
puzzle.tree[puzzle.current_node_id],
puzzle.current_game().get_board()
)),
);

let middle = PaddedView::new(
Margins::lrtb(2, 2, 0, 2),
LinearLayout::horizontal()
.child(node_display)
.child(TextView::new(puzzle.profiler.print())),
);

LinearLayout::vertical()
.child(up_view)
.child(node_display)
.child(children)
.child(middle)
.child(PaddedView::new(Margins::lrtb(2, 0, 0, 1), children))
}

pub fn run(filename: &str) {
Expand Down
3 changes: 2 additions & 1 deletion src/bin/cli/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::thread;
use std::time::Duration;
use tsumego_solver::generation::generate_puzzle;
use tsumego_solver::go::GoBoard;
use tsumego_solver::puzzle::NoProfile;

pub fn run(output_directory: &Path, thread_count: u8) -> io::Result<()> {
fs::create_dir_all(output_directory)?;
Expand All @@ -15,7 +16,7 @@ pub fn run(output_directory: &Path, thread_count: u8) -> io::Result<()> {
for _ in 0..thread_count {
let tx = tx.clone();
thread::spawn(move || loop {
let puzzle = generate_puzzle(Duration::from_secs(1));
let puzzle = generate_puzzle::<NoProfile>(Duration::from_secs(1));
tx.send(puzzle).unwrap();
});
}
Expand Down
5 changes: 3 additions & 2 deletions src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ mod candidate;
mod validation;

use crate::go::GoBoard;
use crate::puzzle::Profiler;
pub use candidate::generate_candidate;
use std::time::Duration;
pub use validation::validate_candidate;

pub fn generate_puzzle(timeout: Duration) -> GoBoard {
pub fn generate_puzzle<P: Profiler>(timeout: Duration) -> GoBoard {
let mut rng = rand::thread_rng();

loop {
let candidate = generate_candidate(&mut rng);

if validate_candidate(candidate, timeout) {
if validate_candidate::<P>(candidate, timeout) {
return candidate;
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/generation/validation.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use crate::go::{GoBoard, GoGame, GoPlayer};
use crate::puzzle::Profiler;
use crate::puzzle::Puzzle;
use std::time::Duration;

pub fn validate_candidate(candidate: GoBoard, timeout: Duration) -> bool {
pub fn validate_candidate<P: Profiler>(candidate: GoBoard, timeout: Duration) -> bool {
if candidate.has_dead_groups() {
return false;
}

GoPlayer::both().all(|first_player| {
let mut puzzle = Puzzle::new(GoGame::from_board(candidate, *first_player));
let mut puzzle = Puzzle::<P>::new(GoGame::from_board(candidate, *first_player));

if !puzzle.solve_with_timeout(timeout) {
return false;
Expand Down
39 changes: 30 additions & 9 deletions src/puzzle.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod profiler;
mod proof_number;

use crate::go::{GoBoard, GoGame, GoPlayer, Move};
use petgraph::stable_graph::NodeIndex;
use petgraph::stable_graph::StableGraph;
use petgraph::visit::EdgeRef;
use petgraph::Direction;
pub use profiler::{NoProfile, Profile, Profiler};
use proof_number::ProofNumber;
use std::fmt;
use std::fmt::{Debug, Formatter};
Expand Down Expand Up @@ -89,18 +91,19 @@ impl AndOrNode {
}
}

pub struct Puzzle {
pub struct Puzzle<P: Profiler> {
player: GoPlayer,
attacker: GoPlayer,
pub tree: StableGraph<AndOrNode, Move>,
pub root_id: NodeIndex,
pub current_node_id: NodeIndex,
game_stack: Vec<GoGame>,
current_type: NodeType,
pub profiler: P,
}

impl Puzzle {
pub fn new(game: GoGame) -> Puzzle {
impl<P: Profiler> Puzzle<P> {
pub fn new(game: GoGame) -> Puzzle<P> {
// debug_assert_eq!(game.plys(), 0);

let attacker = if !(game.get_board().out_of_bounds().expand_one()
Expand All @@ -126,10 +129,11 @@ impl Puzzle {
current_node_id: root_id,
game_stack: vec![game],
current_type: NodeType::Or,
profiler: P::new(),
}
}

pub fn from_sgf(sgf_string: &str) -> Puzzle {
pub fn from_sgf(sgf_string: &str) -> Puzzle<P> {
Self::new(GoGame::from_sgf(sgf_string))
}

Expand Down Expand Up @@ -163,6 +167,7 @@ impl Puzzle {
.add_edge(self.current_node_id, new_node_id, Move::PassTwice);

not_empty = true;
self.profiler.add_nodes(1);
}

debug_assert!(
Expand All @@ -171,6 +176,8 @@ impl Puzzle {
game
);

self.profiler.add_nodes(moves.len() as u8);

for (child, board_move) in moves {
let new_node = if !child
.get_board()
Expand Down Expand Up @@ -258,6 +265,8 @@ impl Puzzle {
self.current_type = self.current_type.flip();
self.game_stack
.push(self.current_game().play_move(go_move).unwrap());

self.profiler.move_down();
}

pub fn move_up(&mut self) -> bool {
Expand All @@ -270,6 +279,8 @@ impl Puzzle {
self.current_type = self.current_type.flip();
self.game_stack.pop();

self.profiler.move_up();

true
} else {
false
Expand Down Expand Up @@ -412,59 +423,69 @@ mod tests {
fn true_simple1() {
let tsumego = GoGame::from_sgf(include_str!("test_sgfs/puzzles/true_simple1.sgf"));

let mut puzzle = Puzzle::new(tsumego);
let mut puzzle = Puzzle::<Profile>::new(tsumego);

puzzle.solve();

assert!(puzzle.root_node().is_proved());
assert_eq!(puzzle.first_move(), Move::Place(BoardPosition::new(4, 0)));
assert_eq!(puzzle.profiler.node_count, 556);
assert_eq!(puzzle.profiler.max_depth, 6);
}

#[test]
fn true_simple2() {
let tsumego = GoGame::from_sgf(include_str!("test_sgfs/puzzles/true_simple2.sgf"));

let mut puzzle = Puzzle::new(tsumego);
let mut puzzle = Puzzle::<Profile>::new(tsumego);

puzzle.solve();

assert!(puzzle.root_node().is_proved(), "{:?}", puzzle.root_node());
assert_eq!(puzzle.first_move(), Move::Place(BoardPosition::new(2, 1)));
assert_eq!(puzzle.profiler.node_count, 9270);
assert_eq!(puzzle.profiler.max_depth, 12);
}

#[test]
fn true_simple3() {
let tsumego = GoGame::from_sgf(include_str!("test_sgfs/puzzles/true_simple3.sgf"));

let mut puzzle = Puzzle::new(tsumego);
let mut puzzle = Puzzle::<Profile>::new(tsumego);

puzzle.solve();

assert!(puzzle.root_node().is_proved(), "{:?}", puzzle.root_node());
assert_eq!(puzzle.first_move(), Move::Place(BoardPosition::new(5, 0)));
assert_eq!(puzzle.profiler.node_count, 132);
assert_eq!(puzzle.profiler.max_depth, 8);
}

#[test]
fn true_simple4() {
let tsumego = GoGame::from_sgf(include_str!("test_sgfs/puzzles/true_simple4.sgf"));

let mut puzzle = Puzzle::new(tsumego);
let mut puzzle = Puzzle::<Profile>::new(tsumego);

puzzle.solve();

assert!(puzzle.root_node().is_proved(), "{:?}", puzzle.root_node());
assert_eq!(puzzle.first_move(), Move::Place(BoardPosition::new(7, 0)));
assert_eq!(puzzle.profiler.node_count, 42067);
assert_eq!(puzzle.profiler.max_depth, 11);
}

#[test]
fn true_medium1() {
let tsumego = GoGame::from_sgf(include_str!("test_sgfs/puzzles/true_medium1.sgf"));

let mut puzzle = Puzzle::new(tsumego);
let mut puzzle = Puzzle::<Profile>::new(tsumego);

puzzle.solve();

assert!(puzzle.root_node().is_proved(), "{:?}", puzzle.root_node());
assert_eq!(puzzle.first_move(), Move::Place(BoardPosition::new(14, 2)));
assert_eq!(puzzle.profiler.node_count, 213407);
assert_eq!(puzzle.profiler.max_depth, 26);
}
}
60 changes: 60 additions & 0 deletions src/puzzle/profiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
pub trait Profiler {
fn new() -> Self;
fn move_up(&mut self);
fn move_down(&mut self);
fn add_nodes(&mut self, node_count: u8);
}

pub struct NoProfile;

impl Profiler for NoProfile {
fn new() -> NoProfile {
NoProfile
}

fn move_up(&mut self) {}

fn move_down(&mut self) {}

fn add_nodes(&mut self, _node_count: u8) {}
}

pub struct Profile {
current_depth: u8,
pub max_depth: u8,
pub node_count: u32,
}

impl Profile {
pub fn print(&self) -> String {
format!(
"Max Depth: {}\nNode Count: {}\n",
self.max_depth, self.node_count
)
}
}

impl Profiler for Profile {
fn new() -> Profile {
Profile {
current_depth: 1,
max_depth: 1,
node_count: 1,
}
}

fn move_up(&mut self) {
self.current_depth -= 1;
}

fn move_down(&mut self) {
self.current_depth += 1;
if self.current_depth > self.max_depth {
self.max_depth = self.current_depth;
}
}

fn add_nodes(&mut self, node_count: u8) {
self.node_count += node_count as u32;
}
}

0 comments on commit 563aae0

Please sign in to comment.