summaryrefslogtreecommitdiff
path: root/src/nnuefeatures.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-15 13:11:22 -0700
committerScott Gasch <[email protected]>2026-10-15 13:11:22 -0700
commit993b6eeb2a9ac7b659c473b6883fa53af1ad3a0b (patch)
treec5388824dec15a7691ce9952a6eb4af66e837127 /src/nnuefeatures.c
parent53b380c658f828c4c0e096be2763e6b012c23abd (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 'src/nnuefeatures.c')
-rw-r--r--src/nnuefeatures.c248
1 files changed, 248 insertions, 0 deletions
diff --git a/src/nnuefeatures.c b/src/nnuefeatures.c
new file mode 100644
index 0000000..c43e267
--- /dev/null
+++ b/src/nnuefeatures.c
@@ -0,0 +1,248 @@
+/**
+
+Copyright (c) Scott Gasch
+
+Module Name:
+
+ nnuefeatures.c
+
+Abstract:
+
+ Standalone reference implementation of the HalfKAv2_hm feature
+ index formula, used ONLY to cross-check against the Python
+ reference encoder (annotate_pgns/nnue_feature_spec.py) -- see
+ docs/NNUE.md's "Canonical HalfKAv2_hm feature-indexing spec"
+ section for the formulas this file implements, sourced verbatim
+ from Stockfish's src/nnue/features/half_ka_v2_hm.{h,cpp} (git tag
+ sf_16).
+
+ This is deliberately NOT wired into search/eval -- no accumulator,
+ no incremental maintenance, nothing performance-sensitive. It
+ exists purely so `dump nnuefeatures` can print the exact same
+ active-feature-index set the eventual real C inference code must
+ produce, so a Python/C cross-checker has a C-side ground truth to
+ diff against before any of that real code exists. The *real*
+ MakeMove/UnmakeMove accumulator plumbing (per NNUE.md's
+ sequencing, gated behind a dedicated branch, not started) will
+ reuse this same index math, not this file's dump-command
+ plumbing.
+
+Author:
+
+ Scott Gasch ([email protected]) 15 Oct 2026
+
+**/
+
+#include "chess.h"
+
+//
+// HalfKAv2_hm constants, verbatim from Stockfish's half_ka_v2_hm.h.
+// PS_NB counts 5 non-king piece types x 2 "us/them" roles x 64
+// squares, plus one shared 64-square king plane (own king and enemy
+// king features both land in this one plane, distinguished only by
+// which square is set).
+//
+#define NNUE_SQUARE_NB (64)
+#define NNUE_PS_W_PAWN (0 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_PAWN (1 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_KNIGHT (2 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_KNIGHT (3 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_BISHOP (4 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_BISHOP (5 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_ROOK (6 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_ROOK (7 * NNUE_SQUARE_NB)
+#define NNUE_PS_W_QUEEN (8 * NNUE_SQUARE_NB)
+#define NNUE_PS_B_QUEEN (9 * NNUE_SQUARE_NB)
+#define NNUE_PS_KING (10 * NNUE_SQUARE_NB)
+#define NNUE_PS_NB (11 * NNUE_SQUARE_NB) // 704
+
+#define NNUE_DIMENSIONS ((NNUE_SQUARE_NB * NNUE_PS_NB) / 2) // 22528
+
+//
+// Convert a typhoon 0x88 COOR to Stockfish's flat 0-63 square index
+// (file + 8*(rank-1), rank/file both derived from the engine's own
+// RANK()/FILE() macros -- see docs/NNUE.md's spec for why this
+// one-line conversion is the single highest-risk spot in the whole
+// scheme).
+//
+static int
+CoorToSq64(COOR c)
+{
+ return (int)FILE(c) + 8 * ((int)RANK(c) - 1);
+}
+
+//
+// King bucket (0..31) for a king on Stockfish-square sq64, from
+// color uPerspective's (WHITE/BLACK) point of view. Verified against
+// Stockfish's literal KingBuckets[][] table -- see docs/NNUE.md.
+//
+static int
+KingBucket(ULONG uPerspective, int sq64)
+{
+ int file = sq64 & 7;
+ int rank0 = sq64 >> 3; // 0..7, rank-1
+ int fileQuartet = (file < 4) ? file : (7 - file); // 0..3, mirrored
+ int rankGroup = (uPerspective == WHITE) ? (7 - rank0) : rank0;
+ return rankGroup * 4 + fileQuartet;
+}
+
+//
+// Orientation XOR mask for perspective uPerspective whose own king
+// sits on Stockfish-square ksq64. Only ksq64's file selects the
+// branch (queenside half a-d vs. kingside half e-h); any rank.
+//
+static int
+OrientMask(ULONG uPerspective, int ksq64)
+{
+ int file = ksq64 & 7;
+ if (uPerspective == WHITE)
+ {
+ return (file < 4) ? 7 : 0; // SQ_H1 : SQ_A1
+ }
+ return (file < 4) ? 63 : 56; // SQ_H8 : SQ_A8
+}
+
+//
+// Piece-square plane offset for a piece of color uPieceColor and
+// type uPieceType (chess.h's PAWN..KING values), as seen from
+// perspective uPerspective. Both kings (own and enemy) share
+// NNUE_PS_KING.
+//
+static int
+PiecePlane(ULONG uPerspective, ULONG uPieceColor, ULONG uPieceType)
+{
+ FLAG fOwn;
+
+ if (uPieceType == KING)
+ {
+ return NNUE_PS_KING;
+ }
+ fOwn = (uPieceColor == uPerspective);
+ switch (uPieceType)
+ {
+ case PAWN: return fOwn ? NNUE_PS_W_PAWN : NNUE_PS_B_PAWN;
+ case KNIGHT: return fOwn ? NNUE_PS_W_KNIGHT : NNUE_PS_B_KNIGHT;
+ case BISHOP: return fOwn ? NNUE_PS_W_BISHOP : NNUE_PS_B_BISHOP;
+ case ROOK: return fOwn ? NNUE_PS_W_ROOK : NNUE_PS_B_ROOK;
+ case QUEEN: return fOwn ? NNUE_PS_W_QUEEN : NNUE_PS_B_QUEEN;
+ default:
+ ASSERT(FALSE);
+ return 0;
+ }
+}
+
+static int
+CompareInts(const void *a, const void *b)
+{
+ int x = *(const int *)a;
+ int y = *(const int *)b;
+ return (x > y) - (x < y);
+}
+
+//
+// Fill pIndices (caller-allocated, at least 32 ints) with the sorted
+// set of active HalfKAv2_hm feature indices for perspective
+// uPerspective at position pos. Returns the count (<=32).
+//
+static int
+ActiveFeatureIndices(POSITION *pos, ULONG uPerspective, int *pIndices)
+{
+ int f, r;
+ COOR cKing = ILLEGAL_COOR;
+ int ksq64;
+ int orientMask;
+ int kingBucketOffset;
+ int n = 0;
+
+ //
+ // Find this perspective's own king square.
+ //
+ for (f = 0; f < 8; f++)
+ {
+ for (r = 1; r <= 8; r++)
+ {
+ COOR c = FILE_RANK_TO_COOR(f, r);
+ PIECE p = pos->rgSquare[c].pPiece;
+ if (!IS_EMPTY(p) &&
+ (PIECE_TYPE(p) == KING) &&
+ (PIECE_COLOR(p) == uPerspective))
+ {
+ cKing = c;
+ }
+ }
+ }
+ ASSERT(cKing != ILLEGAL_COOR);
+ ksq64 = CoorToSq64(cKing);
+ orientMask = OrientMask(uPerspective, ksq64);
+ kingBucketOffset = KingBucket(uPerspective, ksq64) * NNUE_PS_NB;
+
+ for (f = 0; f < 8; f++)
+ {
+ for (r = 1; r <= 8; r++)
+ {
+ COOR c = FILE_RANK_TO_COOR(f, r);
+ PIECE p = pos->rgSquare[c].pPiece;
+ int sq64;
+ int idx;
+
+ if (IS_EMPTY(p))
+ {
+ continue;
+ }
+ sq64 = CoorToSq64(c);
+ idx = (sq64 ^ orientMask) +
+ PiecePlane(uPerspective, PIECE_COLOR(p), PIECE_TYPE(p)) +
+ kingBucketOffset;
+ ASSERT((idx >= 0) && (idx < NNUE_DIMENSIONS));
+ ASSERT(n < 32);
+ pIndices[n++] = idx;
+ }
+ }
+ qsort(pIndices, n, sizeof(int), CompareInts);
+ return n;
+}
+
+void
+DumpNnueFeatures(POSITION *pos)
+/**
+
+Routine description:
+
+ Prints the sorted set of active HalfKAv2_hm feature indices for
+ both perspectives at the current position, one line per
+ perspective, in a format trivial for a Python cross-checker to
+ parse:
+
+ NNUE_FEATURES white <idx> <idx> ...
+ NNUE_FEATURES black <idx> <idx> ...
+
+Parameters:
+
+ POSITION *pos : position to dump
+
+Return value:
+
+ void
+
+**/
+{
+ int rgIndices[32];
+ int n;
+ int i;
+
+ n = ActiveFeatureIndices(pos, WHITE, rgIndices);
+ Trace("NNUE_FEATURES white");
+ for (i = 0; i < n; i++)
+ {
+ Trace(" %d", rgIndices[i]);
+ }
+ Trace("\n");
+
+ n = ActiveFeatureIndices(pos, BLACK, rgIndices);
+ Trace("NNUE_FEATURES black");
+ for (i = 0; i < n; i++)
+ {
+ Trace(" %d", rgIndices[i]);
+ }
+ Trace("\n");
+}