1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
#!/usr/bin/env python3
"""Python reference implementation of the HalfKAv2_hm feature index
formula -- see ../src/docs/NNUE.md's "Canonical HalfKAv2_hm
feature-indexing spec" section, sourced verbatim from Stockfish's
src/nnue/features/half_ka_v2_hm.{h,cpp} (git tag sf_16).
This module's only job is to be an independent, from-scratch
implementation of the same math as ../src/nnuefeatures.c
(`dump nnuefeatures`), using python-chess's own (already
rank*8+file, 0-63, a1=0) square numbering directly -- no 0x88
conversion needed on this side, that's purely a typhoon-C-side
concern.
See cross_check_features.py for the actual Python-vs-C diff driver.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src" / "eval_tune"))
import chess # noqa: E402
SQUARE_NB = 64
PS_W_PAWN, PS_B_PAWN = 0 * SQUARE_NB, 1 * SQUARE_NB
PS_W_KNIGHT, PS_B_KNIGHT = 2 * SQUARE_NB, 3 * SQUARE_NB
PS_W_BISHOP, PS_B_BISHOP = 4 * SQUARE_NB, 5 * SQUARE_NB
PS_W_ROOK, PS_B_ROOK = 6 * SQUARE_NB, 7 * SQUARE_NB
PS_W_QUEEN, PS_B_QUEEN = 8 * SQUARE_NB, 9 * SQUARE_NB
PS_KING = 10 * SQUARE_NB
PS_NB = 11 * SQUARE_NB # 704
DIMENSIONS = (SQUARE_NB * PS_NB) // 2 # 22528
_OWN_PLANE = {
chess.PAWN: PS_W_PAWN,
chess.KNIGHT: PS_W_KNIGHT,
chess.BISHOP: PS_W_BISHOP,
chess.ROOK: PS_W_ROOK,
chess.QUEEN: PS_W_QUEEN,
}
_ENEMY_PLANE = {
chess.PAWN: PS_B_PAWN,
chess.KNIGHT: PS_B_KNIGHT,
chess.BISHOP: PS_B_BISHOP,
chess.ROOK: PS_B_ROOK,
chess.QUEEN: PS_B_QUEEN,
}
def king_bucket(perspective, sq):
"""sq, perspective: python-chess 0-63 square / chess.WHITE|BLACK."""
file = chess.square_file(sq)
rank0 = chess.square_rank(sq) # 0..7
file_quartet = file if file < 4 else (7 - file)
rank_group = (7 - rank0) if perspective == chess.WHITE else rank0
return rank_group * 4 + file_quartet
def orient_mask(perspective, ksq):
file = chess.square_file(ksq)
if perspective == chess.WHITE:
return 7 if file < 4 else 0 # SQ_H1 : SQ_A1
return 63 if file < 4 else 56 # SQ_H8 : SQ_A8
def piece_plane(perspective, piece_color, piece_type):
if piece_type == chess.KING:
return PS_KING
table = _OWN_PLANE if piece_color == perspective else _ENEMY_PLANE
return table[piece_type]
def active_feature_indices(board, perspective):
"""board: chess.Board. perspective: chess.WHITE or chess.BLACK.
Returns a sorted list of active feature indices (len <= 32)."""
ksq = board.king(perspective)
mask = orient_mask(perspective, ksq)
bucket_offset = king_bucket(perspective, ksq) * PS_NB
indices = []
for sq, piece in board.piece_map().items():
idx = (sq ^ mask) + piece_plane(perspective, piece.color, piece.piece_type) + bucket_offset
assert 0 <= idx < DIMENSIONS, idx
indices.append(idx)
indices.sort()
return indices
if __name__ == "__main__":
# Quick manual smoke test against the starting position.
board = chess.Board()
print("white:", active_feature_indices(board, chess.WHITE))
print("black:", active_feature_indices(board, chess.BLACK))
|