summaryrefslogtreecommitdiff
path: root/annotate_pgns/nnue_feature_spec.py
blob: b121c82a80e9637c6540c31a2a8deef0aa31a196 (plain)
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))