From 563aae0169986ff60aeda02241ab5e14045df72f Mon Sep 17 00:00:00 2001 From: Cameron Martin Date: Thu, 7 May 2020 15:15:17 +0100 Subject: [PATCH] Emit statistics from puzzle solver. (#17) 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. --- benches/main.rs | 25 ++++++++++++--- src/bin/cli/explore.rs | 21 ++++++++----- src/bin/cli/generate.rs | 3 +- src/generation.rs | 5 +-- src/generation/validation.rs | 5 +-- src/puzzle.rs | 39 +++++++++++++++++------ src/puzzle/profiler.rs | 60 ++++++++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 src/puzzle/profiler.rs diff --git a/benches/main.rs b/benches/main.rs index f729b18..d3c0157 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -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) { @@ -71,7 +72,11 @@ 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::::from_sgf(include_str!( + "../src/test_sgfs/puzzles/true_simple1.sgf" + )) + }, |mut puzzle| puzzle.solve(), BatchSize::SmallInput, ) @@ -79,7 +84,11 @@ fn solving_puzzles(c: &mut Criterion) { simple.bench_function("2", |b| { b.iter_batched( - || Puzzle::from_sgf(include_str!("../src/test_sgfs/puzzles/true_simple2.sgf")), + || { + Puzzle::::from_sgf(include_str!( + "../src/test_sgfs/puzzles/true_simple2.sgf" + )) + }, |mut puzzle| puzzle.solve(), BatchSize::SmallInput, ) @@ -87,7 +96,11 @@ fn solving_puzzles(c: &mut Criterion) { simple.bench_function("3", |b| { b.iter_batched( - || Puzzle::from_sgf(include_str!("../src/test_sgfs/puzzles/true_simple3.sgf")), + || { + Puzzle::::from_sgf(include_str!( + "../src/test_sgfs/puzzles/true_simple3.sgf" + )) + }, |mut puzzle| puzzle.solve(), BatchSize::SmallInput, ) @@ -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::::from_sgf(include_str!( + "../src/test_sgfs/puzzles/true_medium1.sgf" + )) + }, |mut puzzle| puzzle.solve(), BatchSize::SmallInput, ) diff --git a/src/bin/cli/explore.rs b/src/bin/cli/explore.rs index d20efed..58dea2f 100644 --- a/src/bin/cli/explore.rs +++ b/src/bin/cli/explore.rs @@ -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 { let game = GoGame::from_sgf(&fs::read_to_string(Path::new(filename)).unwrap()); Puzzle::new(game) } -fn create_layer(puzzle_cell: Rc>) -> LinearLayout { +fn create_layer(puzzle_cell: Rc>>) -> 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| { @@ -50,7 +50,7 @@ fn create_layer(puzzle_cell: Rc>) -> 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], @@ -58,10 +58,17 @@ fn create_layer(puzzle_cell: Rc>) -> LinearLayout { )), ); + 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) { diff --git a/src/bin/cli/generate.rs b/src/bin/cli/generate.rs index d1f66ee..3049791 100644 --- a/src/bin/cli/generate.rs +++ b/src/bin/cli/generate.rs @@ -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)?; @@ -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::(Duration::from_secs(1)); tx.send(puzzle).unwrap(); }); } diff --git a/src/generation.rs b/src/generation.rs index 604accd..f249617 100644 --- a/src/generation.rs +++ b/src/generation.rs @@ -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(timeout: Duration) -> GoBoard { let mut rng = rand::thread_rng(); loop { let candidate = generate_candidate(&mut rng); - if validate_candidate(candidate, timeout) { + if validate_candidate::

(candidate, timeout) { return candidate; } } diff --git a/src/generation/validation.rs b/src/generation/validation.rs index eaf0b7e..0c0d33a 100644 --- a/src/generation/validation.rs +++ b/src/generation/validation.rs @@ -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(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::

::new(GoGame::from_board(candidate, *first_player)); if !puzzle.solve_with_timeout(timeout) { return false; diff --git a/src/puzzle.rs b/src/puzzle.rs index 26c99d7..c5595de 100644 --- a/src/puzzle.rs +++ b/src/puzzle.rs @@ -1,3 +1,4 @@ +mod profiler; mod proof_number; use crate::go::{GoBoard, GoGame, GoPlayer, Move}; @@ -5,6 +6,7 @@ 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}; @@ -89,7 +91,7 @@ impl AndOrNode { } } -pub struct Puzzle { +pub struct Puzzle { player: GoPlayer, attacker: GoPlayer, pub tree: StableGraph, @@ -97,10 +99,11 @@ pub struct Puzzle { pub current_node_id: NodeIndex, game_stack: Vec, current_type: NodeType, + pub profiler: P, } -impl Puzzle { - pub fn new(game: GoGame) -> Puzzle { +impl Puzzle

{ + pub fn new(game: GoGame) -> Puzzle

{ // debug_assert_eq!(game.plys(), 0); let attacker = if !(game.get_board().out_of_bounds().expand_one() @@ -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

{ Self::new(GoGame::from_sgf(sgf_string)) } @@ -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!( @@ -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() @@ -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 { @@ -270,6 +279,8 @@ impl Puzzle { self.current_type = self.current_type.flip(); self.game_stack.pop(); + self.profiler.move_up(); + true } else { false @@ -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::::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::::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::::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::::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::::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); } } diff --git a/src/puzzle/profiler.rs b/src/puzzle/profiler.rs new file mode 100644 index 0000000..421ecaf --- /dev/null +++ b/src/puzzle/profiler.rs @@ -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; + } +}