summaryrefslogtreecommitdiff
path: root/src/fhmodel.c
blob: b3935bc6cc3ddceee1f1734004bcf0f3cf980e44 (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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// fhmodel.c -- production inference for the FH% logistic-regression
// models. See fhmodel.h and docs/REGRESSION.md.
//
// Fully quantized: every runtime computation here is integer
// fixed-point (Q16 for table/final-sum arithmetic, Q30 for the
// handful of raw-feature weights -- see fhmodel_weights.h's header
// comment for why two scales). No floats, no log()/exp() calls.
#include "chess.h"
#include "fhmodel_weights.h"

// Mate-score window gate -- see fhmodel_weights.h's
// FHM_MATE_GAP_THRESHOLD (matches load_records.py's
// MATE_GAP_THRESHOLD exactly) and docs/REGRESSION.md's mate-window
// note. A `gap` this large means the static eval is itself encoding
// a detected mate score compared against an ordinary cp-scale
// window -- a structurally different regime this model was never
// trained on (those rows were excluded entirely, not just clipped)
// and must not extrapolate into.

static INLINE INT
_FhmLog1pLookup(const INT *table, ULONG uMaxIndex, ULONG uValue)
{
    ULONG uClamped = (uValue > uMaxIndex) ? uMaxIndex : uValue;
    return(table[uClamped]);
}

// Converts a Q16 fixed-point logit into the sigmoid*100000 return
// convention, via table lookup only (no exp() call). Linearly maps
// the clamped logit into the table's index range.
static ULONG
_FhmLogitQ16ToThousandths(INT iLogitQ16)
{
    INT64 iRangeQ16 = (INT64)(FHM_SIGMOID_LOGIT_RANGE * (1 << FHM_Q_TABLE));
    INT64 iClamped = iLogitQ16;
    INT64 iIndex;

    if (iClamped < -iRangeQ16) iClamped = -iRangeQ16;
    if (iClamped > iRangeQ16) iClamped = iRangeQ16;

    // index = (clamped + range) / (2*range) * (TABLE_SIZE - 1)
    iIndex = ((iClamped + iRangeQ16) * (FHM_SIGMOID_TABLE_SIZE - 1)) /
             (2 * iRangeQ16);
    if (iIndex < 0) iIndex = 0;
    if (iIndex >= FHM_SIGMOID_TABLE_SIZE) iIndex = FHM_SIGMOID_TABLE_SIZE - 1;

    return((ULONG)g_rgiSigmoidThousandthsQ16[iIndex]);
}

// Shared node-level logit (Q16), common to both tables' identical
// 7-feature node-level set. iGap is the caller's own "how far past
// the relevant edge" quantity (always >= 0 by construction -- see
// callers), already confirmed <= FHM_MATE_GAP_THRESHOLD by the
// caller's own gate before this is ever reached.
static INT
_FhmNodeLogitQ16(const INT *rgiLogGap, const INT *rgiLogGapOverflow,
                  const INT *rgiLogDepth, const INT *rgiLogMaterial,
                  const INT *rgiNodeTypeWeight,
                  INT iWImprove, INT iWEnemyImprove, INT iWImproveXEnemy,
                  ULONG uGapCap, SCORE iGap, ULONG uDepth,
                  SCORE iImprovement, SCORE iEnemyImprovement,
                  ULONG uTotalNonPawnMaterial, NODE_TYPE eNodeTypeGuess)
{
    ULONG uGapNonNeg = (iGap > 0) ? (ULONG)iGap : 0;
    ULONG uGapClipped = (uGapNonNeg < uGapCap) ? uGapNonNeg : uGapCap;
    ULONG uGapOverflow = (uGapNonNeg > uGapCap) ? (uGapNonNeg - uGapCap) : 0;
    ULONG uDepthPlies = uDepth / ONE_PLY;
    INT64 iSumQ16 = 0;

    iSumQ16 += _FhmLog1pLookup(rgiLogGap, uGapCap, uGapClipped);
    iSumQ16 += _FhmLog1pLookup(rgiLogGapOverflow,
                                FHM_MATE_GAP_THRESHOLD - uGapCap, uGapOverflow);
    iSumQ16 += _FhmLog1pLookup(rgiLogDepth, FHM_DEPTH_PLIES_MAX, uDepthPlies);
    iSumQ16 += _FhmLog1pLookup(rgiLogMaterial, FHM_MATERIAL_MAX, uTotalNonPawnMaterial);
    iSumQ16 += rgiNodeTypeWeight[(ULONG)eNodeTypeGuess];

    // Raw (non-tabulated) terms: Q30 weight * raw integer value,
    // rescaled down to the shared Q16 accumulation frame.
    iSumQ16 += ((INT64)iWImprove * (INT64)iImprovement) >> FHM_RAW_SHIFT;
    iSumQ16 += ((INT64)iWEnemyImprove * (INT64)iEnemyImprovement) >> FHM_RAW_SHIFT;
    iSumQ16 += ((INT64)iWImproveXEnemy *
                ((INT64)iImprovement * (INT64)iEnemyImprovement)) >> FHM_RAW_SHIFT;

    return((INT)iSumQ16);
}

FHM_NODE_BASE
FhModelAlphaNodeBaseQ16(IN SCORE iEval,
                         IN SCORE iAlpha,
                         IN ULONG uDepth,
                         IN SCORE iImprovement,
                         IN SCORE iEnemyImprovement,
                         IN ULONG uTotalNonPawnMaterial,
                         IN NODE_TYPE eNodeTypeGuess)
{
    SCORE iGap = (SCORE)(iAlpha - iEval);
    INT iLogitQ16;

    if (iGap > FHM_MATE_GAP_THRESHOLD)
    {
        return((FHM_NODE_BASE)FHM_MATE_GATED);
    }
    iLogitQ16 = _FhmNodeLogitQ16(
        g_rgiAlphaLogGapQ16, g_rgiAlphaLogGapOverflowQ16,
        g_rgiAlphaLogDepthQ16, g_rgiAlphaLogMaterialQ16,
        g_rgiAlphaNodeTypeWeightQ16,
        FHM_ALPHA_W_IMPROVE_Q30, FHM_ALPHA_W_ENEMY_IMPROVE_Q30,
        FHM_ALPHA_W_IMPROVE_X_ENEMY_Q30,
        FHM_GAP_CAP_ALPHA, iGap, uDepth, iImprovement, iEnemyImprovement,
        uTotalNonPawnMaterial, eNodeTypeGuess);
    iLogitQ16 += FHM_ALPHA_INTERCEPT_Q16;
    return((FHM_NODE_BASE)iLogitQ16);
}

ULONG
FhModelAlphaMoveThousandths(IN FHM_NODE_BASE nodeBase,
                            IN SCORE iMoveValue,
                            IN ULONG uMoveOrdinal,
                            IN ULONG uMoveFhPctThousandths,
                            IN ULONG uMoveFhAttempts,
                            IN FH_COLLECT_TIER eTier)
{
    INT64 iSumQ16 = (INT64)(INT)nodeBase;

    iSumQ16 += _FhmLog1pLookup(g_rgiAlphaLogOrdinalQ16, FHM_ORDINAL_MAX, uMoveOrdinal);
    iSumQ16 += _FhmLog1pLookup(g_rgiAlphaLogFhAttemptsQ16, FHM_FH_ATTEMPTS_MAX, uMoveFhAttempts);
    iSumQ16 += g_rgiAlphaTierWeightQ16[(ULONG)eTier];
    iSumQ16 += ((INT64)FHM_ALPHA_W_MOVE_VALUE_Q30 * (INT64)iMoveValue) >> FHM_RAW_SHIFT;
    iSumQ16 += ((INT64)FHM_ALPHA_W_MOVE_FH_PCT_Q30 * (INT64)uMoveFhPctThousandths) >> FHM_RAW_SHIFT;

    return(_FhmLogitQ16ToThousandths((INT)iSumQ16));
}

// Whole-node-only -- see fhmodel.h. Move-level terms fixed at exactly
// the defaults a FHC_TIER_NO_MOVE training row had (fhcollect.c's
// FhCollectNodeExit: iMoveValue=0, uMoveOrdinal=0,
// uMoveFhPctThousandths=0, uMoveFhAttempts=0) -- log1p(0)==0 and
// 0*weight==0 for all of them, so this collapses to the node logit +
// tier weight + intercept, but computed via the same lookup path as
// the alpha function for consistency.
ULONG
FhModelBetaPredictedFHThousandths(IN SCORE iEval,
                                   IN SCORE iBeta,
                                   IN ULONG uDepth,
                                   IN SCORE iImprovement,
                                   IN SCORE iEnemyImprovement,
                                   IN ULONG uTotalNonPawnMaterial,
                                   IN NODE_TYPE eNodeTypeGuess)
{
    SCORE iGap = (SCORE)(iEval - iBeta);
    INT64 iSumQ16;

    if (iGap > FHM_MATE_GAP_THRESHOLD)
    {
        return(MOVE_FH_ESTIMATE_NO_DATA);
    }
    iSumQ16 = _FhmNodeLogitQ16(
        g_rgiBetaLogGapQ16, g_rgiBetaLogGapOverflowQ16,
        g_rgiBetaLogDepthQ16, g_rgiBetaLogMaterialQ16,
        g_rgiBetaNodeTypeWeightQ16,
        FHM_BETA_W_IMPROVE_Q30, FHM_BETA_W_ENEMY_IMPROVE_Q30,
        FHM_BETA_W_IMPROVE_X_ENEMY_Q30,
        FHM_GAP_CAP_BETA, iGap, uDepth, iImprovement, iEnemyImprovement,
        uTotalNonPawnMaterial, eNodeTypeGuess);
    iSumQ16 += g_rgiBetaTierWeightQ16[(ULONG)FHC_TIER_NO_MOVE];
    iSumQ16 += FHM_BETA_INTERCEPT_Q16;

    return(_FhmLogitQ16ToThousandths((INT)iSumQ16));
}

// See fhmodel.h's comment: this engine's own convention for "is this
// the hash move" is x==0 (confirmed by grep -- there's no separately
// stored mvHash compared against mv anywhere in search.c's move
// loop), so this does NOT reuse FhClassifyMoveTier's mv-vs-mvHash
// comparison (which would need a real mvHash, not available here) --
// it inlines the same capture/killer/countermove decode logic that
// function uses for everything past the hash check.
FH_COLLECT_TIER
FhModelTierForMove(IN ULONG x, IN MOVE mv, IN SCORE iMoveSortValue)
{
    FLAG fIsCaptureOrPromotion;

    if (0 == x)
    {
        return(FHC_TIER_HASH);
    }
    fIsCaptureOrPromotion = IS_CAPTURE_OR_PROMOTION(mv);
    if (0 != fIsCaptureOrPromotion)
    {
        return((iMoveSortValue >= SORT_THESE_FIRST) ?
               FHC_TIER_GOOD_CAPTURE : FHC_TIER_LOSING_CAPTURE);
    }
    if (0 != (iMoveSortValue & (FIRST_KILLER | SECOND_KILLER | THIRD_KILLER | FOURTH_KILLER)))
    {
        return(FHC_TIER_KILLER);
    }
    if (COUNTERMOVE_TIER_BONUS == (iMoveSortValue & COUNTERMOVE_TIER_BONUS))
    {
        return(FHC_TIER_COUNTERMOVE);
    }
    return(FHC_TIER_LEFTOVER_QUIET);
}