diff --git a/Cargo.toml b/Cargo.toml index 9b48bdec..9e4da1da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ authors = [ "mark raistrick ", ] rust-version = "1.78" -version = "0.9.0" +version = "1.0.0" edition = "2021" license = "AGPL-3.0 license" description = "A UCI compliant chess engine" @@ -24,55 +24,74 @@ categories = ["games"] odonata-base = { path = "./crates/odonata-base" } odonata-engine = { path = "./crates/odonata-engine" } +alphanumeric-sort = "1.4.4" +anyhow = { version = "1.0", features = ["backtrace"] } +append-only-vec = "0.1.3" +argmin = { version = "0.8" } +argmin-math = { version = "0.3", features = ["ndarray_latest-nolinalg-serde"] } arrayvec = { version = "0.7", features = ["serde"] } +backtrace = "0.3.64" bitflags = { version = "2.5.0", features = ["serde"] } -clap = { version = "4.5", features = ["derive"] } -once_cell = "1.19" +boxcar = "0.2" byteorder = "1.5.0" -rand = "0.8" -rand_chacha = "0.3" -fs-err = "2.11" -regex = "1.4" -test-log = { version = "0.2", features = ["trace"] } -anyhow = { version = "1.0", features = ["backtrace"] } -backtrace = "0.3.64" -crossbeam-utils = "0.8" +chrono = { version = "0.4" } +clap = { version = "4.5", features = ["derive"] } +console = "0.15" crossbeam-channel = "0.5" crossbeam-queue = "0.3" +crossbeam-utils = "0.8" +ctrlc = "3.4" +daisychain = { version = "0.0.5" } +derive_more = "0.99" format_num = "0.1" +fs-err = "2.11" +fslock = "0.2.1" +glob = "0.3" hdrhist = "0.5.0" include_dir = "0.7.2" indexmap = { version = "2.2", features = ["serde"] } +indicatif = { version = "0.17", features = ["rayon"] } itertools = "0.13" +liblinear = "1.0.0" log = { version = "0.4", features = ["release_max_level_debug"] } +nalgebra = "0.32" +ndarray = { version = "0.15.6", features = ["rayon"] } +nom = "7.1.1" +nom-supreme = "0.8.0" num-traits = "0.2" +once_cell = "1.19" +pariter = "0.5.1" +perf-event = "0.4.7" petgraph = "0.6.0" +postcard = { version = "1.0.8", features = ["use-std"] } +pretty_assertions = "1.4.0" +rand = "0.8" +rand_chacha = "0.3" +rayon = "1.10" +regex = "1.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.8" +serde_yaml = "0.9" +simba = "0.8.1" static_init = "1.0" +statrs = "0.16.0" strum = "0.26" strum_macros = "0.26" tabled = "0.15.0" +tabwriter = "1.4" +test-log = { version = "0.2", features = ["trace"] } +testresult = "0.4.0" +textplots = "0.8" thread_local = "1.1.4" +timeout-readwrite = "0.3.2" toml = { version = "0.8", features = ["display", "parse", "indexmap"] } -tabwriter = "1.4" -append-only-vec = "0.1.3" -daisychain = { version = "0.0.5" } -console = "0.15" +tracing-appender = "0.2.3" +url = "2.3.1" +wide = "0.7.22" xshell = "0.2" -derive_more = "0.99" -pariter = "0.5.1" -fslock = "0.2.1" -rayon = "1.10" -timeout-readwrite = "0.3.2" -# serde_json = "1.0" -# serde_with = "1.9" -alphanumeric-sort = "1.4.4" -chrono = { version = "0.4" } -nom = "7.1.1" -nom-supreme = "0.8.0" -perf-event = "0.4.7" + + plotters = { version = "0.3.4", default-features = false, features = [ "svg_backend", "full_palette", @@ -83,22 +102,7 @@ plotters = { version = "0.3.4", default-features = false, features = [ pprof = { git = "https://github.com/Erigara/pprof-rs.git", branch = "fix_pointer_align", features = [ "flamegraph", ] } -serde_yaml = "0.9" -# serde_regex = "1.1.0" -statrs = "0.16.0" -url = "2.3.1" -ctrlc = "3.4" -glob = "0.3" -# log = { version = "0.4", features = ["release_max_level_debug"] } -argmin = { version = "0.8" } -argmin-math = { version = "0.3", features = ["ndarray_latest-nolinalg-serde"] } -liblinear = "1.0.0" -nalgebra = "0.32" -ndarray = { version = "0.15.6", features = ["rayon"] } -indicatif = { version = "0.17", features = ["rayon"] } -textplots = "0.8" -boxcar = "0.2" -postcard = { version = "1.0.8", features = ["use-std"] } + # env_logger = "0.11" tracing = { version = "0.1.37", features = [ "max_level_trace", @@ -111,11 +115,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "ansi", "fmt", ] } -tracing-appender = "0.2.3" -pretty_assertions = "1.4.0" -wide = "0.7.22" -simba = "0.8.1" -testresult = "0.4.0" + # criterion = "0.5" diff --git a/crates/odonata-base/src/boards/board.rs b/crates/odonata-base/src/boards/board.rs index 9d42ad91..e2a814e7 100644 --- a/crates/odonata-base/src/boards/board.rs +++ b/crates/odonata-base/src/boards/board.rs @@ -144,6 +144,20 @@ impl Default for Board { } } +impl Ord for Board { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.halfmove_clock() + .cmp(&other.halfmove_clock()) + .then(self.hash().cmp(&other.hash())) + } +} + +impl PartialOrd for Board { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl fmt::Debug for Board { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Board").field("fen", &self.to_fen()).finish() @@ -219,18 +233,34 @@ impl BoardBuilder { /// Parses a FEN string to create a board. FEN format is detailed at https://en.wikipedia.org/wiki/Forsyth–Edwards_Notation /// terminology of "piece placement data" from http://kirill-kryukov.com/chess/doc/fen.html pub fn parse_piece_placement(fen: &str) -> Result { - let mut pos = String::from(fen); - for i in 1..=8 { - pos = pos.replace(i.to_string().as_str(), " ".repeat(i).as_str()); - } - // pos.retain(|ch| "pPRrNnBbQqKk ".contains(ch)); - let r: Vec<&str> = pos.rsplit('/').collect(); - if r.iter().any(|r| r.chars().count() != 8) || r.len() != 8 { - bail!("expected 8 ranks of 8 pieces in fen {}", fen); - } + Ok(Self::parse_piece_placement2(fen)?.1) + } + + fn parse_piece_placement2(fen: &str) -> Result<(&str, Self)> { let mut bb = Board::builder(); - bb.set(Bitboard::all(), &r.concat())?; - Ok(bb) + let mut ir = 7_u32; + let mut ic = 0_u32; + for (_i, c) in fen.char_indices() { + match c { + c if c.is_ascii_digit() => ic += (c as u8 - b'0') as u32, + '.' => ic += 1, + '/' => { + if ir == 0 || ic != 8 { + bail!("expected 8 ranks of 8 pieces in fen {}", fen); + } + ic = 0; + ir -= 1; + } + _ => { + let (p, c) = Piece::and_color_from_char(c)?; + let sq = Square::from_xy(ic, ir); + bb.add_piece(sq, p, c); + ic += 1; + } + }; + } + anyhow::ensure!(ir == 0 && ic == 8, "expected 8 ranks of 8 pieces in fen {}", fen); + Ok((fen, bb)) } pub fn clear(&mut self, bb: Bitboard) { @@ -397,6 +427,13 @@ impl Board { } } + #[inline] + pub fn piece_color(&self, sq: Square) -> Option<(Piece, Color)> { + let c = self.color_of(sq)?; + let p = self.piece_unchecked(sq); // caught by color_of above + Some((p, c)) + } + #[inline] pub fn is_occupied_by(&self, sq: Square, p: Piece) -> bool { sq.is_in(self.pieces(p)) @@ -900,7 +937,7 @@ impl Board { if words.len() < 6 { bail!("must specify at least 6 parts in epd/fen '{}'", fen); } - let mut bb = BoardBuilder::parse_piece_placement(words[0])?; + let (mut _s, mut bb) = BoardBuilder::parse_piece_placement2(words[0])?; bb.set_turn(Color::parse(words[1])?); bb.set_castling(CastlingRights::parse(words[2])?); bb.set_ep_square(if words[3] == "-" { @@ -973,17 +1010,17 @@ mod tests { } #[test] - fn to_fen() { + fn test_to_fen() { for &fen in &[ "7k/8/8/8/8/8/8/7K b KQkq - 45 100", Catalog::STARTING_POSITION_FEN, "8/8/8/8/8/8/8/B7 w - - 0 0", ] { let b = Board::parse_fen(fen).unwrap(); + println!("{fen}"); + // println!("{:#}", b); + // println!("{:L>}", b); assert_eq!(fen, b.to_fen()); - println!("{:#}", b); - println!("{}", b); - println!("{:L>}", b); } } @@ -1003,7 +1040,7 @@ mod tests { } #[test] - fn parse_piece() -> Result<()> { + fn test_fen_parse_errors() -> Result<()> { let fen1 = "1/1/7/8/8/8/PPPPPPPP/RNBQKBNR"; assert_eq!( BoardBuilder::parse_piece_placement(fen1).unwrap_err().to_string(), @@ -1021,7 +1058,7 @@ mod tests { BoardBuilder::parse_piece_placement("X7/8/8/8/8/8/8/8") .unwrap_err() .to_string(), - "Unknown piece 'X'" + "Unknown color/piece 'X'" ); let buf = BoardBuilder::parse_piece_placement("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")?.build(); assert_eq!(buf.get(Bitboard::A1), "R"); @@ -1032,10 +1069,21 @@ mod tests { #[test] fn parse_fen() -> Result<()> { let b = Board::parse_fen("7k/8/8/8/8/8/8/7K b KQkq - 45 100")?; + assert_eq!(b.piece_color(Square::H8), Some((Piece::King, Color::Black))); + assert_eq!(b.piece_color(Square::H1), Some((Piece::King, Color::White))); + assert_eq!(b.occupied().popcount(), 2); assert_eq!(b.color_us(), Color::Black); assert_eq!(b.fullmove_number(), 100); assert_eq!(b.halfmove_clock(), 45); assert_eq!(b.castling(), CastlingRights::all()); + + let b = Board::parse_fen("7k/p7/8/8/8/8/7P/7K b KQkq - 45 100")?; + assert_eq!(b.piece_color(Square::A7), Some((Piece::Pawn, Color::Black))); + assert_eq!(b.piece_color(Square::H2), Some((Piece::Pawn, Color::White))); + assert_eq!(b.piece_color(Square::H8), Some((Piece::King, Color::Black))); + assert_eq!(b.piece_color(Square::H1), Some((Piece::King, Color::White))); + assert_eq!(b.occupied().popcount(), 4); + Ok(()) } diff --git a/crates/odonata-base/src/boards/boardcalcs.rs b/crates/odonata-base/src/boards/boardcalcs.rs index 9b4d7e01..9ab8860f 100644 --- a/crates/odonata-base/src/boards/boardcalcs.rs +++ b/crates/odonata-base/src/boards/boardcalcs.rs @@ -123,6 +123,7 @@ mod tests { use crate::catalog::*; use crate::infra::profiler::PerfProfiler; use crate::mv::Move; + use crate::other::tags::TagOps; use crate::other::Perft; #[test] @@ -147,7 +148,7 @@ mod tests { fn test_pinned() { for epd in Catalog::pins() { let pins = BoardCalcs::pinned_and_discoverers(&epd.board(), epd.board().color_us()).0; - assert_eq!(pins, epd.bitboard("Sq").unwrap(), "{epd}"); + assert_eq!(Some(pins), epd.bitboard("Sq"), "{epd}\ntags:{:#?}", epd.tags()); } } @@ -156,9 +157,7 @@ mod tests { let positions = Catalog::discovered_check(); for epd in positions { let discoverers = BoardCalcs::pinned_and_discoverers(&epd.board(), epd.board().color_us()).1; - assert_eq!(discoverers, epd.bitboard("Sq").unwrap(), "{epd}"); - let discoverers = BoardCalcs::pinned_and_discoverers(&epd.board(), epd.board().color_us()).1; - assert_eq!(discoverers, epd.bitboard("Sq").unwrap(), "{epd}"); + assert_eq!(Some(discoverers), epd.bitboard("Sq"), "{epd}"); } } diff --git a/crates/odonata-base/src/boards/movegen.rs b/crates/odonata-base/src/boards/movegen.rs index f8bbb7c4..b931e919 100644 --- a/crates/odonata-base/src/boards/movegen.rs +++ b/crates/odonata-base/src/boards/movegen.rs @@ -649,7 +649,7 @@ mod tests { .sorted() .join(" "); - assert_eq!(lm, expected, "{epd} {:#}", epd.board()); + assert_eq!(lm, expected, "{epd:#?} {:#}", epd.board()); } } diff --git a/crates/odonata-base/src/domain/info.rs b/crates/odonata-base/src/domain/info.rs index f366e6f8..17e4f558 100644 --- a/crates/odonata-base/src/domain/info.rs +++ b/crates/odonata-base/src/domain/info.rs @@ -125,6 +125,34 @@ impl Info { }; Ok(()) } + + // pub fn to_epd(&self, b: &Board) -> Epd { + // let mut epd = Epd::from_board(b.clone()); + // if let Some(pv) = &self.pv { + // epd.set_tag("sv", &pv.to_san(b)); + // epd.set_tag("sm", &pv.first().unwrap_or_default().to_san(b)); + // } + // if let Some(score) = self.score { + // epd.set_tag("ce", &score.as_i16().to_string()); + // } + // if let Some(depth) = self.depth { + // epd.set_tag("acd", &depth.to_string()); + // } + // if let Some(nodes) = self.nodes { + // epd.set_tag("acn", &nodes.to_string()); + // } + // if let Some(seldepth) = self.seldepth { + // epd.set_tag("acsd", &seldepth.to_string()); + // } + // if let Some(time_millis) = self.time_millis { + // epd.set_tag("acs", &(time_millis / 1000).to_string()); + // epd.set_tag("Acms", &time_millis.to_string()); + // } + // if let Some(comment) = &self.string_text { + // epd.set_tag("c0", comment); + // } + // epd + // } } impl fmt::Display for Info { diff --git a/crates/odonata-base/src/domain/score.rs b/crates/odonata-base/src/domain/score.rs index d4c2acac..17e453c6 100644 --- a/crates/odonata-base/src/domain/score.rs +++ b/crates/odonata-base/src/domain/score.rs @@ -13,13 +13,20 @@ use crate::prelude::*; // bound: NodeType, // } -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(transparent)] /// from the point of view of the player: +ve = winning, -ve = losing pub struct Score { cp: i16, } + +impl fmt::Debug for Score { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + pub trait ToScore { fn cp(self) -> Score; } @@ -165,6 +172,7 @@ impl Score { cp.clamp(-Self::INF as i32, Self::INF as i32) == cp } + #[inline(always)] fn assert_within_range(cp: i32) { debug_assert!(Self::within_range(cp), "centipawns {cp} out of range"); } diff --git a/crates/odonata-base/src/eg/endgame.rs b/crates/odonata-base/src/eg/endgame.rs index 5502e8cc..0b9cab05 100644 --- a/crates/odonata-base/src/eg/endgame.rs +++ b/crates/odonata-base/src/eg/endgame.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumCount, IntoStaticStr}; +use strum::IntoEnumIterator; +use strum_macros::{Display, EnumCount, EnumIter, IntoStaticStr}; use crate::prelude::*; use crate::trace::stat::{SliceStat, Stat}; @@ -54,7 +55,7 @@ impl Configurable for EndGameScoring { } } -#[derive(Copy, Default, Clone, PartialEq, Debug, IntoStaticStr, EnumCount, Display)] +#[derive(Copy, Default, Clone, PartialEq, Debug, IntoStaticStr, EnumCount, EnumIter, Display)] pub enum EndGame { #[default] Unknown, // for when its too costly to work out who wins @@ -138,11 +139,11 @@ pub enum EndGame { use static_init::dynamic; #[dynamic] static ENDGAME_COUNTS: Vec = { - let vec = vec![]; - // for eg in EndGame::iter() { - // let s: &'static str = eg.into(); - // vec.push(Stat::new(s)); - // } + let mut vec = vec![]; + for eg in EndGame::iter() { + let s: &'static str = eg.into(); + vec.push(Stat::new(s)); + } vec }; @@ -416,7 +417,7 @@ impl EndGame { pub fn from_board(b: &Board) -> Self { let eg = Self::private_ctor(b); - // ENDGAME_COUNTS[eg as usize].increment(); + ENDGAME_COUNTS[eg as usize].increment(); eg } diff --git a/crates/odonata-base/src/epd.rs b/crates/odonata-base/src/epd.rs index 99879c24..3dd7f857 100644 --- a/crates/odonata-base/src/epd.rs +++ b/crates/odonata-base/src/epd.rs @@ -130,8 +130,9 @@ impl Epd { self.tags_mut().insert(k, v); } + /// empty matching list means all pub fn merge_tags_from(&mut self, other: Epd, matching: &[&str]) { - let Epd { mut tags, .. } = other; + let mut tags = other.tags; let other_tags = if matching.is_empty() { tags } else { @@ -319,6 +320,7 @@ mod tests { use test_log::test; use super::*; + use crate::infra::profiler::PerfProfiler; use crate::infra::utils::read_file; use crate::other::tags::EpdOps as _; use crate::prelude::*; @@ -442,12 +444,12 @@ mod tests { } #[test] - fn test_epd_file_parse() -> Result<()> { + fn bench_epd_file_parse() -> Result<()> { // let positions = Position::parse_epd_file("../odonata-extras/epd/quiet-labeled.epd")?; - let positions = Epd::parse_many_epd(read_file("../../ext/epd/com15.epd")?)?; - for p in positions { - println!(">> {}", p); - } + let mut prof = PerfProfiler::new("epd.parse"); + let text = read_file("../../ext/epd/com15.epd")?; + let epds = prof.bench(|| Epd::parse_many_epd(&text))?; + assert_eq!(epds[0].tags().get("acd"), Some("21")); Ok(()) } diff --git a/crates/odonata-base/src/infra/lockless_hashmap.rs b/crates/odonata-base/src/infra/lockless_hashmap.rs index 6cce2a7e..cceaccd8 100644 --- a/crates/odonata-base/src/infra/lockless_hashmap.rs +++ b/crates/odonata-base/src/infra/lockless_hashmap.rs @@ -219,8 +219,8 @@ mod tests1 { #[derive(Clone)] pub struct UnsharedTable { - array: Vec<(Cell, Cell)>, - + pub enabled: bool, + array: Vec<(Cell, Cell)>, pub hits: Cell, pub misses: Cell, pub collisions: Cell, @@ -228,9 +228,10 @@ pub struct UnsharedTable { impl Configurable for UnsharedTable { fn set(&mut self, p: Param) -> Result { + self.enabled.set(p.get("enabled"))?; let mut size = 0; if size.set(p.get("size"))? { - *self = Self::from(size); + self.array = Self::from(size).array; } Ok(p.is_modified()) } @@ -252,8 +253,10 @@ impl fmt::Display for UnsharedTable { } impl fmt::Debug for UnsharedTable { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(self, f) + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UnsharedTable") + .field("size", &self.array.len()) + .finish() } } @@ -290,6 +293,7 @@ impl UnsharedTable { assert!(capacity > 0); Self { // array: vec![Self::INIT; capacity], + enabled: true, array: (0..capacity) .map(|_| (Cell::new(0), Cell::new(T::default()))) .collect_vec(), @@ -312,7 +316,7 @@ impl UnsharedTable { pub fn probe(&self, hash: Hash) -> Option { let key = hash as usize % self.capacity(); - if self.array[key].0.get() == hash && hash != 0 { + if self.enabled && self.array[key].0.get() == hash && hash != 0 { self.hits.set(self.hits.get() + 1); Some(self.array[key].1.get()) } else { diff --git a/crates/odonata-base/src/infra/metric.rs b/crates/odonata-base/src/infra/metric.rs index b9b1ba3c..79b1adf7 100644 --- a/crates/odonata-base/src/infra/metric.rs +++ b/crates/odonata-base/src/infra/metric.rs @@ -1064,9 +1064,9 @@ impl fmt::Display for Metrics { // let mut b = Builder::default(); b.set_header(["Counter", "Value"]); - // for eg in EndGame::iter() { - // b.push_record([&eg.to_string(), &i(self.endgame[eg as usize])]); - // } + for eg in EndGame::iter() { + b.push_record([&eg.to_string(), &i(self.endgame[eg as usize])]); + } let mut t = b.build(); t.with(style.clone()) .with(Modify::new(Rows::single(0)).with(Border::default().top('-'))) diff --git a/crates/odonata-base/src/infra/mod.rs b/crates/odonata-base/src/infra/mod.rs index d9377fdb..7155a4dd 100644 --- a/crates/odonata-base/src/infra/mod.rs +++ b/crates/odonata-base/src/infra/mod.rs @@ -3,6 +3,7 @@ pub mod lockless_hashmap; pub mod math; pub mod metric; pub mod param; +pub mod quote; pub mod resources; pub mod utils; pub mod value; diff --git a/crates/odonata-base/src/infra/version.rs b/crates/odonata-base/src/infra/version.rs index a2dbbebf..0b36af9b 100644 --- a/crates/odonata-base/src/infra/version.rs +++ b/crates/odonata-base/src/infra/version.rs @@ -129,8 +129,8 @@ impl Version { s += &format!("cargo profile: {}\n", Version::compiled_profile_name()); s += &format!("avx2 : {}\n", avx2); s += &format!("bmi2 : {}\n", bmi2); - s += &format!("popcnt : {}\n", popcnt); s += &format!("lzcnt : {}\n", lzcnt); + s += &format!("popcnt : {}\n", popcnt); s } diff --git a/crates/odonata-base/src/mv.rs b/crates/odonata-base/src/mv.rs index e1d5d798..d0f60c5a 100644 --- a/crates/odonata-base/src/mv.rs +++ b/crates/odonata-base/src/mv.rs @@ -736,8 +736,9 @@ impl Move { const OFFSET_FLAG: i32 = 12; // 4 bits const OFFSET_MOVER: i32 = 16; // 3 bits const OFFSET_CAPTURE: i32 = 19; // 3 bits - // const OFFSET_PRIOR_EP: i32 = 22; // 6 bits - // const OFFSET_PRIOR_CR: i32 = 28; // 4 bits + // const OFFSET_PRIOR_CR: i32 = 28; // 2 bits + // still need 128=7bits for restore hmvc or EP sq. 8 bits. + // Ba2xNg3/15 } impl fmt::Debug for Move { diff --git a/crates/odonata-base/src/other/tags.rs b/crates/odonata-base/src/other/tags.rs index 5ab45cd7..b597ae0a 100644 --- a/crates/odonata-base/src/other/tags.rs +++ b/crates/odonata-base/src/other/tags.rs @@ -3,13 +3,13 @@ use std::fmt::{self, Display}; use anyhow::{Context as _, Result}; use itertools::Itertools; -use once_cell::sync::Lazy; -use regex::Regex; use serde::{Deserialize, Serialize}; use crate::domain::score::Score; use crate::domain::wdl::WdlOutcome; +use crate::infra::quote::QuoteParser; use crate::movelist::ScoredMoveList; +use crate::piece::Ply; use crate::prelude::{Board, Move}; use crate::variation::Variation; use crate::{Epd, MoveList}; @@ -60,6 +60,15 @@ pub trait TagOps: Display { .map(Score::from_cp) } + fn score_for_depth(&self, depth: Ply) -> Option { + self.tags() + .get(Tags::MCE)? + .splitn(depth as usize + 2, ',') + .nth(depth as usize) + .and_then(|s| s.parse().ok()) + .map(Score::from_cp) + } + fn score_from_tag(&self, tag: &str) -> Option { self.get(tag).and_then(|s| s.parse().ok()).map(Score::from_cp) } @@ -189,6 +198,7 @@ impl Tags { pub const PC: &'static str = "Pc"; // pgn "PlyCount" - total game moves from pgn pub const CC: &'static str = "cc"; pub const CE: &'static str = "ce"; + pub const MCE: &'static str = "Mce"; // multi ce: -12,23,345,-12,0,,,,67,12 pub const CPL: &'static str = "Cpl"; // centipawn loss pub const C9: &'static str = "c9"; pub const DM: &'static str = "dm"; @@ -327,22 +337,20 @@ impl Tags { pub fn parse_epd_tags(_board: &Board, tags_str: &str) -> Result { let mut tags = Tags::new(); - let ops: Vec<&str> = Self::split_into_tags(tags_str); - for op in ops { - let words: Vec<&str> = Tags::split_into_words(op); - debug_assert!( - !words.is_empty(), - "no words parsing EPD operation '{}' from '{}'", - op, - tags_str - ); - tags.0.insert(words[0].to_string(), words[1..].join(" ")); + // trim to remove a trailing space after last ";" + for op in QuoteParser::new(tags_str.trim(), |c| c == ';') { + let op = op.trim(); + let (opcode, operands) = match op.split_once(char::is_whitespace) { + Some((opcode, operands)) => (opcode, operands.trim_matches('"').trim_matches('\'').to_string()), + None => (op, String::new()), + }; + tags.0.insert(opcode.to_string(), operands); } Ok(tags) } pub fn parse_single_tag(_b: &Board, v: &str) -> Result { - let words: Vec<&str> = Self::split_into_words(v); + let words: Vec<&str> = QuoteParser::new(v, char::is_whitespace).without_quotes().collect_vec(); let mut tags = Tags::new(); tags.set(words[0], &words[1..].join(" ")); Ok(tags) @@ -360,91 +368,9 @@ impl Tags { x => x, }) .collect() - // Tags { - // annotator_depth: self.acd, - // sv: self.pv, - // ann_scored_moves: self.eng_scored_moves, - // multi_pv: self.multi_pv, - // ..Self::default() - // } } pub fn validate(&self, _b: &Board) -> anyhow::Result<()> { - // self.annotator_depth.as_ref().map(|v| v.validate()).transpose().with_context(|| "Ad")?; - // self.analysis_count_milli_seconds.as_ref().map(|v| v.validate()).transpose().with_context(|| "Acms")?; - // self.branching_factor.as_ref().map(|v| v.validate()).transpose().with_context(|| "Bf")?; - // self.ann_scored_moves - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .with_context(|| "Asm")?; - // self.eng_scored_moves - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .with_context(|| "Esm")?; - // self.multi_pv - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .with_context(|| "Mpv")?; - // // self.result.as_ref().map(|v| v.validate()).transpose().with_context(|| "Res")?; - // // self.squares.as_ref().map(|v| v.validate()).transpose().with_context(|| "Sq")?; - // self.avoid_moves - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .with_context(|| "am")?; - // // self.acd.as_ref().map(|v| v.validate()).transpose().with_context(|| "acd")?; - // // self.analysis_count_nodes.as_ref().map(|v| v.validate()).transpose().with_context(|| "acn")?; - // // self.analysis_count_sel_depth.as_ref().map(|v| v.validate()).transpose().with_context(|| "acsd")?; - // // self.analysis_count_seconds.as_ref().map(|v| v.validate()).transpose().with_context(|| "acs")?; - // self.best_moves - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .with_context(|| "bm")?; - // // self.chess_clock.as_ref().map(|v| v.validate()).transpose().with_context(|| "cc")?; - // // self.centipawn_evaluation.as_ref().map(|v| v.validate()).transpose().with_context(|| "ce")?; - // // self.direct_mate.as_ref().map(|v| v.validate()).transpose().with_context(|| "dm")?; - // // self.eco.as_ref().map(|v| v.validate()).transpose().with_context(|| "eco")?; - // // self.full_move_number.as_ref().map(|v| v.validate()).transpose().with_context(|| "fmvn")?; - // // self.half_move_clock.as_ref().map(|v| v.validate()).transpose().with_context(|| "hmvc")?; - // // self.id.as_ref().map(|v| v.validate()).transpose().with_context(|| "id")?; - // self.predicted_move - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .map_err(anyhow::Error::msg) - // .with_context(|| "pm")?; - // self.pv - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .with_context(|| "pv")?; - // // self.repetition_count.as_ref().map(|v| v.validate()).transpose().with_context(|| "rc")?; - // // self.no_op.as_ref().map(|v| v.validate()).transpose().with_context(|| "noop")?; - // self.supplied_move - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .map_err(anyhow::Error::msg) - // .with_context(|| "sm")?; - - // self.game_move - // .as_ref() - // .map(|v| v.validate(b)) - // .transpose() - // .map_err(anyhow::Error::msg) - // .with_context(|| "Gm")?; - // // we dont validate sv as it is used to reach the board state - // // self.sv - // // .as_ref() - // // .map(|v| v.validate(b)) - // // .transpose() - // // .with_context(|| "sv")?; - // // self.timestamp.as_ref().map(|v| v.validate()).transpose().with_context(|| "ts")?; - Ok(()) } @@ -481,258 +407,32 @@ impl Tags { } s } - - // pub fn to_uci(&self) -> String { - // use std::fmt::Write; - // let mut s = String::new(); - // for (k, v) in self.as_hash_map_uci().iter() { - // if v.is_empty() { - // write!(s, " {};", k).unwrap(); - // } else if v.contains(char::is_whitespace) { - // write!(s, " {} \"{}\";", k, v).unwrap(); - // } else { - // write!(s, " {} {};", k, v).unwrap(); - // } - // } - // s - // } - fn split_into_words(s: &str) -> Vec<&str> { - REGEX_SPLIT_WORDS - .captures_iter(s) - .map(|cap| { - cap.get(1) - .or_else(|| cap.get(2)) - .or_else(|| cap.get(3)) - .unwrap() - .as_str() - }) - .collect() - } - - fn split_into_tags(s: &str) -> Vec<&str> { - REGEX_SPLIT_TAGS - .captures_iter(s) - .map(|cap| { - cap.get(1) - .or_else(|| cap.get(2)) - .or_else(|| cap.get(3)) - .unwrap() - .as_str() - }) - .collect() - } } -static REGEX_SPLIT_TAGS: Lazy = Lazy::new(|| { - Regex::new( - r#"(?x) - ([^";]* - " [^"]* " # a quoted string (possibly containing ";") - [^";]* - ); - | - ([^';]* - ' [^']* ' # a quoted string (possibly containing ";") - [^';]* - ); - | - ([^;"']+) # an opcode and operand(s) without any quotes - ; - "#, - ) - .unwrap() -}); - -static REGEX_SPLIT_WORDS: Lazy = Lazy::new(|| { - Regex::new( - r#"(?x) - (?: - [^"\s]* - " ([^"]*) " # a double quoted string (possibly containing whitespace) - [^"\s]* - )(?:$|\s)| - (?: - [^'\s]* - ' ([^']*) ' # a single quoted string (possibly containing whitespace) - [^'\s]* - )(?:$|\s) - | - ([^\s"']+) # an opcode/operand without any quotes - (?:$|\s)"#, - ) - .unwrap() -}); - #[cfg(test)] mod tests { - // use serde_json::{value::Value, Map}; use std::mem::size_of; use test_log::test; use super::*; - - // #[test] - // fn tags_basics() { - // let b = &Board::starting_pos(); - // let mut tags = Tags::new(); - // tags.pv = Some("e4 e5 d4 b8c6".var(b)); - // tags.sv = Some("e4".var(b)); - // tags.acd = Some(3); - // tags.eco = Some("".to_string()); - // tags.id = None; - // let map2: IndexMap<&str, String> = indexmap! { - // "acd" => "3".to_string(), - // "pv" => "e2e4 e7e5 d2d4 b8c6".to_string(), - // "eco" => "".to_string(), - // "sv" => "e2e4".to_string(), - // }; - // let map1 = tags.as_hash_map_uci(); - // assert_eq!(map1, map2); // contents match - // assert!(map1.iter().ne(map2.iter())); // ordering different - - // let map3: IndexMap<&str, String> = indexmap! { - // "acd" => "3".to_string(), - // "eco" => "".to_string(), - // "pv" => "e2e4 e7e5 d2d4 b8c6".to_string(), - // "sv" => "e2e4".to_string(), - // }; - // assert_equal(map1.iter(), map3.iter()); // ordering same - - // // UCI - san format - // assert_eq!( - // tags.to_uci(), - // " acd 3; eco; pv \"e2e4 e7e5 d2d4 b8c6\"; sv e2e4;" - // ); - - // // HashMap - san format - // let map1s = tags.as_hash_map_san(b); - // let map3s: IndexMap<&str, String> = indexmap! { - // "acd" => "3".to_string(), - // "eco" => "".to_string(), - // "pv" => "e4 e5 d4 Nc6".to_string(), - // "sv" => "e4".to_string(), - // }; - // assert_equal(map1s.iter(), map3s.iter()); // ordering same - - // // PGN - san format - // assert_eq!( - // tags.to_pgn(b), - // "[%acd 3] [%eco] [%pv \"e4 e5 d4 Nc6\"] [%sv e4]" - // ); - - // // EPD - san format - // assert_eq!(tags.to_epd(b), " acd 3; eco; pv \"e4 e5 d4 Nc6\"; sv e4;"); - - // let pred = |s: &str| ["acd", "Asm", "sv", "c1"].contains(&s); - // assert_eq!(tags.clone().filter(pred).to_epd(b), " acd 3; sv e4;"); - // assert_eq!(tags.clone().filter(|_| false).to_epd(b), ""); - - // tags.comments[0] = Some("0".to_string()); - // tags.comments[1] = Some("1".to_string()); - // assert_eq!(tags.clone().filter(pred).comments[0], None); - // assert_eq!(tags.clone().filter(pred).comments[1], Some("1".to_string())); - // assert_eq!(tags.clone().filter(pred).to_epd(b), " acd 3; sv e4; c1 1;"); - // // JSON - // // json parsing of Tags - failing due to nulls - // // assert_eq!( - // // jsonrpc_core::to_string(&tags).unwrap(), - // // r#"{"acd":"3", "eco":"", "pv":"e4 e5 d4 Nc6", "sv":"e4"}"# - // // ); - // } - - // // #[test] - // fn test_tags_memory() { - // let mut tags = Tags::default(); - // tags.analysis_count_seconds = Some(4); - // tags.acd = Some(3); - // tags.comments[0] = Some(String::from("Hello World2")); - // tags.id = Some(String::from("Hello World")); - // let _vec = vec![tags; 10_000_000]; - // std::thread::sleep(Duration::from_secs_f32(100.0)); - // } - - // #[test] - // fn test_parsing_tags() { - // let b = &Board::default(); - - // let tags = Tags::parse_single_tag(b, "acd 3").unwrap(); - // assert_eq!(tags, Tags { - // acd: Some(3), - // ..Tags::default() - // }); - - // let mut tags = Tags::new(); - // tags.comments[0] = Some("Hello World".to_string()); - // assert_eq!( - // Tags::parse_key_and_tag(b, "c0", "Hello World").unwrap(), - // tags - // ); - // } - - #[test] - fn test_split_words() { - let vec = Tags::split_into_words(r#"bm e4"#); - assert_eq!(vec, vec!["bm", "e4"]); - - let vec = Tags::split_into_words(r#"id "my name is bob""#); - assert_eq!(vec, vec!["id", "my name is bob"]); - - let vec = Tags::split_into_words(r#"id 'my name is bob'"#); - assert_eq!(vec, vec!["id", "my name is bob"]); - } - - #[test] - fn test_split_into_tags() { - let vec = Tags::split_into_tags(r#"cat"meo;w";"mouse";"toad;;;;;;" ;zebra;"#); - assert_eq!(vec, vec!["cat\"meo;w\"", "\"mouse\"", "\"toad;;;;;;\" ", "zebra"]); - - let vec = Tags::split_into_tags(r#"cat'meo;w';'mouse';'toad;;;;;;' ;zebra;"#); - assert_eq!(vec, vec!["cat\'meo;w\'", "\'mouse\'", "\'toad;;;;;;\' ", "zebra"]); - - let vec = Tags::split_into_tags(r#";cat;mouse;toad;;;;;;sheep;zebra"#); - assert_eq!(vec, vec!["cat", "mouse", "toad", "sheep"]); - - // OK, but not desirable (unmatched quote parsing) - let vec = Tags::split_into_tags(r#";ca"t;mouse;"#); - assert_eq!(vec, vec!["t", "mouse"]); - // let vec = split_on_regex("cat;mat;sat;"); - // assert_eq!(vec, vec!["cat;", "mat;", "sat;"], "cat;mat;sat;"); - // let vec = split_on_regex("cat \"hello\";mat;sat;"); - // assert_eq!(vec, vec!["cat \"hello\";", "mat;", "sat;"], "cat;mat;sat;"); - } - - // #[ignore] - // #[test] - // fn tags_x() { - // let mut tags = Tags::default(); - // tags.result = Some("Hello Word".to_owned()); - // let value = serde_json::to_value(tags).unwrap(); - // dbg!(std::mem::size_of_val(&value)); - // dbg!(&value); - // if let Value::Object(map) = &value { - // dbg!(map); - // } - - // let mut map = Map::new(); - // map.insert( - // "result".to_owned(), - // Value::String("Hello World2".to_owned()), - // ); - - // let tags2: Tags = serde_json::from_value(Value::Object(map)).unwrap(); - // dbg!(tags2); - // } + use crate::domain::score::ToScore; #[test] fn test_parse_epd_tags() { - let s = r#"acd 1000; bm e4; ce 123; draw_reject; id "TEST CASE.1";"#; + let s = r#"Mce 1,3,-14; acd 1000; bm e4; ce 123; draw_reject; id "TEST CASE.1";"#; let tags = Tags::parse_epd_tags(&Board::starting_pos(), s).unwrap(); + println!("{tags:#?}"); assert_eq!(tags.get("acd"), Some("1000")); assert_eq!(tags.get("bm"), Some("e4")); assert_eq!(tags.score(), Some(Score::from_cp(123))); assert_eq!(tags.get("draw_reject"), Some("")); assert_eq!(tags.get("id"), Some("TEST CASE.1")); + assert_eq!(tags.get(Tags::MCE), Some("1,3,-14")); + assert_eq!(tags.score_for_depth(0), Some(1.cp())); + assert_eq!(tags.score_for_depth(1), Some(3.cp())); + assert_eq!(tags.score_for_depth(2), Some((-14).cp())); + assert_eq!(tags.score_for_depth(3), None); assert_eq!(format!("{tags}"), " ".to_string() + s); } diff --git a/crates/odonata-base/src/piece.rs b/crates/odonata-base/src/piece.rs index 5019bab8..a38956f6 100644 --- a/crates/odonata-base/src/piece.rs +++ b/crates/odonata-base/src/piece.rs @@ -246,6 +246,7 @@ pub enum Piece { King, } + impl From for Piece { fn from(u: u8) -> Self { Piece::from_index(u as usize) @@ -356,17 +357,30 @@ impl Piece { #[inline] pub fn from_char(ch: char) -> Result { - Ok(match ch.to_ascii_uppercase() { - 'P' => Piece::Pawn, - 'N' => Piece::Knight, - 'B' => Piece::Bishop, - 'R' => Piece::Rook, - 'Q' => Piece::Queen, - 'K' => Piece::King, - _ => bail!("Unknown piece '{}'", ch), + Ok(Self::and_color_from_char(ch.to_ascii_uppercase())?.0) + } + + #[inline] + pub fn and_color_from_char(ch: char) -> Result<(Piece, Color)> { + Ok(match ch { + 'P' => (Piece::Pawn, Color::White), + 'N' => (Piece::Knight, Color::White), + 'B' => (Piece::Bishop, Color::White), + 'R' => (Piece::Rook, Color::White), + 'Q' => (Piece::Queen, Color::White), + 'K' => (Piece::King, Color::White), + 'p' => (Piece::Pawn, Color::Black), + 'n' => (Piece::Knight, Color::Black), + 'b' => (Piece::Bishop, Color::Black), + 'r' => (Piece::Rook, Color::Black), + 'q' => (Piece::Queen, Color::Black), + 'k' => (Piece::King, Color::Black), + _ => bail!("Unknown color/piece '{}'", ch), }) } + + #[inline] pub const fn to_char(&self, c: Color) -> char { match c { diff --git a/crates/odonata-base/src/variation.rs b/crates/odonata-base/src/variation.rs index cf25de82..25ecdb16 100644 --- a/crates/odonata-base/src/variation.rs +++ b/crates/odonata-base/src/variation.rs @@ -33,7 +33,7 @@ impl Default for Variation { #[inline] fn default() -> Self { Self { - moves: Vec::with_capacity(60), + moves: Vec::new(), } } } diff --git a/crates/odonata-engine/Cargo.toml b/crates/odonata-engine/Cargo.toml index 51911e7a..e39f7369 100644 --- a/crates/odonata-engine/Cargo.toml +++ b/crates/odonata-engine/Cargo.toml @@ -42,6 +42,7 @@ strum_macros.workspace = true strum.workspace = true tabled.workspace = true tabwriter.workspace = true +thread_local.workspace = true test-log.workspace = true toml.workspace = true tracing.workspace = true diff --git a/crates/odonata-engine/resources/r61-net.i16.bin b/crates/odonata-engine/resources/r61-net.i16.bin deleted file mode 100644 index 1182c6fd..00000000 Binary files a/crates/odonata-engine/resources/r61-net.i16.bin and /dev/null differ diff --git a/crates/odonata-engine/src/bin/odonata.rs b/crates/odonata-engine/src/bin/odonata.rs index ec82d159..e705ef0f 100644 --- a/crates/odonata-engine/src/bin/odonata.rs +++ b/crates/odonata-engine/src/bin/odonata.rs @@ -47,7 +47,7 @@ enum Cmd { Bench, /// Show uci settings and other configuration - ShowConfig, + Config, /// Execute a series of uci commands Uci { command: String }, @@ -99,7 +99,7 @@ pub fn main() -> anyhow::Result<()> { uci.strict_error_handling = cli.strict; match cli.command.unwrap_or(Cmd::Engine) { - Cmd::ShowConfig => uci.add_prelude("uci; show_config; quit").run(), + Cmd::Config => uci.add_prelude("uci; config; quit").run(), Cmd::Bench => uci .add_prelude("position startpos; go depth 11; isready; bench; quit") .run(), diff --git a/crates/odonata-engine/src/comms/bench.rs b/crates/odonata-engine/src/comms/bench.rs index 442c0fb4..017bac37 100644 --- a/crates/odonata-engine/src/comms/bench.rs +++ b/crates/odonata-engine/src/comms/bench.rs @@ -50,7 +50,7 @@ impl Bench { let nps = Formatting::f64(res.nodes as f64 / elapsed.as_secs_f64()); let bf = res.bf; let bf_string = Formatting::decimal(2, bf); - let fen = res.to_results_epd().board().to_fen(); + let fen = res.to_epd().board().to_fen(); total_bf += bf; total_time += elapsed; total_nodes += res.nodes; @@ -103,14 +103,14 @@ mod tests { s.parse() .unwrap_or_else(|_| panic!("RUST_BENCH_TC not a valid time control: {s}")) } else { - TimeControl::NodeCount(1000) + TimeControl::NodeCount(100000) }; let mut prof = PerfProfiler::new("bench_bratko_approx"); prof.bench(|| total_nodes += Bench::search(tc.clone(), None, HashMap::new()).unwrap()); prof.set_iters(total_nodes / 1000); // total number of searches - let mut prof_accurate = PerfProfiler::new("bench.bratko"); + let mut prof_accurate = PerfProfiler::new("bench_bratko"); let mut engine = ThreadedSearch::new(); let mut nodes = 0; diff --git a/crates/odonata-engine/src/comms/uci_server.rs b/crates/odonata-engine/src/comms/uci_server.rs index a92cf94a..79a217d7 100644 --- a/crates/odonata-engine/src/comms/uci_server.rs +++ b/crates/odonata-engine/src/comms/uci_server.rs @@ -223,7 +223,7 @@ impl UciServer { "compiler" => self.uci_compiler(), "show_options" => self.uci_show_options(), "metrics" => self.uci_metrics(&Args::parse(&input)), - "show_config" => self.ext_uci_show_config(), + "config" => self.ext_uci_show_config(), "eval" | "." => self.ext_uci_explain_eval(), "explain_last_search" | "?" => self.uci_explain_last_search(), @@ -606,6 +606,8 @@ impl UciServer { self.engine.lock().unwrap().search_stop(); let engine = self.engine.lock().unwrap(); let cfg = &engine.show_config()?; + let cfg = cfg.replace("},\n", "\n"); + let cfg = cfg.replace("{\n", "\n"); Self::print(&format!("# start config\n{cfg}")); Self::print(&format!("# end config\n")); Ok(()) diff --git a/crates/odonata-engine/src/eval/hce.rs b/crates/odonata-engine/src/eval/hce.rs index 5c705c52..f033301f 100644 --- a/crates/odonata-engine/src/eval/hce.rs +++ b/crates/odonata-engine/src/eval/hce.rs @@ -3,12 +3,9 @@ use std::fmt; use std::path::PathBuf; use odonata_base::boards::Position; -use odonata_base::domain::node::{Counter, Event, Node}; use odonata_base::domain::staticeval::{EvalExplain, StaticEval}; use odonata_base::eg::endgame::EndGameScoring; use odonata_base::infra::component::{Component, State}; -use odonata_base::infra::lockless_hashmap::UnsharedTable; -use odonata_base::infra::metric::Metrics; use odonata_base::other::{Phase, Phaser}; use odonata_base::prelude::*; use once_cell::unsync::OnceCell; @@ -31,14 +28,12 @@ pub struct Hce { rounding: Rounding, mobility_phase_disable: u8, pub quantum: i32, - cache_size: usize, draw_scaling: f32, draw_scaling_noisy: f32, see: See, pub endgame: EndGameScoring, pub phaser: Phaser, weights_raw: Softcoded, - eval_cache: UnsharedTable, weights_i32: OnceCell>, weights_f64: OnceCell>, weights_f32: OnceCell>, @@ -46,7 +41,6 @@ pub struct Hce { impl Default for Hce { fn default() -> Self { - const DEFAULT_CACHE_SIZE: usize = 10_000; let hce_file = "eval.hce.toml"; Self { hce_file: hce_file.into(), @@ -64,8 +58,6 @@ impl Default for Hce { phasing: true, mobility_phase_disable: 101, quantum: 1, - cache_size: DEFAULT_CACHE_SIZE, - eval_cache: UnsharedTable::with_size(DEFAULT_CACHE_SIZE), } } } @@ -80,13 +72,11 @@ impl Configurable for Hce { self.rounding.set(p.get("rounding"))?; self.mobility_phase_disable.set(p.get("mobility_phase_disable"))?; self.quantum.set(p.get("quantum"))?; - self.cache_size.set(p.get("cache_size"))?; self.draw_scaling.set(p.get("draw_scaling"))?; self.draw_scaling_noisy.set(p.get("draw_scaling_noisy"))?; self.see.set(p.get("see"))?; self.endgame.set(p.get("endgame"))?; self.phaser.set(p.get("phaser"))?; - self.eval_cache.set(p.get("eval_cache"))?; Ok(p.is_modified()) } } @@ -232,10 +222,6 @@ impl StaticEval for Hce { } fn new_game(&mut self) { - self.eval_cache = UnsharedTable::with_size(self.cache_size); - // self.mb.new_game(); - // self.pawn_cache.clear(); - self.eval_cache.clear(); } fn eval_draw(&self, _b: &Board, _ply: Ply) -> Score { @@ -248,11 +234,6 @@ impl Component for Hce { use State::*; match s { NewGame => { - self.eval_cache = UnsharedTable::with_size(self.cache_size); - // self.mb.new_game(); - self.phaser.new_game(); - // self.pawn_cache.clear(); - self.eval_cache.clear(); } SetPosition => { // self.mb.new_position(); @@ -278,12 +259,9 @@ impl fmt::Display for Hce { impl fmt::Debug for Hce { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "cache size : {}", self.cache_size)?; - writeln!(f, "eval_cache : {}", self.eval_cache)?; writeln!(f, "draw scaling : {}", self.draw_scaling)?; writeln!(f, "rounding : {}", self.rounding)?; writeln!(f, "weights kind : {}", self.weights_kind)?; - writeln!(f, "utilization (‰) : {}", self.eval_cache.hashfull_per_mille())?; // writeln!(f, "[material balance]\n{}", self.mb)?; writeln!(f, "[phaser]\n{}", self.phaser)?; writeln!(f, "phasing : {}", self.phasing)?; @@ -458,33 +436,7 @@ impl Hce { } fn w_eval_some(&self, b: &Board) -> Score { - if self.cache_size == 0 { - return self.w_eval_no_cache(b); - } - - if let Some(score) = self.eval_cache.probe(b.hash()) { - Metrics::incr(Counter::EvalCacheHit); - Metrics::incr_node( - &Node { - ply: b.ply(), - ..Node::default() - }, - Event::EvalCacheHit, - ); - score - } else { - Metrics::incr(Counter::EvalCacheMiss); - Metrics::incr_node( - &Node { - ply: b.ply(), - ..Node::default() - }, - Event::EvalCacheMiss, - ); - let s = self.w_eval_no_cache(b); - self.eval_cache.store(b.hash(), s); - s - } + self.w_eval_no_cache(b) } // /// the value of the capture or promotion (or both for promo capture) @@ -500,6 +452,7 @@ mod tests { use std::hint::black_box; use odonata_base::catalog::Catalog; + use odonata_base::domain::node::Node; use odonata_base::infra::profiler::*; use test_log::test; diff --git a/crates/odonata-engine/src/eval/mod.rs b/crates/odonata-engine/src/eval/mod.rs index 6412ac9c..e47ae253 100644 --- a/crates/odonata-engine/src/eval/mod.rs +++ b/crates/odonata-engine/src/eval/mod.rs @@ -1,11 +1,12 @@ -use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; +use std::sync::Arc; +use network::Bucket; use odonata_base::boards::Position; use odonata_base::domain::staticeval::{EvalExplain, StaticEval}; use odonata_base::eg::EndGame; -use odonata_base::infra::utils::Formatting; +use odonata_base::infra::lockless_hashmap::UnsharedTable; use odonata_base::prelude::*; use strum_macros::{Display, EnumString}; @@ -29,11 +30,13 @@ pub mod weight; #[derive(Debug, Clone)] pub struct Eval { - pub eval_kind: EvalKind, - pub hce: Box, - pub nnue_file: PathBuf, - pub nnue: Box, - pub incremental: bool, + pub eval_kind: EvalKind, + pub hce: Box, + pub nnue_file: PathBuf, + pub nnue: Arc, + pub incremental: bool, + pub fixed_bucket: i32, + pub eval_cache: UnsharedTable, } // impl Clone for Eval { @@ -58,12 +61,15 @@ pub enum EvalKind { impl Default for Eval { fn default() -> Self { + const DEFAULT_CACHE_SIZE: usize = 10_000; Self { - eval_kind: EvalKind::Blend, - hce: Default::default(), - nnue: Box::new(Nnue::from_file("").expect("unable to load")), - incremental: true, - nnue_file: PathBuf::new(), + eval_kind: EvalKind::Nnue, + hce: Default::default(), + nnue: Arc::new(Nnue::from_file("").expect("unable to load")), + incremental: true, + fixed_bucket: -1, + nnue_file: PathBuf::new(), + eval_cache: UnsharedTable::with_size(DEFAULT_CACHE_SIZE), } } } @@ -85,13 +91,21 @@ impl Display for Eval { ), }; write!(f, "{}: {}", self.eval_kind, delegate)?; + writeln!(f, "utilization (‰) : {}", self.eval_cache.hashfull_per_mille())?; Ok(()) } } impl Eval { pub fn reload(&mut self) -> Result<()> { - self.nnue = Box::new(Nnue::from_file(&self.nnue_file)?); + let mut nnue = Nnue::from_file(&self.nnue_file)?; + if self.fixed_bucket >= 0 { + match &mut nnue { + Nnue::Nnue768Float(net) => net.bucket = Some(Bucket::Constant(self.fixed_bucket as usize)), + Nnue::Nnue768(net) => net.bucket = Some(Bucket::Constant(self.fixed_bucket as usize)), + }; + } + self.nnue = Arc::new(nnue); // self.hce.reload_weights()?; self.new_game(); Ok(()) @@ -102,10 +116,14 @@ impl Configurable for Eval { fn set(&mut self, p: Param) -> Result { self.eval_kind.set(p.get("eval_kind"))?; self.incremental.set(p.get("incremental"))?; - self.hce.set(p.get("hce"))?; + if self.fixed_bucket.set(p.get("fixed_bucket"))? { + self.reload()?; + }; if self.nnue_file.set(p.get("nnue_file"))? { self.reload()?; }; + self.hce.set(p.get("hce"))?; + self.eval_cache.set(p.get("eval_cache"))?; Ok(p.is_modified()) } } @@ -113,30 +131,36 @@ impl Configurable for Eval { impl StaticEval for Eval { fn new_game(&mut self) { self.nnue.new_game(); + self.eval_cache.clear(); } fn static_eval(&self, eval_pos: &Position) -> Score { - if self.eval_kind == EvalKind::Hce { - return self.hce.static_eval(eval_pos); - } - - let cp = match self.incremental { - true => self.nnue.eval(eval_pos), - false => self.nnue.eval_stateless(eval_pos.board()), - } as i32; - // let hce = self.hce.static_eval(eval_pos).as_i16() as i32; - let sc = if self.eval_kind == EvalKind::Blend { - let material = eval_pos.board().material().centipawns_as_white(); - let material = eval_pos.board().turn().chooser_wb(1, -1) * material; - let wt = WeightOf::::new(cp, material / 3 + 5 * cp / 6); // 4/5 better - wt.interpolate(eval_pos.board().phase(&self.hce.phaser)) + let eval = if let Some(eval) = self.eval_cache.probe(eval_pos.board().hash()) { + eval } else { - cp + let eval = if self.eval_kind == EvalKind::Hce { + self.hce.static_eval(eval_pos) + } else { + let cp = match self.incremental { + true => self.nnue.eval(eval_pos.board()), + false => self.nnue.eval(eval_pos.board()), + } as i32; + // let hce = self.hce.static_eval(eval_pos).as_i16() as i32; + let sc = if self.eval_kind == EvalKind::Blend { + let material = eval_pos.board().material().centipawns_as_white(); + let material = eval_pos.board().turn().chooser_wb(1, -1) * material; + let wt = WeightOf::::new(cp, material / 3 + 5 * cp / 6); // 4/5 better + wt.interpolate(eval_pos.board().phase(&self.hce.phaser)) + } else { + cp + }; + Score::from_cp(sc) + }; + self.eval_cache.store(eval_pos.board().hash(), eval); + eval }; - let pov = Score::from_cp(sc); - let eg = EndGame::from_board(eval_pos.board()); - eg.endgame_score_adjust(eval_pos.board(), pov, &self.hce.endgame) + eg.endgame_score_adjust(eval_pos.board(), eval, &self.hce.endgame) } fn static_eval_explain(&self, pos: &Position) -> EvalExplain { @@ -144,25 +168,13 @@ impl StaticEval for Eval { return self.hce.static_eval_explain(pos); } - let mut cells = HashMap::new(); - for sq in pos.board.occupied().squares() { - let p = pos.board().piece(sq).unwrap(); - let c = pos.board().color_of(sq).unwrap(); - let score1 = self.nnue.eval_stateless(pos.board()); - let mut bb = pos.board().clone().into_builder(); - bb.set_piece(sq, None); - let score2 = self.nnue.eval_stateless(&bb.build()); - let cp = score1 - score2; - let key = (7 - sq.rank_index(), sq.file_index()); - cells.insert(key, format!("\n{p:^9}\n\n{cp:^9}\n", p = p.to_char(c))); - } - let t = Formatting::to_table(cells, "\n\n\n\n\n"); + let t = self.nnue.explain(pos.board()); println!("{t}"); use std::fmt::Write; let mut e = EvalExplain::default(); - let cp = self.nnue.eval(pos) as i32; + let cp = self.nnue.eval(pos.board()) as i32; let pov = Score::from_cp(cp); let sc = self.static_eval(pos); diff --git a/crates/odonata-engine/src/eval/network.rs b/crates/odonata-engine/src/eval/network.rs index ec7e8026..8155f37b 100644 --- a/crates/odonata-engine/src/eval/network.rs +++ b/crates/odonata-engine/src/eval/network.rs @@ -1,52 +1,147 @@ use std::any::type_name; +use std::array::from_fn; +use std::cell::RefCell; +use std::collections::HashMap; use std::fmt::{Debug, Display}; -use std::io::{Read, Write}; -use std::ops::{AddAssign, Mul, Neg}; -use std::path::Path; +use std::io::Write; +use std::ops::{AddAssign, Mul, Neg, Sub}; use num_traits::MulAdd; use odonata_base::infra::math::Quantize; -use odonata_base::infra::utils::{self}; +use odonata_base::infra::metric::AtomicMean; +use odonata_base::infra::utils::{self, Formatting}; use odonata_base::prelude::*; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use simba::scalar::RealField; +use thread_local::ThreadLocal; use super::vector::Vector; +macro_rules! NET_PATH { + () => { + "../../resources/r87-net.i16.2.bin" + }; +} + +pub fn net_path() -> &'static [u8] { + static BUF: &[u8] = include_bytes!(NET_PATH!()); + BUF +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub enum Bucket { + Constant(usize), + Majors, + #[default] + QueenPhase, +} + +impl Bucket { + pub fn calc(&self, bd: &Board) -> usize { + match self { + Bucket::Constant(u) => *u, + Bucket::Majors => ((bd.occupied() - bd.pawns()).popcount() / 4).clamp(0, 4) as usize, + Bucket::QueenPhase => match ( + bd.queens().popcount(), + (bd.rooks_or_queens() | bd.bishops() | bd.knights()).popcount(), + ) { + (q, p) if p >= 2 + 5 && q >= 2 => 4, + (_, p) if p >= 2 + 5 => 3, + (_, p) if p >= 5 => 2, + (_, p) if p >= 3 => 1, + (_, _p) => 0, + }, + } + } +} + +pub const BUCKETS_768H2: usize = 8; + pub trait Network { + const BUCKETS: usize; type Accumulators: Clone; type Input; - type Output; + type Output: Sub + Display; fn new_accumulators(&self) -> Self::Accumulators; - fn forward1_input(&self, wb: &mut Self::Accumulators, bd1: &Board, bd2: &Board); + fn forward1_relative(&self, wb: &mut Self::Accumulators, prior: &Board, board: &Board); fn forward1(&self, acc: &mut Self::Accumulators, b: &Board); - fn forward2(&self, pov: Color, state: &Self::Accumulators) -> Self::Output; + fn forward2(&self, state: &Self::Accumulators, b: &Board) -> Self::Output; fn predict(&self, bd: &Board) -> Self::Output { let mut accs = self.new_accumulators(); self.forward1(&mut accs, bd); - self.forward2(bd.turn(), &accs) + self.forward2(&accs, bd) + } + + fn explain(&self, bd: &Board) -> String; + + fn explain_helper(&self, bd: &Board) -> String { + let mut cells = HashMap::new(); + for sq in bd.occupied().squares() { + let p = bd.piece(sq).unwrap(); + let c = bd.color_of(sq).unwrap(); + let score1 = self.predict(bd); + let mut bb = bd.clone().into_builder(); + bb.set_piece(sq, None); + let score2 = self.predict(&bb.build()); + let cp = score1 - score2; + let key = (7 - sq.rank_index(), sq.file_index()); + cells.insert(key, format!("\n{p:^9}\n\n{cp:^9}\n", p = p.to_char(c))); + } + let t = Formatting::to_table(cells, "\n\n\n\n\n"); + t.to_string() } } -#[derive(PartialEq, Default, Clone, Serialize, Deserialize)] +pub static METRIC_BOARD_DELTA: AtomicMean = AtomicMean::new(); +pub static METRIC_BOARD_OCCUPANCY: AtomicMean = AtomicMean::new(); + +#[derive(Default, Serialize, Deserialize)] +#[serde(bound = "Vector: Serialize + DeserializeOwned")] #[repr(C, align(64))] -pub struct Network768xH2 { +pub struct Network768xH2 { pub n_features: usize, pub n_hidden_layer: usize, pub wt: Vec>, // (acc = hl_size) * input_size pub bi: Vector, - pub h1_wt: [Vector; 2], // hl_size - pub h1_bi: Vector, // size 1 + pub h1_wt: [[Vector; 2]; BUCKETS_768H2], // hl_size + pub h1_bi: [Vector; BUCKETS_768H2], // size 1 + #[serde(default)] - pub description: String, + pub description: String, + + #[serde(skip)] + board: ThreadLocal>, + + #[serde(skip)] + accumulator: ThreadLocal; 2]>>, + + #[serde(skip)] + pub bucket: Option, +} + +impl PartialEq for Network768xH2 +where + T: Send + PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.n_features == other.n_features + && self.n_hidden_layer == other.n_hidden_layer + && self.wt == other.wt + && self.bi == other.bi + && self.h1_wt == other.h1_wt + && self.h1_bi == other.h1_bi + && self.description == other.description + // && self.board == other.board + // && self.accumulator == other.accumulator + } } impl Display for Network768xH2 where - T: Copy + Default, + T: Send, Vector: Debug, { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -62,50 +157,50 @@ where writeln!(f, "wt[{i}] : {:?}", self.wt[i])?; } writeln!(f, "bi : {:?}", self.bi)?; - writeln!(f, "h1[0] : {:?}", self.h1_wt[0])?; - writeln!(f, "h1[1] : {:?}", self.h1_wt[1])?; - writeln!(f, "h1_bi : {:?}", self.h1_bi)?; + writeln!(f, "h1[0][0] : {:?}", self.h1_wt[0][0])?; + writeln!(f, "h1[0][1] : {:?}", self.h1_wt[0][1])?; + writeln!(f, "h1_bi[0] : {:?}", self.h1_bi[0])?; Ok(()) } } -#[inline(always)] -pub fn crelu(value: T) -> T { - T::clamp(value, T::zero(), T::one()) -} - -impl Network768xH2 { - pub fn from_file(path: impl AsRef) -> Result> { - let path = path.as_ref().to_string_lossy().into_owned(); - // if path.as_ref().to_str() = Some("") {} - let mut buf = Vec::new(); - if path.is_empty() { - debug!(target: "config", "loading nnue from default location"); - let buf = include_bytes!("../../resources/r61-net.i16.bin"); - buf.as_slice(); - NetworkLoader::read_postcard_format(buf) - } else if path.ends_with("i16.yaml") { - let net = serde_yaml::from_reader(file_open(&path)?).context(path)?; - Ok(Box::new(net)) - } else { - debug!(target: "config", "loading binary nnue from {}", path); - let _bytes = utils::file_open(&path).unwrap().read_to_end(&mut buf).unwrap(); - NetworkLoader::read_postcard_format(buf.as_slice()) +impl Network768xH2 { + pub fn new(n_features: usize, n_hidden_layer: usize) -> Self { + Self { + n_features, + n_hidden_layer, + description: String::new(), + wt: vec![Vector::new(n_hidden_layer); n_features], + bi: Vector::new(n_hidden_layer), + h1_wt: from_fn(|_| [Vector::new(n_hidden_layer), Vector::new(n_hidden_layer)]), + h1_bi: from_fn(|_| Vector::new(1)), + board: Default::default(), + accumulator: Default::default(), // bucket = 0 for empty (default) board + bucket: Some(Bucket::QueenPhase), } } } -impl Network768xH2 { - pub fn from_file(path: impl AsRef) -> Result> { - let path = path.as_ref(); - if path.to_string_lossy().ends_with(".yaml") { - Ok(serde_yaml::from_reader(file_open(path)?)?) - } else { - anyhow::bail!("unknown file formal {path:?}"); - } +impl Debug for Network768xH2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Network768xH2") + .field("inbuilt", &NET_PATH!()) + .field("n_features", &self.n_features) + .field("n_hidden_layer", &self.n_hidden_layer) + .finish() } } +#[inline(always)] +pub fn crelu(value: T) -> T { + T::clamp(value, T::zero(), T::one()) +} + +#[inline(always)] +fn crelu_i16(x: i16) -> i32 { + (x as i32).clamp(0, 255) +} + /// we accumulate for both perspectives: w+b /// /// the feature index is w: 0...384, b: 384..768 @@ -142,15 +237,19 @@ pub fn feature768_diff_iter(b1: &Board, b2: &Board) -> impl Iterator Network768xH2 where - T: Clone + Default + Copy + Mul + MulAdd + AddAssign + Neg + num_traits::One, + T: Send + Default + Copy + Mul + MulAdd + AddAssign + Neg + num_traits::One, Vector: for<'a> AddAssign<&'a Vector>, - Self: for<'a> Deserialize<'a>, + Self: DeserializeOwned, { - fn new_accumulators(&self) -> (Vector, Vector) { - (self.bi.clone(), self.bi.clone()) + fn new_accumulators(&self) -> [Vector; 2] { + [self.bi.clone(), self.bi.clone()] } - fn forward1(&self, (w, b): &mut (Vector, Vector), bd: &Board) { + pub fn bucket_for_board(&self, bd: &Board) -> usize { + self.bucket.as_ref().unwrap_or(&Bucket::default()).calc(bd) + } + + fn forward1(&self, [w, b]: &mut [Vector; 2], bd: &Board) { for sq in bd.occupied().squares() { let p = bd.piece_unchecked(sq); let c = bd.color_of(sq).unwrap(); @@ -158,19 +257,25 @@ where *w += &self.wt[wf]; *b += &self.wt[bf]; } + METRIC_BOARD_OCCUPANCY.add(bd.occupied().popcount() as i64); } - fn forward1_input(&self, (w, b): &mut (Vector, Vector), bd1: &Board, bd2: &Board) { + fn forward1_relative(&self, [w, b]: &mut [Vector; 2], bd1: &Board, bd2: &Board) { + let mut deltas = 0; for (p, sq, c) in feature768_diff_iter(bd1, bd2) { + deltas += 1; let (wi, bi) = feature768(p, sq, c); w.mul_add_assign(T::one(), &self.wt[wi]); b.mul_add_assign(T::one(), &self.wt[bi]); } for (p, sq, c) in feature768_diff_iter(bd2, bd1) { + deltas += 1; let (wi, bi) = feature768(p, sq, c); w.mul_add_assign(-T::one(), &self.wt[wi]); b.mul_add_assign(-T::one(), &self.wt[bi]); } + // if tracing::enabled!(target:"metrics", tracing::Level::TRACE) { + METRIC_BOARD_DELTA.add(deltas); } } @@ -189,11 +294,12 @@ where } q.bi = (&self.bi * factors[0]).quantize()?; - q.h1_wt[0] = (&self.h1_wt[0] * factors[1]).quantize()?; - q.h1_wt[1] = (&self.h1_wt[1] * factors[1]).quantize()?; - - // compare with a wt of 1 and this would be scaled by factors[0] - q.h1_bi = (&self.h1_bi * (factors[0] * factors[1])).quantize()?; + for b in 0..BUCKETS_768H2 { + q.h1_wt[b][0] = (&self.h1_wt[b][0] * factors[1]).quantize()?; + q.h1_wt[b][1] = (&self.h1_wt[b][1] * factors[1]).quantize()?; + // compare with a wt of 1 and this would be scaled by factors[0] + q.h1_bi[b] = (&self.h1_bi[b] * (factors[0] * factors[1])).quantize()?; + } Ok(q) } @@ -202,20 +308,24 @@ where pub type Float = f32; impl Network for Network768xH2 { - type Accumulators = (Vector, Vector); // white, black + const BUCKETS: usize = BUCKETS_768H2; + type Accumulators = [Vector; 2]; // white, black type Input = Float; type Output = Float; - fn forward2(&self, pov: Color, (w, b): &Self::Accumulators) -> Self::Output { - let mut output = self.h1_bi.get(0); + fn forward2(&self, [w, b]: &Self::Accumulators, bd: &Board) -> Self::Output { + let pov = bd.turn(); + let u = self.bucket_for_board(bd); + + let mut output = self.h1_bi[u].get(0); match pov { Color::White => { - w.apply_zip(&self.h1_wt[0], |x, y| output += crelu(*x) * *y); - b.apply_zip(&self.h1_wt[1], |x, y| output += crelu(*x) * *y); + w.apply_zip(&self.h1_wt[u][0], |x, y| output += crelu(*x) * *y); + b.apply_zip(&self.h1_wt[u][1], |x, y| output += crelu(*x) * *y); } Color::Black => { - b.apply_zip(&self.h1_wt[0], |x, y| output += crelu(*x) * *y); - w.apply_zip(&self.h1_wt[1], |x, y| output += crelu(*x) * *y); + b.apply_zip(&self.h1_wt[u][0], |x, y| output += crelu(*x) * *y); + w.apply_zip(&self.h1_wt[u][1], |x, y| output += crelu(*x) * *y); } } output * 400. @@ -225,36 +335,60 @@ impl Network for Network768xH2 { self.new_accumulators() } - fn forward1_input(&self, wb: &mut Self::Accumulators, bd1: &Board, bd2: &Board) { - self.forward1_input(wb, bd1, bd2) + fn forward1_relative(&self, wb: &mut Self::Accumulators, bd1: &Board, bd2: &Board) { + self.forward1_relative(wb, bd1, bd2) } fn forward1(&self, acc: &mut Self::Accumulators, b: &Board) { self.forward1(acc, b) } -} -#[inline(always)] -fn crelu_i16(x: i16) -> i32 { - (x as i32).clamp(0, 255) + fn predict(&self, b: &Board) -> Self::Output { + let acc = self.accumulator.get_or(|| RefCell::new(self.new_accumulators())); + let prior = self.board.get_or_default(); + self.forward1_relative(&mut acc.borrow_mut(), &prior.borrow(), b); + *prior.borrow_mut() = b.clone(); + self.forward2(&acc.borrow(), b) + } + + fn explain(&self, b: &Board) -> String { + let t = Network::explain_helper(self, b); + let bucket = self.bucket_for_board(b); + format!("{t}\nBucket: {bucket}") + } } impl Network for Network768xH2 { - type Accumulators = (Vector, Vector); // white, black + const BUCKETS: usize = BUCKETS_768H2; + type Accumulators = [Vector; 2]; // white, black type Input = i16; type Output = i16; - fn forward2(&self, pov: Color, (w, b): &Self::Accumulators) -> Self::Output { - let mut output = self.h1_bi.get(0) as i32; + fn new_accumulators(&self) -> Self::Accumulators { + self.new_accumulators() + } + + fn forward1(&self, acc: &mut Self::Accumulators, b: &Board) { + self.forward1(acc, b) + } + + fn forward1_relative(&self, wb: &mut Self::Accumulators, bd1: &Board, bd2: &Board) { + self.forward1_relative(wb, bd1, bd2) + } + + fn forward2(&self, [w, b]: &Self::Accumulators, bd: &Board) -> Self::Output { + let pov = bd.turn(); + let u = self.bucket_for_board(bd); + let mut output = self.h1_bi[u].get(0) as i32; match pov { Color::White => { - w.apply_zip(&self.h1_wt[0], |x, y| output += crelu_i16(*x) * *y as i32); - b.apply_zip(&self.h1_wt[1], |x, y| output += crelu_i16(*x) * *y as i32); + w.apply_zip(&self.h1_wt[u][0], |x, y| output += crelu_i16(*x) * *y as i32); + b.apply_zip(&self.h1_wt[u][1], |x, y| output += crelu_i16(*x) * *y as i32); } Color::Black => { - b.apply_zip(&self.h1_wt[0], |x, y| output += crelu_i16(*x) * *y as i32); - w.apply_zip(&self.h1_wt[1], |x, y| output += crelu_i16(*x) * *y as i32); + b.apply_zip(&self.h1_wt[u][0], |x, y| output += crelu_i16(*x) * *y as i32); + w.apply_zip(&self.h1_wt[u][1], |x, y| output += crelu_i16(*x) * *y as i32); } } output *= 400; @@ -262,39 +396,18 @@ impl Network for Network768xH2 { output as i16 } - fn new_accumulators(&self) -> Self::Accumulators { - self.new_accumulators() - } - - fn forward1_input(&self, wb: &mut Self::Accumulators, bd1: &Board, bd2: &Board) { - self.forward1_input(wb, bd1, bd2) - } - - fn forward1(&self, acc: &mut Self::Accumulators, b: &Board) { - self.forward1(acc, b) - } -} - -impl Network768xH2 { - pub fn new(n_features: usize, n_hidden_layer: usize) -> Self { - Self { - n_features, - n_hidden_layer, - description: String::new(), - wt: vec![Vector::new(n_hidden_layer); n_features], - bi: Vector::new(n_hidden_layer), - h1_wt: [Vector::new(n_hidden_layer), Vector::new(n_hidden_layer)], - h1_bi: Vector::new(1), - } + fn predict(&self, b: &Board) -> Self::Output { + let acc = self.accumulator.get_or(|| RefCell::new(self.new_accumulators())); + let prior = self.board.get_or_default(); + self.forward1_relative(&mut acc.borrow_mut(), &prior.borrow(), b); + b.clone_into(&mut prior.borrow_mut()); + self.forward2(&acc.borrow(), b) } -} -impl Debug for Network768xH2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Network768xH2") - .field("n_features", &self.n_features) - .field("n_hidden_layer", &self.n_hidden_layer) - .finish() + fn explain(&self, bd: &Board) -> String { + let t = Network::explain_helper(self, bd); + let bucket = self.bucket_for_board(bd); + format!("{t}\nBucket: {bucket}") } } @@ -312,6 +425,25 @@ impl NetworkLoader { } } +// impl Network768xH2 { +// pub fn from_file(path: impl AsRef) -> Result> { +// let path = path.as_ref().to_string_lossy().into_owned(); +// // if path.as_ref().to_str() = Some("") {} +// let mut buf = Vec::new(); +// if path.is_empty() { +// debug!(target: "config", "loading nnue from default location"); +// NetworkLoader::read_postcard_format(net_path()) +// } else if path.ends_with("i16.yaml") { +// let net = serde_yaml::from_reader(file_open(&path)?).context(path)?; +// Ok(Box::new(net)) +// } else { +// debug!(target: "config", "loading binary nnue from {}", path); +// let _bytes = utils::file_open(&path).unwrap().read_to_end(&mut buf).unwrap(); +// NetworkLoader::read_postcard_format(buf.as_slice()) +// } +// } +// } + #[cfg(test)] mod tests { use std::fs::read_to_string; @@ -358,27 +490,27 @@ mod tests { println!("sub {p} {sq} {c}"); } - let mut acc2 = net.new_accumulators(); - let sc2a = prof_abs.bench(|| { - net.forward1(&mut acc2, &bd2); - net.forward2(bd2.turn(), &acc2) - }); - - let mut acc1 = net.new_accumulators(); - net.forward1(&mut acc1, &bd1); - let sc2b = prof_rel.bench(|| { - net.forward1_input(&mut acc1, &bd1, &bd2); - net.forward2(bd2.turn(), &acc1) - }); - assert_eq!(sc2a, sc2b); - prof_fw1.bench(|| { - net.forward1(&mut acc1, &bd1); - }); - prof_fw2.bench(|| net.forward2(bd1.turn(), &acc1)); + for epd in Catalog::example_game() { + let board = epd.board(); + let sc2a = prof_abs.bench(|| { + let mut acc2 = net.new_accumulators(); + net.forward1(&mut acc2, &board); + net.forward2(&acc2, &board) + }); + let sc2b = prof_rel.bench(|| net.predict(&board)); + println!("{epd} = {sc2a}"); + assert_eq!(sc2a, sc2b, "{epd}"); + + let mut acc1 = net.new_accumulators(); + prof_fw1.bench(|| net.forward1(&mut acc1, &board)); + prof_fw2.bench(|| net.forward2(&acc1, &board)); + } + println!("delta board changes: {:.2}", METRIC_BOARD_DELTA.mean()); + println!("abs board changes : {:.2}", METRIC_BOARD_OCCUPANCY.mean()); } fn network_fixture() -> Box> { - let file = "../../crates/odonata-engine/resources/r61-net.i16.bin"; + let file = "../../crates/odonata-engine/resources/r81-net.i16.2.bin"; let mut buf = Vec::new(); let _bytes = file_open(file).unwrap().read_to_end(&mut buf).unwrap(); NetworkLoader::read_postcard_format(&buf).unwrap() diff --git a/crates/odonata-engine/src/eval/nnue.rs b/crates/odonata-engine/src/eval/nnue.rs index bb7ac8a6..532c1785 100644 --- a/crates/odonata-engine/src/eval/nnue.rs +++ b/crates/odonata-engine/src/eval/nnue.rs @@ -1,143 +1,125 @@ -use std::cell::RefCell; use std::fmt::{Debug, Display}; +use std::io::Read; use std::path::Path; -use odonata_base::boards::Position; +use anyhow::Context; +use odonata_base::infra::utils; use odonata_base::prelude::*; +use serde::{Deserialize, Serialize}; -use super::network::{Network, Network768xH2}; +use super::network::{net_path, Network, Network768xH2}; +use crate::eval::network::{Bucket, BUCKETS_768H2}; +use crate::eval::vector::Vector; -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum Nnue { - Nnue(NnueMixin>), - Nnue768H2(NnueMixin>), + Nnue768Float(Network768xH2), + Nnue768(Network768xH2), } impl Display for Nnue { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let net = match self { - Nnue::Nnue(nnue) => nnue.net.to_string(), - Nnue::Nnue768H2(nnue) => nnue.net.to_string(), + Nnue::Nnue768Float(nnue) => nnue.to_string(), + Nnue::Nnue768(nnue) => nnue.to_string(), }; write!(f, "nnue<{net}>")?; Ok(()) } } + + + impl Nnue { pub fn from_file(path: impl AsRef) -> Result { let nnue_file = path.as_ref().to_path_buf(); match nnue_file.to_string_lossy() { f if f.is_empty() => { - let net = Network768xH2::::from_file("")?; - let nnue = NnueMixin::> { - wb_acc: RefCell::new(net.new_accumulators()), - last: RefCell::new(Board::new_empty()), - net, - }; - Ok(Nnue::Nnue768H2(nnue)) + let net: Network768xH2 = postcard::from_bytes(net_path())?; + Ok(Nnue::Nnue768(net)) } - f if f.ends_with(".bin") => { - let net = Network768xH2::::from_file(nnue_file)?; - let nnue = NnueMixin::> { - wb_acc: RefCell::new(net.new_accumulators()), - last: RefCell::new(Board::new_empty()), - net, - }; - Ok(Nnue::Nnue768H2(nnue)) + f if f.ends_with(".i16.bin") => { + debug!(target: "config", "loading binary nnue from {f:?}"); + let mut buf = Vec::new(); + let _bytes = utils::file_open(&path)?.read_to_end(&mut buf)?; + + #[derive(Default, Serialize, Deserialize)] + #[repr(C, align(64))] + pub struct Network768xH2Old { + pub n_features: usize, + pub n_hidden_layer: usize, + pub wt: Vec>, // (acc = hl_size) * input_size + pub bi: Vector, + pub h1_wt: [Vector; 2], // hl_size + pub h1_bi: Vector, // size 1 + + #[serde(default)] + pub description: String, + } + let net: Network768xH2Old = postcard::from_bytes(&buf).context(format!("{nnue_file:?}"))?; + let mut net2 = Network768xH2::::default(); + net2.n_features = net.n_features; + net2.n_hidden_layer = net.n_hidden_layer; + net2.wt = net.wt; + net2.bi = net.bi; + for i in 0..BUCKETS_768H2 { + net2.h1_wt[i] = net.h1_wt.clone(); + net2.h1_bi[i] = net.h1_bi.clone(); + } + net2.description = net.description; + Ok(Nnue::Nnue768(net2)) } - f if f.ends_with("i16.yaml") => { - let net = Network768xH2::::from_file(nnue_file)?; - let nnue = NnueMixin::> { - wb_acc: RefCell::new(net.new_accumulators()), - last: RefCell::new(Board::new_empty()), - net, - }; - Ok(Nnue::Nnue768H2(nnue)) + f if f.ends_with(".i16.2.bin") => { + debug!(target: "config", "loading binary nnue ver 2 from {f:?}"); + let mut buf = Vec::new(); + let _bytes = utils::file_open(&path)?.read_to_end(&mut buf)?; + let mut net: Network768xH2 = postcard::from_bytes(&buf).context(format!("{nnue_file:?}"))?; + net.bucket = Some(Bucket::QueenPhase); + Ok(Nnue::Nnue768(net)) } - f if f.ends_with(".yaml") => { - let net = Network768xH2::::from_file(nnue_file)?; - let nnue = NnueMixin { - wb_acc: RefCell::new(net.new_accumulators()), - last: RefCell::new(Board::starting_pos()), - net, - }; - Ok(Nnue::Nnue(nnue)) + f if f.ends_with(".i16.yaml") => { + let net: Network768xH2 = + serde_yaml::from_reader(file_open(&nnue_file)?).context(format!("{nnue_file:?}"))?; + Ok(Nnue::Nnue768(net)) } - _ => unreachable!(), + f if f.ends_with(".f32.yaml") => { + let path = path.as_ref(); + let mut net: Network768xH2 = serde_yaml::from_reader(file_open(path)?)?; + net.bucket = Some(Bucket::QueenPhase); + Ok(Nnue::Nnue768Float(net)) + } + f => anyhow::bail!("unknown file formal {f}"), } } - pub fn eval(&self, pos: &Position) -> i16 { - match self { - Nnue::Nnue(nnue) => nnue.eval(pos) as i16, - Nnue::Nnue768H2(nnue) => nnue.eval(pos), - } - } + // pub fn eval(&self, pos: &Position) -> i16 { + // match self { + // Nnue::Nnue(nnue) => nnue.eval(pos) as i16, + // Nnue::Nnue768H2(nnue) => nnue.eval(pos), + // } + // } - pub fn eval_stateless(&self, b: &Board) -> i16 { + pub fn explain(&self, b: &Board) -> String { match self { - Nnue::Nnue(nnue) => nnue.eval_stateless(b) as i16, - Nnue::Nnue768H2(nnue) => nnue.eval_stateless(b), + Nnue::Nnue768Float(nnue) => nnue.explain(b), + Nnue::Nnue768(nnue) => nnue.explain(b), } } - pub fn new_game(&mut self) { + pub fn eval(&self, b: &Board) -> i16 { match self { - Nnue::Nnue(nnue) => nnue.new_game(), - Nnue::Nnue768H2(nnue) => nnue.new_game(), + Nnue::Nnue768Float(nnue) => nnue.predict(b) as i16, + Nnue::Nnue768(nnue) => nnue.predict(b), } } -} - -#[derive(Clone)] -pub struct NnueMixin { - wb_acc: RefCell, - net: Box, - last: RefCell, -} - -// impl Default for NnueMixin { -// fn default() -> Self { -// Self::from_file("").unwrap() -// } -// } - -impl Debug for NnueMixin { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "net : {:?}", self.net)?; - writeln!(f, "last eval bd: {}", self.last.borrow().to_fen())?; - Ok(()) - } -} - -impl NnueMixin { - - pub fn new_game(&mut self) { - let mut acc = self.net.new_accumulators(); - let b = Board::starting_pos(); - self.net.forward1(&mut acc, &b); - self.wb_acc = RefCell::new(acc); - self.last = RefCell::new(b); - } - - #[cfg(test)] - pub fn testing_eval_current(&self) -> N::Output { - let turn = self.last.borrow().turn(); - self.net.forward2(turn, &self.wb_acc.borrow()) - } - pub fn eval(&self, pos: &Position) -> N::Output { - let b = pos.board(); - self.eval_stateless(b) - } - - pub fn eval_stateless(&self, board: &Board) -> N::Output { - let mut last = self.last.borrow_mut(); - let mut acc = self.wb_acc.borrow_mut(); - self.net.forward1_input(&mut acc, &last, board); - *last = board.clone(); - self.net.forward2(board.turn(), &acc) + pub fn new_game(&self) { + let empty = &Board::new_empty(); + let _ = match self { + Nnue::Nnue768Float(nnue) => nnue.predict(empty), + Nnue::Nnue768(nnue) => nnue.predict(empty) as f32, + }; } } @@ -180,7 +162,7 @@ mod tests { fn test_nn_random_moves() { let mut rand = ChaChaRng::seed_from_u64(1); let nnue1 = Nnue::from_file("").unwrap(); - let nnue2 = nnue1.clone(); + let nnue2 = Nnue::from_file("").unwrap(); for i in 0..1000 { let epd = Epd::starting_pos(); @@ -197,22 +179,22 @@ mod tests { let mut b = epd.setup_board(); for mv in var.moves() { b.apply_move(mv); - nnue2.eval_stateless(&b); + nnue2.eval(&b); } // final board - let cp1 = nnue1.eval_stateless(&epd.board()); - let cp2 = nnue2.eval_stateless(&epd.board()); + let cp1 = nnue1.eval(&epd.board()); + let cp2 = nnue2.eval(&epd.board()); assert_eq!(cp1, cp2, "#{i} (e) {epd} {nnue2:#?}"); let setup = epd.setup_board(); for i in 0..var.len() { let b = setup.make_moves(&var.take(i)); - nnue2.eval_stateless(&b); + nnue2.eval(&b); } // back to setup board - let cp1 = nnue1.eval_stateless(&epd.setup_board()); - let cp2 = nnue2.eval_stateless(&epd.setup_board()); + let cp1 = nnue1.eval(&epd.setup_board()); + let cp2 = nnue2.eval(&epd.setup_board()); assert_eq!(cp1, cp2, "#{i} (s) {epd} {nnue2:#?}"); } } @@ -229,7 +211,7 @@ mod tests { let ns_512 = Nnue::from_file(nnue_512).unwrap(); // let _score = PerfProfiler::new("nnue128 startpos").bench(|| ns_128.eval_stateless(&board)); // let _score = PerfProfiler::new("nnue256 startpos").bench(|| ns_256.eval_stateless(&board)); - let score = PerfProfiler::new("nnue512 startpos").bench(|| ns_512.eval_stateless(&board)); + let score = PerfProfiler::new("nnue512 startpos").bench(|| ns_512.eval(&board)); info!("nnue score = {score} for startpos"); let var = board.parse_san_variation("e4 e5 d4 exd4 Nf3 Bb4 c3").unwrap(); let b = var @@ -259,23 +241,23 @@ mod tests { // let mut prof_adj_6m = PerfProfiler::new("nnue adj-6"); // let mut prof_adj_6p = PerfProfiler::new("nnue adj+6"); - prof_mv0.bench(|| ns_512.eval_stateless(&b[0])); - prof_mv1.bench(|| ns_512.eval_stateless(&b[1])); - prof_mv2.bench(|| ns_512.eval_stateless(&b[2])); - prof_mv3c.bench(|| ns_512.eval_stateless(&b[3])); - prof_mv4.bench(|| ns_512.eval_stateless(&b[4])); - prof_mv5.bench(|| ns_512.eval_stateless(&b[5])); - prof_mv6.bench(|| ns_512.eval_stateless(&b[6])); + prof_mv0.bench(|| ns_512.eval(&b[0])); + prof_mv1.bench(|| ns_512.eval(&b[1])); + prof_mv2.bench(|| ns_512.eval(&b[2])); + prof_mv3c.bench(|| ns_512.eval(&b[3])); + prof_mv4.bench(|| ns_512.eval(&b[4])); + prof_mv5.bench(|| ns_512.eval(&b[5])); + prof_mv6.bench(|| ns_512.eval(&b[6])); - prof_pop6.bench(|| ns_512.eval_stateless(&b[5])); - prof_pop5.bench(|| ns_512.eval_stateless(&b[4])); - prof_pop4.bench(|| ns_512.eval_stateless(&b[3])); + prof_pop6.bench(|| ns_512.eval(&b[5])); + prof_pop5.bench(|| ns_512.eval(&b[4])); + prof_pop4.bench(|| ns_512.eval(&b[3])); // prof_eval.bench(|| ns_512.eval_stateless(&b[3])); // prof_mv0_256.bench(|| ns_256.eval_stateless(&b[0])); // prof_mv0_512.bench(|| ns_512.eval_stateless(&b[0])); - prof_eval_512.bench(|| ns_512.eval_stateless(&b[0])); + prof_eval_512.bench(|| ns_512.eval(&b[0])); // let mut pos2 = pos.clone(); // pos2.eval_stateless(&b[0]); // pos2.eval_stateless(&b[1]); @@ -303,10 +285,10 @@ mod tests { let tot = prof.bench(|| { let mut sc = 0; for b in boards.iter() { - sc += ns.eval_stateless(b); + sc += ns.eval(b); } for b in boards.iter().rev() { - sc -= ns.eval_stateless(b); + sc -= ns.eval(b); } sc }); diff --git a/crates/odonata-engine/src/eval/vector.rs b/crates/odonata-engine/src/eval/vector.rs index ce2803f7..12f8adb9 100644 --- a/crates/odonata-engine/src/eval/vector.rs +++ b/crates/odonata-engine/src/eval/vector.rs @@ -8,7 +8,7 @@ use odonata_base::infra::math::Quantize; use odonata_base::infra::utils; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -const N: usize = 512; +const N: usize = 1024; #[derive(Clone, PartialEq)] #[repr(C)] @@ -157,6 +157,8 @@ impl Vector { 128 => self.vals[..128].iter_mut().zip(&*rhs.vals).for_each(|(x, y)| f(x, y)), 256 => self.vals[..256].iter_mut().zip(&*rhs.vals).for_each(|(x, y)| f(x, y)), 512 => self.vals[..512].iter_mut().zip(&*rhs.vals).for_each(|(x, y)| f(x, y)), + // 768 => self.vals[..768].iter_mut().zip(&*rhs.vals).for_each(|(x, y)| f(x, y)), + // 1024 => self.vals[..1024].iter_mut().zip(&*rhs.vals).for_each(|(x, y)| f(x, y)), _ => self.vals[..n].iter_mut().zip(&rhs.vals[..]).for_each(|(x, y)| f(x, y)), } } @@ -171,6 +173,8 @@ impl Vector { 128 => self.vals[..128].iter_mut().zip(&*rhs1.vals).zip(&*rhs2.vals).for_each(|((x, y), z)| f(x, y, z)), 256 => self.vals[..256].iter_mut().zip(&*rhs1.vals).zip(&*rhs2.vals).for_each(|((x, y), z)| f(x, y, z)), 512 => self.vals[..512].iter_mut().zip(&*rhs1.vals).zip(&*rhs2.vals).for_each(|((x, y), z)| f(x, y, z)), + // 768 => self.vals[..768].iter_mut().zip(&*rhs1.vals).zip(&*rhs2.vals).for_each(|((x, y), z)| f(x, y, z)), + // 1024 => self.vals[..1024].iter_mut().zip(&*rhs1.vals).zip(&*rhs2.vals).for_each(|((x, y), z)| f(x, y, z)), _ => self.vals[..n].iter_mut().zip(&rhs1.vals[..]).zip(&rhs2.vals[..]).for_each(|((x, y), z)| f(x, y, z)), } } @@ -183,6 +187,8 @@ impl Vector { 128 => self.vals[..128].iter().zip(&rhs.vals[..128]).for_each(|(x, y)| f(x, y)), 256 => self.vals[..256].iter().zip(&rhs.vals[..256]).for_each(|(x, y)| f(x, y)), 512 => self.vals[..512].iter().zip(&rhs.vals[..512]).for_each(|(x, y)| f(x, y)), + // 768 => self.vals[..768].iter().zip(&rhs.vals[..768]).for_each(|(x, y)| f(x, y)), + // 1024 => self.vals[..1024].iter().zip(&rhs.vals[..1024]).for_each(|(x, y)| f(x, y)), _ => self.vals[..n].iter().zip(&rhs.vals[..]).for_each(|(x, y)| f(x, y)), } } @@ -194,6 +200,8 @@ impl Vector { 128 => self.vals[..128].iter_mut().for_each(f), 256 => self.vals[..256].iter_mut().for_each(f), 512 => self.vals[..512].iter_mut().for_each(f), + // 768 => self.vals[..768].iter_mut().for_each(f), + // 1024 => self.vals[..1024].iter_mut().for_each(f), _ => self.vals[..n].iter_mut().for_each(f), } } diff --git a/crates/odonata-engine/src/search/alphabeta.rs b/crates/odonata-engine/src/search/alphabeta.rs index 7ca6d210..5f10e594 100644 --- a/crates/odonata-engine/src/search/alphabeta.rs +++ b/crates/odonata-engine/src/search/alphabeta.rs @@ -139,6 +139,7 @@ impl Search { }; if let Some(s) = self.mate_distance(&mut n) { + trace!(target: "mate", "mate distance {s} {n} {pos}"); return Ok((s, Event::MateDistSuccess)); } @@ -460,6 +461,7 @@ mod tests { use odonata_base::catalog::*; use odonata_base::domain::staticeval::StaticEval; use odonata_base::domain::timecontrol::*; + use odonata_base::infra::utils::ToStringOr; use odonata_base::other::tags::EpdOps as _; use test_log::test; @@ -470,7 +472,9 @@ mod tests { let positions = Catalog::mate_in_2(); for (i, epd) in positions.iter().enumerate() { let mut eng = ThreadedSearch::new(); + eng.search.mate_dist.enabled = false; eng.search.tt.allow_truncated_pv = false; + eng.search.tt.enabled = true; eng.set_callback(crate::comms::uci_server::UciServer::uci_info); // search.tt.enabled = false; let res = eng.search(epd.clone(), TimeControl::Depth(7)).unwrap(); @@ -505,6 +509,8 @@ mod tests { #[test] fn test_mate_in_4() -> Result<()> { let mut eng = ThreadedSearch::new(); + eng.set_callback(crate::comms::uci_server::UciServer::uci_info); + eng.search.controller.multi_pv = 6; eprintln!("{:?}", eng.search.eval); for epd in Catalog::mate_in_4().iter() { eng.start_game()?; @@ -520,6 +526,7 @@ mod tests { .eval .static_eval_explain(&Position::from_board(final_board_pos.clone())); if res.score().unwrap().mate_in() != Some(4) { + eprintln!("pv: {}\nexpected pv: {}\n", res.pv(), epd.var("pv").to_string_or("n/a")); eprintln!("result: {}\nexpected: {epd}", res.to_san(&epd.board())); eprintln!("b+pv = {final_board_pos}"); eprintln!("score: {current_board_score}\nfscore: {final_board_score}"); diff --git a/crates/odonata-engine/src/search/engine.rs b/crates/odonata-engine/src/search/engine.rs index aaac06a7..0d6bb9f4 100644 --- a/crates/odonata-engine/src/search/engine.rs +++ b/crates/odonata-engine/src/search/engine.rs @@ -493,7 +493,7 @@ mod tests { // // eng.algo.explainer.enabled = true; // eng.algo.explainer.add_variation_to_explain(Variation::new()); let res = eng.search(epd, TimeControl::Depth(5)).unwrap(); - println!("{}", res.to_results_epd()); + println!("{}", res.to_epd()); } #[test] @@ -540,7 +540,7 @@ mod tests { search.search.set_callback(UciServer::uci_info); let sr = search.search(pos.clone(), TimeControl::Depth(12)).unwrap(); println!("{}", search.search.eval); - println!("{}", sr.to_results_epd()); + println!("{}", sr.to_epd()); assert_eq!( sr.supplied_move().unwrap(), Move::parse_uci("f6h4", &pos.board()).unwrap() @@ -613,19 +613,19 @@ mod tests { let res = eng.search(p.clone(), tc).unwrap(); let pv1 = res.pv(); eng.search.tt.current_age -= 1; - println!("{:<40} - {}", pv1.to_uci(), res.to_results_epd()); + println!("{:<40} - {}", pv1.to_uci(), res.to_epd()); let tc = TimeControl::Depth(7); eng.search.tt.allow_truncated_pv = true; let res = eng.search(p.clone(), tc).unwrap(); let pv2 = res.pv(); - println!("{:<40} - {}", pv2.to_uci(), res.to_results_epd()); + println!("{:<40} - {}", pv2.to_uci(), res.to_epd()); let tc = TimeControl::Depth(7); eng.search.tt.allow_truncated_pv = false; let res = eng.search(p.clone(), tc).unwrap(); let pv3 = res.pv(); - println!("{:<40} - {}\n", pv3.to_uci(), res.to_results_epd()); + println!("{:<40} - {}\n", pv3.to_uci(), res.to_epd()); // assert_eq!(pv1, pv2, "{}", p ); } diff --git a/crates/odonata-engine/src/search/mate_distance.rs b/crates/odonata-engine/src/search/mate_distance.rs index b8927254..7b8cb74e 100644 --- a/crates/odonata-engine/src/search/mate_distance.rs +++ b/crates/odonata-engine/src/search/mate_distance.rs @@ -25,7 +25,7 @@ impl Component for MateDistance { impl Default for MateDistance { fn default() -> Self { MateDistance { - enabled: true, + enabled: false, raise_alpha: true, reduce_beta: true, } diff --git a/crates/odonata-engine/src/search/nmp.rs b/crates/odonata-engine/src/search/nmp.rs index deb04032..7de126e9 100644 --- a/crates/odonata-engine/src/search/nmp.rs +++ b/crates/odonata-engine/src/search/nmp.rs @@ -174,6 +174,11 @@ impl NullMovePruning { return (false, "non numeric eval"); } + if !n.beta.is_finite() { + Metrics::incr_node(n, Event::NmpDeclineEvalMargin); + return (false, "beta"); + } + if eval < n.beta + self.eval_margin { Metrics::incr_node(n, Event::NmpDeclineEvalMargin); return (false, "margin"); @@ -237,7 +242,8 @@ impl Search { }; let (allow, reason) = self.nmp.allow(trail, pos.board(), n, eval); - if self.nmp.logging { + // nmp relies on beta numeric + if self.nmp.logging && n.beta.is_numeric() { let reason = reason.to_string(); // real.score > beta (but performed null move) => Good nmp // real.score <= beta (but performed null move) => Wasted Null Move Search diff --git a/crates/odonata-engine/src/search/qs.rs b/crates/odonata-engine/src/search/qs.rs index 5b75b816..d6927066 100644 --- a/crates/odonata-engine/src/search/qs.rs +++ b/crates/odonata-engine/src/search/qs.rs @@ -197,7 +197,7 @@ impl RunQs<'_> { return Err(pat); } - if !in_check { + if !in_check { if pat >= n.beta { Metrics::incr_node(&n, Event::QsCatCutStandingPat); self.trail.prune_node(&n, pat, Event::QsCatCutStandingPat); @@ -253,6 +253,7 @@ impl RunQs<'_> { Metrics::incr_node(&n, Event::QsMoveGen); let mut moves = MoveList::new(); self.gen_sorted_moves(in_check, &n, pos.board(), lm, hm, &mut moves); + trace!("qs: moves [{moves}]"); // if in_check && moves.is_empty() { // return Ok(Score::from_mate_in_moves(0).clamp_score()); @@ -262,10 +263,12 @@ impl RunQs<'_> { let mut bs = None; // Some(pat); for &mv in moves.iter() { Metrics::incr_node(&n, Event::QsMoveCount); - if !in_check && self.can_see_prune_move(mv, &n, pat, pos.board()) { + if !in_check && self.can_see_prune_move(mv, &n, pat, pos.board()) { + trace!("qs: move {mv} see pruned"); continue; } if !in_check && self.can_delta_prune_move(mv, &n, pat, pos.board()) { + trace!("qs: move {mv} delta pruned"); continue; } @@ -599,7 +602,7 @@ mod tests { println!( "search: {pv_act:<20} {pos_act}\nexpected:{pv_exp:<20} {pos_exp}\n", pv_act = res.pv().to_string(), - pos_act = res.to_results_epd(), + pos_act = res.to_epd(), pv_exp = epd.var("pv").unwrap().to_string(), pos_exp = epd, // res = eng.algo.results, diff --git a/crates/odonata-engine/src/search/search_results.rs b/crates/odonata-engine/src/search/search_results.rs index d37a0d1b..ce0559f1 100644 --- a/crates/odonata-engine/src/search/search_results.rs +++ b/crates/odonata-engine/src/search/search_results.rs @@ -8,6 +8,7 @@ use odonata_base::domain::staticeval::StaticEval; use odonata_base::infra::utils::calculate_branching_factor_by_nodes_and_depth; use odonata_base::movelist::ScoredMoveList; use odonata_base::other::outcome::Outcome; +use odonata_base::other::Tags; use odonata_base::prelude::*; use odonata_base::variation::{MultiVariation, ScoredVariation}; use odonata_base::Epd; @@ -154,6 +155,15 @@ impl Response { ) } + pub fn info_by_depth(&self, depth: Ply) -> Option<&Info> { + self.infos + .iter() + .filter(|i| i.pv.is_some()) + .filter(|i| i.multi_pv.unwrap_or(1) == 1) + .filter(|i| i.depth == Some(depth)) + .nth(0) + } + pub fn extract_multi_pv(infos: &[Info], max_depth: Option) -> MultiVariation { // step #1, find max multipv index - where multi_pv exists let max_index = infos.iter().map(|i| i.multi_pv.unwrap_or(1)).max(); @@ -314,7 +324,22 @@ impl Response { self.multi_pv.clone().into() } - pub fn to_results_epd(&self) -> Epd { + // pub fn to_epd_by_ply(&self) -> HashMap { + // let mut epds = HashMap::new(); + // for info in self.infos.iter() { + // if let Some(multi_pv) = info.multi_pv { + // if multi_pv > 0 { + // continue; + // } + // } + // if let Some(depth) = info.depth { + // epds.insert(depth, info.to_epd(&self.input.board())); + // } + // } + // epds + // } + + pub fn to_epd(&self) -> Epd { let mut epd = self.input.clone(); let b = &epd.board(); epd.set_tag("sv", &self.pv().to_san(b)); @@ -339,6 +364,19 @@ impl Response { epd.set_tag("Acms", &self.time_millis.to_string()); epd.set_tag("acn", &self.nodes.to_string()); epd.set_tag("Bf", &((self.bf * 1000.0) as i32).to_string()); + + let mut scores: Vec> = vec![]; + for d in 0..=self.depth { + let info = self.info_by_depth(d); + scores.push(info.and_then(|i| i.score)); + } + epd.set_tag( + Tags::MCE, + &scores + .iter() + .map(|sc| sc.map(|sc| sc.to_epd()).unwrap_or_default()) + .join(","), + ); // if fields.contains(&Tags::ESM) { // tags.eng_scored_moves = Some(self.scored_move_list()); // } @@ -352,7 +390,7 @@ impl Response { #[cfg(test)] mod tests { - use odonata_base::prelude::testing::Testing; + use odonata_base::{other::tags::TagOps, prelude::testing::Testing}; use odonata_base::prelude::*; use odonata_base::variation::MultiVariation; use odonata_base::Epd; @@ -413,6 +451,12 @@ bestmove g2g4 ponder e7e5 // assert_eq!(BareMove::parse_uci("g3g6")?, "g3g6".mv()); // assert_eq!(sr.best_move(), Ok("g3g6".try_into()?)); assert_eq!(sr.nodes, 100_000); + assert!(sr.info_by_depth(9).is_none()); + assert!(sr.info_by_depth(10).is_some()); + assert!(sr.info_by_depth(11).is_some()); + assert!(sr.info_by_depth(12).is_none()); + assert_eq!(sr.to_epd().get("Mce"), Some(",,,,,,,,,,32763,32763")); + assert_eq!(sr.to_epd().score_for_depth(10), Some(Score::from_mate_in_moves(2))); // "g2g4"[b] assert_eq!(sr.supplied_move().unwrap(), Move::parse_uci("g2g4", &b).unwrap()); assert_eq!(sr.pv(), "e2e4 e7e5 a2a3".var(&b)); @@ -423,7 +467,7 @@ bestmove g2g4 ponder e7e5 assert_eq!(sr.depth, 11); assert_eq!(sr.bf > 2.5, true); assert_eq!(sr.bf < 3.0, true); - info!("{}", "a3a4".mv(&b)); + // info!("{}", "a3a4".mv(&b)); let s = r#"info depth 10 seldepth 10 nodes 61329 nps 1039000 score mate 2 hashfull 40 time 58 pv h2h4 e7e5 b2b3 info depth 11 seldepth 12 nodes 82712 nps 973000 score mate 2 hashfull 45 time 84 pv e2e4 e7e5 a2a3 diff --git a/docs/README.md b/docs/README.md index 1c79cd79..543fcf49 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ # Odonata A chess engine written in Rust. -Odonata was created by [Andy Watkins](https://github.com/akanalytics), with [Mark Raistrick](https://github.com/raistrma) assisting with designs and discussion. Bugs in the Rust code will certainly be mine though - Andy ;-) +Odonata is designed by [Andy Watkins](https://github.com/akanalytics), with [Mark Raistrick](https://github.com/raistrma) assisting with designs and discussion. Bugs in the Rust code will certainly be mine though - Andy ;-) My lockdown hobby was writing a chess engine, and learning Python and Rust in the process. I started Decemeber 2020. Python and Rust are very different from Java, which I had programmed maybe 10 years previous. Certainly my first efforts at Rust are not very clean, clever or idiomatic, but the code improves as I revisit areas to build improvements. diff --git a/docs/changelog.md b/docs/changelog.md index 75d62423..5620e2b8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,13 @@ # Changelog +# Release 1.0 (22 Jul 2024) +- NNUE evaluation (768 - 1024)x2 - 5 +- Introduced bucketed L2 based on queen-count/phase, increased HL to 1024 +- Increased training dataset to 700m positions +- Relabeled positions using Odonata's NNUE rather than HCE +- Fixed some bugs in the tuner +- Expected Elo +150 vs Odonata 0.9.0 + # Release 0.9.0 (6 Jun 2024) - NNUE evaluation (768 - 512x2 - 1 network) - Trained using simple self-written CPU-device NN trainer (mini-batch, AdamW), and own self-play and evals.