diff options
| author | Scott Gasch <[email protected]> | 2026-10-15 13:11:22 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-15 13:11:22 -0700 |
| commit | 993b6eeb2a9ac7b659c473b6883fa53af1ad3a0b (patch) | |
| tree | c5388824dec15a7691ce9952a6eb4af66e837127 /annotate_pgns/nnue_feature_spec.py | |
| parent | 53b380c658f828c4c0e096be2763e6b012c23abd (diff) | |
Phase A NNUE: feature-indexing spec, annotation/training pipeline, dump nnuefeatures
Builds out the NNUE Phase A prototype per docs/NNUE.md:
- src/nnuefeatures.c: standalone HalfKAv2_hm feature-index reference
implementation (new `dump nnuefeatures` command), sourced from
Stockfish's half_ka_v2_hm.{h,cpp} (sf_16 tag), including the 0x88->64
square conversion typhoon's own board representation needs.
- annotate_pgns/: sharded TWIC/self-play annotation harness
(annotate.py), position extraction, quiet-position filtering (flat
and game-shaped, plus an upstream --prefilter-quiet option),
train/holdout game-level stratified split, WDL-sigmoid K fitting,
the Python HalfKAv2_hm reference encoder (nnue_feature_spec.py) and
its bit-for-bit cross-checker against the C side
(cross_check_features.py), and a first PyTorch training pipeline
(nnue_model.py, nnue_train.py, build_training_tensors.py) -- a
256x2-32-32-1-ish net trained locally on CPU against ~1.85M quiet
TWIC positions as a pipeline/framework validation pass.
- Finish an in-progress InitMagic/InitLMRTable/InitEval ->
InitializeBitboardMagic/InitializeLMRTable/InitializeEval rename:
chess.h's prototypes hadn't been updated to match the already-renamed
definitions/call sites, leaving 4 implicit-function-declaration
warnings.
- tests/arasan.epd: new curated EPD suite.
- docs/: add the project's design docs (previously untracked).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'annotate_pgns/nnue_feature_spec.py')
| -rw-r--r-- | annotate_pgns/nnue_feature_spec.py | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/annotate_pgns/nnue_feature_spec.py b/annotate_pgns/nnue_feature_spec.py new file mode 100644 index 0000000..b121c82 --- /dev/null +++ b/annotate_pgns/nnue_feature_spec.py @@ -0,0 +1,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)) |
