rust-pgn-reader-python-binding 3.4.0__tar.gz → 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/Cargo.lock +16 -5
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/Cargo.toml +7 -6
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/PKG-INFO +1 -1
- rust_pgn_reader_python_binding-4.0.0/README.md +64 -0
- rust_pgn_reader_python_binding-4.0.0/benches/parquet_bench.rs +154 -0
- rust_pgn_reader_python_binding-4.0.0/rust_pgn_reader_python_binding.pyi +384 -0
- rust_pgn_reader_python_binding-4.0.0/src/bench_data_access.py +95 -0
- rust_pgn_reader_python_binding-3.4.0/src/bench_parquet.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_game.py +3 -5
- rust_pgn_reader_python_binding-3.4.0/src/bench.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_game_pgn.py +6 -7
- rust_pgn_reader_python_binding-3.4.0/src/bench_parquet_arrow.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_games.py +2 -6
- rust_pgn_reader_python_binding-3.4.0/src/bench_parquet_parallel.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_games_from_strings.py +1 -3
- rust_pgn_reader_python_binding-4.0.0/src/board_serialization.rs +133 -0
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/example_chess960.py +7 -11
- rust_pgn_reader_python_binding-4.0.0/src/example_complex.py +41 -0
- rust_pgn_reader_python_binding-4.0.0/src/example_parse_games.py +169 -0
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/example_rowgroup.py +4 -3
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/example_str.py +13 -12
- rust_pgn_reader_python_binding-4.0.0/src/lib.rs +349 -0
- rust_pgn_reader_python_binding-4.0.0/src/python_bindings.rs +877 -0
- rust_pgn_reader_python_binding-4.0.0/src/test.py +949 -0
- rust_pgn_reader_python_binding-4.0.0/src/visitor.rs +849 -0
- rust_pgn_reader_python_binding-3.4.0/README.md +0 -45
- rust_pgn_reader_python_binding-3.4.0/benches/parquet_bench.rs +0 -63
- rust_pgn_reader_python_binding-3.4.0/rust_pgn_reader_python_binding.pyi +0 -54
- rust_pgn_reader_python_binding-3.4.0/src/lib.rs +0 -776
- rust_pgn_reader_python_binding-3.4.0/src/test.py +0 -684
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/.github/workflows/CI.yml +0 -0
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/.gitignore +0 -0
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/pyproject.toml +0 -0
- {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/comment_parsing.rs +0 -0
|
@@ -749,6 +749,15 @@ version = "3.0.4"
|
|
|
749
749
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
750
750
|
checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02"
|
|
751
751
|
|
|
752
|
+
[[package]]
|
|
753
|
+
name = "inventory"
|
|
754
|
+
version = "0.3.21"
|
|
755
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
756
|
+
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
|
|
757
|
+
dependencies = [
|
|
758
|
+
"rustversion",
|
|
759
|
+
]
|
|
760
|
+
|
|
752
761
|
[[package]]
|
|
753
762
|
name = "itertools"
|
|
754
763
|
version = "0.13.0"
|
|
@@ -1080,9 +1089,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
|
|
1080
1089
|
|
|
1081
1090
|
[[package]]
|
|
1082
1091
|
name = "pgn-reader"
|
|
1083
|
-
version = "0.
|
|
1092
|
+
version = "0.29.0"
|
|
1084
1093
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1085
|
-
checksum = "
|
|
1094
|
+
checksum = "b822a2cfc1c90c109ea35458ad9bff6669198fdd4ea8de1132e702b4d9f64b88"
|
|
1086
1095
|
dependencies = [
|
|
1087
1096
|
"btoi",
|
|
1088
1097
|
"memchr",
|
|
@@ -1175,6 +1184,7 @@ dependencies = [
|
|
|
1175
1184
|
"chrono-tz",
|
|
1176
1185
|
"indexmap",
|
|
1177
1186
|
"indoc",
|
|
1187
|
+
"inventory",
|
|
1178
1188
|
"libc",
|
|
1179
1189
|
"memoffset",
|
|
1180
1190
|
"once_cell",
|
|
@@ -1322,13 +1332,14 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
|
|
1322
1332
|
|
|
1323
1333
|
[[package]]
|
|
1324
1334
|
name = "rust_pgn_reader_python_binding"
|
|
1325
|
-
version = "
|
|
1335
|
+
version = "4.0.0"
|
|
1326
1336
|
dependencies = [
|
|
1327
1337
|
"arrow",
|
|
1328
1338
|
"arrow-array",
|
|
1329
1339
|
"criterion",
|
|
1330
1340
|
"nom",
|
|
1331
1341
|
"num_cpus",
|
|
1342
|
+
"numpy",
|
|
1332
1343
|
"parquet",
|
|
1333
1344
|
"pgn-reader",
|
|
1334
1345
|
"pyo3",
|
|
@@ -1430,9 +1441,9 @@ dependencies = [
|
|
|
1430
1441
|
|
|
1431
1442
|
[[package]]
|
|
1432
1443
|
name = "shakmaty"
|
|
1433
|
-
version = "0.
|
|
1444
|
+
version = "0.30.0"
|
|
1434
1445
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1435
|
-
checksum = "
|
|
1446
|
+
checksum = "41cef923c4ad8bf68e6f52a3091499108b8fec71ea8c4ba27d1acfb263f53241"
|
|
1436
1447
|
dependencies = [
|
|
1437
1448
|
"arrayvec",
|
|
1438
1449
|
"bitflags",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "rust_pgn_reader_python_binding"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.0.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
|
|
@@ -11,19 +11,20 @@ crate-type = ["lib", "cdylib"]
|
|
|
11
11
|
# lib is needed for the native benchmark
|
|
12
12
|
|
|
13
13
|
[dependencies]
|
|
14
|
-
pyo3 = "0.27.2"
|
|
15
|
-
shakmaty = "0.
|
|
16
|
-
pgn-reader = "0.
|
|
14
|
+
pyo3 = { version = "0.27.2", features = ["multiple-pymethods"] }
|
|
15
|
+
shakmaty = "0.30"
|
|
16
|
+
pgn-reader = "0.29.0"
|
|
17
17
|
nom = "8.0"
|
|
18
18
|
rayon = "1.11"
|
|
19
19
|
num_cpus = "1.17"
|
|
20
20
|
arrow-array = "57"
|
|
21
21
|
pyo3-arrow = "0.15"
|
|
22
|
+
numpy = "0.27"
|
|
23
|
+
parquet = "57"
|
|
24
|
+
arrow = "57"
|
|
22
25
|
|
|
23
26
|
[dev-dependencies]
|
|
24
27
|
criterion = "0.8"
|
|
25
|
-
parquet = "57"
|
|
26
|
-
arrow = "57"
|
|
27
28
|
|
|
28
29
|
[[bench]]
|
|
29
30
|
harness = false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rust_pgn_reader_python_binding
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# rust_pgn_reader_python_binding
|
|
2
|
+
## Fast PGN parsing bindings for Python
|
|
3
|
+
This project adds Python bindings to [rust-pgn-reader](https://github.com/niklasf/rust-pgn-reader). In addition, it also parses and extracts [%clk ..] and [%eval ..] tags from comments.
|
|
4
|
+
|
|
5
|
+
## Installing
|
|
6
|
+
`pip install rust_pgn_reader_python_binding`
|
|
7
|
+
|
|
8
|
+
## API
|
|
9
|
+
|
|
10
|
+
Three entry points are available:
|
|
11
|
+
|
|
12
|
+
- `parse_game(pgn)` - Parse a single PGN string
|
|
13
|
+
- `parse_games(chunked_array)` - Parse games from a PyArrow ChunkedArray (multithreaded)
|
|
14
|
+
- `parse_games_from_strings(pgns)` - Parse a list of PGN strings (multithreaded)
|
|
15
|
+
|
|
16
|
+
All return a `ParsedGames` container with flat NumPy arrays, supporting:
|
|
17
|
+
- Indexing (`result[i]`), slicing (`result[1:3]`), and iteration (`for game in result`)
|
|
18
|
+
- Per-game views (`PyGameView`) with zero-copy array slices
|
|
19
|
+
- Position-to-game and move-to-game mapping for ML workflows
|
|
20
|
+
- Optional comment storage (`store_comments=True`)
|
|
21
|
+
- Optional legal move storage (`store_legal_moves=True`)
|
|
22
|
+
|
|
23
|
+
## Benchmarks
|
|
24
|
+
Below are some benchmarks on Lichess's 2013-07 chess games (293,459 games) on a 7800X3D.
|
|
25
|
+
|
|
26
|
+
| Parser | File format | Time |
|
|
27
|
+
|----------------------------------------------------------------------------|-------------|--------|
|
|
28
|
+
| [rust-pgn-reader](https://github.com/niklasf/rust-pgn-reader/tree/master) | PGN | 1s |
|
|
29
|
+
| rust_pgn_reader_python_binding, parse_games (multithreaded) | parquet | 0.35s |
|
|
30
|
+
| rust_pgn_reader_python_binding, parse_games_from_strings (multithreaded) | parquet | 0.5s |
|
|
31
|
+
| rust_pgn_reader_python_binding, parse_game (single-threaded) | parquet | 3.3s |
|
|
32
|
+
| rust_pgn_reader_python_binding, parse_game (single-threaded) | PGN | 4.7s |
|
|
33
|
+
| [chess-library](https://github.com/Disservin/chess-library) | PGN | 2s |
|
|
34
|
+
| [python-chess](https://github.com/niklasf/python-chess) | PGN | 3+ min |
|
|
35
|
+
|
|
36
|
+
To replicate, download `2013-07-train-00000-of-00001.parquet` and then run:
|
|
37
|
+
|
|
38
|
+
`python src/bench_parse_games.py` (recommended — multithreaded parse_games via Arrow)
|
|
39
|
+
|
|
40
|
+
`python src/bench_parse_games_from_strings.py` (multithreaded parse_games_from_strings)
|
|
41
|
+
|
|
42
|
+
`python src/bench_parse_game.py` (single-threaded parse_game from parquet)
|
|
43
|
+
|
|
44
|
+
`python src/bench_parse_game_pgn.py` (single-threaded parse_game from .pgn file)
|
|
45
|
+
|
|
46
|
+
`python src/bench_data_access.py 2013-07-train-00000-of-00001.parquet` (parsing + data access + memory)
|
|
47
|
+
|
|
48
|
+
## Building
|
|
49
|
+
`maturin develop`
|
|
50
|
+
|
|
51
|
+
`maturin develop --release`
|
|
52
|
+
|
|
53
|
+
For a more thorough tutorial, follow https://lukesalamone.github.io/posts/how-to-create-rust-python-bindings/
|
|
54
|
+
|
|
55
|
+
## Profiling
|
|
56
|
+
`py-spy record -s -F -f speedscope --output profile.speedscope -- python ./src/bench_parse_games.py`
|
|
57
|
+
|
|
58
|
+
Linux/WSL-only:
|
|
59
|
+
`py-spy record -s -F -n -f speedscope --output profile.speedscope -- python ./src/bench_parse_games.py`
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
`cargo test`
|
|
63
|
+
|
|
64
|
+
`python -m unittest src/test.py`
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//! Benchmark for PGN parsing API, designed to mirror the Python workflow.
|
|
2
|
+
//!
|
|
3
|
+
//! `cargo bench --bench parquet_bench`
|
|
4
|
+
//! `samply record --rate 10000 cargo bench --bench parquet_bench`
|
|
5
|
+
|
|
6
|
+
use arrow::array::{Array, StringArray};
|
|
7
|
+
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
|
|
8
|
+
use rayon::prelude::*;
|
|
9
|
+
use rayon::ThreadPoolBuilder;
|
|
10
|
+
use std::fs::File;
|
|
11
|
+
use std::path::Path;
|
|
12
|
+
use std::time::Instant;
|
|
13
|
+
|
|
14
|
+
use rust_pgn_reader_python_binding::{parse_game_to_buffers, Buffers, ParseConfig};
|
|
15
|
+
|
|
16
|
+
const FILE_PATH: &str = "2013-07-train-00000-of-00001.parquet";
|
|
17
|
+
|
|
18
|
+
/// Chunk multiplier for explicit chunking.
|
|
19
|
+
/// 1 = exactly num_threads chunks (minimal overhead)
|
|
20
|
+
/// Higher values provide better load balancing at cost of more buffers.
|
|
21
|
+
const CHUNK_MULTIPLIER: usize = 1;
|
|
22
|
+
|
|
23
|
+
/// Read parquet file and return the raw Arrow StringArrays.
|
|
24
|
+
/// This preserves Arrow's memory layout for zero-copy string access.
|
|
25
|
+
fn read_parquet_to_string_arrays(file_path: &str) -> Vec<StringArray> {
|
|
26
|
+
let file = File::open(Path::new(file_path)).expect("Unable to open file");
|
|
27
|
+
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
|
|
28
|
+
.expect("Failed to create ParquetRecordBatchReaderBuilder");
|
|
29
|
+
let mut reader = builder
|
|
30
|
+
.build()
|
|
31
|
+
.expect("Failed to build ParquetRecordBatchReader");
|
|
32
|
+
|
|
33
|
+
let mut arrays = Vec::new();
|
|
34
|
+
while let Some(batch) = reader
|
|
35
|
+
.next()
|
|
36
|
+
.transpose()
|
|
37
|
+
.expect("Error reading record batch")
|
|
38
|
+
{
|
|
39
|
+
if let Some(array) = batch
|
|
40
|
+
.column_by_name("movetext")
|
|
41
|
+
.and_then(|col| col.as_any().downcast_ref::<StringArray>())
|
|
42
|
+
{
|
|
43
|
+
arrays.push(array.clone());
|
|
44
|
+
} else {
|
|
45
|
+
panic!("movetext column not found or not a StringArray");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
arrays
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Extract &str slices from Arrow StringArrays (zero-copy).
|
|
52
|
+
fn extract_str_slices<'a>(arrays: &'a [StringArray]) -> Vec<&'a str> {
|
|
53
|
+
let total_len: usize = arrays.iter().map(|a| a.len()).sum();
|
|
54
|
+
let mut slices = Vec::with_capacity(total_len);
|
|
55
|
+
|
|
56
|
+
for array in arrays {
|
|
57
|
+
for i in 0..array.len() {
|
|
58
|
+
if array.is_valid(i) {
|
|
59
|
+
slices.push(array.value(i));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
slices
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Benchmark the parsing API workflow.
|
|
67
|
+
///
|
|
68
|
+
/// 1. Read parquet to Arrow arrays
|
|
69
|
+
/// 2. Extract &str slices from StringArray
|
|
70
|
+
/// 3. Parse in parallel with explicit chunking (par_chunks) -> fixed number of Buffers
|
|
71
|
+
///
|
|
72
|
+
/// No merge step - the chunked architecture keeps per-thread buffers as-is.
|
|
73
|
+
pub fn bench_parse_api() {
|
|
74
|
+
let config = ParseConfig {
|
|
75
|
+
store_comments: false,
|
|
76
|
+
store_legal_moves: false,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Step 1: Read parquet to Arrow StringArrays
|
|
80
|
+
let arrays = read_parquet_to_string_arrays(FILE_PATH);
|
|
81
|
+
|
|
82
|
+
// Step 2: Extract &str slices (zero-copy)
|
|
83
|
+
let pgn_slices = extract_str_slices(&arrays);
|
|
84
|
+
println!("Read {} games from parquet.", pgn_slices.len());
|
|
85
|
+
|
|
86
|
+
// Step 3: Build thread pool and compute capacity estimates
|
|
87
|
+
let num_threads = num_cpus::get();
|
|
88
|
+
let n_games = pgn_slices.len();
|
|
89
|
+
let moves_per_game = 70;
|
|
90
|
+
|
|
91
|
+
// Calculate chunk size for explicit chunking
|
|
92
|
+
let num_chunks = num_threads * CHUNK_MULTIPLIER;
|
|
93
|
+
let chunk_size = (n_games + num_chunks - 1) / num_chunks;
|
|
94
|
+
let chunk_size = chunk_size.max(1);
|
|
95
|
+
let games_per_chunk = chunk_size;
|
|
96
|
+
|
|
97
|
+
println!(
|
|
98
|
+
"Using {} threads, {} chunks, {} games/chunk",
|
|
99
|
+
num_threads, num_chunks, games_per_chunk
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
let thread_pool = ThreadPoolBuilder::new()
|
|
103
|
+
.num_threads(num_threads)
|
|
104
|
+
.build()
|
|
105
|
+
.expect("Failed to build Rayon thread pool");
|
|
106
|
+
|
|
107
|
+
// Step 4: Parse in parallel using par_chunks
|
|
108
|
+
let start = Instant::now();
|
|
109
|
+
|
|
110
|
+
let chunk_results: Vec<Buffers> = thread_pool.install(|| {
|
|
111
|
+
pgn_slices
|
|
112
|
+
.par_chunks(chunk_size)
|
|
113
|
+
.map(|chunk| {
|
|
114
|
+
let mut buffers = Buffers::with_capacity(games_per_chunk, moves_per_game, &config);
|
|
115
|
+
for &pgn in chunk {
|
|
116
|
+
let _ = parse_game_to_buffers(pgn, &mut buffers, &config);
|
|
117
|
+
}
|
|
118
|
+
buffers
|
|
119
|
+
})
|
|
120
|
+
.collect()
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
let duration_parallel = start.elapsed();
|
|
124
|
+
println!("Parallel parsing time: {:?}", duration_parallel);
|
|
125
|
+
println!(
|
|
126
|
+
"Created {} Buffers chunks (no merge needed)",
|
|
127
|
+
chunk_results.len()
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Compute totals from chunks
|
|
131
|
+
let total_games: usize = chunk_results.iter().map(|b| b.num_games()).sum();
|
|
132
|
+
let total_positions: usize = chunk_results.iter().map(|b| b.total_positions()).sum();
|
|
133
|
+
|
|
134
|
+
let duration_total = start.elapsed();
|
|
135
|
+
println!("Total time (parsing, no merge): {:?}", duration_total);
|
|
136
|
+
println!(
|
|
137
|
+
"Parsed {} games, {} total positions.",
|
|
138
|
+
total_games, total_positions
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Measure cleanup time
|
|
142
|
+
let drop_start = Instant::now();
|
|
143
|
+
drop(chunk_results);
|
|
144
|
+
let drop_duration = drop_start.elapsed();
|
|
145
|
+
|
|
146
|
+
let total_duration = start.elapsed();
|
|
147
|
+
println!("Cleanup time (drop): {:?}", drop_duration);
|
|
148
|
+
println!("Total time (parsing + cleanup): {:?}", total_duration);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn main() {
|
|
152
|
+
println!("=== Parse API (Buffers) ===\n");
|
|
153
|
+
bench_parse_api();
|
|
154
|
+
}
|