Rafs-an09002 commited on
Commit
330cbbe
·
verified ·
1 Parent(s): f3b6150

Create engine/evaluate.py

Browse files
Files changed (1) hide show
  1. engine/evaluate.py +193 -0
engine/evaluate.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Nexus-Core Position Evaluator
3
+ Pure ResNet-20 CNN with 12-channel input
4
+
5
+ Research References:
6
+ - He et al. (2016) - Deep Residual Learning for Image Recognition
7
+ - Silver et al. (2017) - AlphaZero position evaluation
8
+ """
9
+
10
+ import onnxruntime as ort
11
+ import numpy as np
12
+ import chess
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import Dict
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class NexusCoreEvaluator:
21
+ """
22
+ Nexus-Core neural network evaluator
23
+ 12-channel CNN input (simpler than Synapse-Base)
24
+ """
25
+
26
+ # Stockfish piece values for material calculation
27
+ PIECE_VALUES = {
28
+ chess.PAWN: 100,
29
+ chess.KNIGHT: 320,
30
+ chess.BISHOP: 330,
31
+ chess.ROOK: 500,
32
+ chess.QUEEN: 900,
33
+ chess.KING: 0
34
+ }
35
+
36
+ def __init__(self, model_path: str, num_threads: int = 2):
37
+ """Initialize evaluator with ONNX model"""
38
+
39
+ self.model_path = Path(model_path)
40
+ if not self.model_path.exists():
41
+ raise FileNotFoundError(f"Model not found: {model_path}")
42
+
43
+ # ONNX Runtime session
44
+ sess_options = ort.SessionOptions()
45
+ sess_options.intra_op_num_threads = num_threads
46
+ sess_options.inter_op_num_threads = num_threads
47
+ sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
48
+ sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
49
+
50
+ logger.info(f"Loading Nexus-Core model from {model_path}...")
51
+ self.session = ort.InferenceSession(
52
+ str(self.model_path),
53
+ sess_options=sess_options,
54
+ providers=['CPUExecutionProvider']
55
+ )
56
+
57
+ self.input_name = self.session.get_inputs()[0].name
58
+ self.output_name = self.session.get_outputs()[0].name
59
+
60
+ logger.info(f"✅ Model loaded: {self.input_name} -> {self.output_name}")
61
+
62
+ def fen_to_12_channel_tensor(self, board: chess.Board) -> np.ndarray:
63
+ """
64
+ Convert board to 12-channel tensor
65
+ Channels: 6 white pieces + 6 black pieces
66
+
67
+ Args:
68
+ board: chess.Board object
69
+
70
+ Returns:
71
+ numpy array of shape (1, 12, 8, 8)
72
+ """
73
+ tensor = np.zeros((1, 12, 8, 8), dtype=np.float32)
74
+
75
+ piece_to_channel = {
76
+ chess.PAWN: 0,
77
+ chess.KNIGHT: 1,
78
+ chess.BISHOP: 2,
79
+ chess.ROOK: 3,
80
+ chess.QUEEN: 4,
81
+ chess.KING: 5
82
+ }
83
+
84
+ # Fill piece positions
85
+ for square, piece in board.piece_map().items():
86
+ rank, file = divmod(square, 8)
87
+ channel = piece_to_channel[piece.piece_type]
88
+
89
+ # White pieces: channels 0-5
90
+ # Black pieces: channels 6-11
91
+ if piece.color == chess.BLACK:
92
+ channel += 6
93
+
94
+ tensor[0, channel, rank, file] = 1.0
95
+
96
+ return tensor
97
+
98
+ def evaluate_neural(self, board: chess.Board) -> float:
99
+ """
100
+ Neural network evaluation
101
+
102
+ Args:
103
+ board: chess.Board object
104
+
105
+ Returns:
106
+ Evaluation score (centipawns from white's perspective)
107
+ """
108
+ # Convert to tensor
109
+ input_tensor = self.fen_to_12_channel_tensor(board)
110
+
111
+ # Run inference
112
+ outputs = self.session.run(
113
+ [self.output_name],
114
+ {self.input_name: input_tensor}
115
+ )
116
+
117
+ # Extract value (tanh output in range [-1, 1])
118
+ raw_value = float(outputs[0][0][0])
119
+
120
+ # Convert to centipawns (scale by 400)
121
+ centipawns = raw_value * 400.0
122
+
123
+ return centipawns
124
+
125
+ def evaluate_material(self, board: chess.Board) -> int:
126
+ """
127
+ Classical material evaluation
128
+
129
+ Args:
130
+ board: chess.Board object
131
+
132
+ Returns:
133
+ Material balance in centipawns
134
+ """
135
+ material = 0
136
+
137
+ for piece_type in [chess.PAWN, chess.KNIGHT, chess.BISHOP,
138
+ chess.ROOK, chess.QUEEN]:
139
+ white_count = len(board.pieces(piece_type, chess.WHITE))
140
+ black_count = len(board.pieces(piece_type, chess.BLACK))
141
+
142
+ material += (white_count - black_count) * self.PIECE_VALUES[piece_type]
143
+
144
+ return material
145
+
146
+ def evaluate_hybrid(self, board: chess.Board) -> float:
147
+ """
148
+ Hybrid evaluation: 90% neural + 10% material
149
+
150
+ Args:
151
+ board: chess.Board object
152
+
153
+ Returns:
154
+ Final evaluation score
155
+ """
156
+ # Neural evaluation (primary)
157
+ neural_eval = self.evaluate_neural(board)
158
+
159
+ # Material evaluation (safety check)
160
+ material_eval = self.evaluate_material(board)
161
+
162
+ # Blend: 90% neural, 10% material
163
+ hybrid_eval = 0.90 * neural_eval + 0.10 * material_eval
164
+
165
+ # Flip for black's perspective
166
+ if board.turn == chess.BLACK:
167
+ hybrid_eval = -hybrid_eval
168
+
169
+ return hybrid_eval
170
+
171
+ def evaluate_mobility(self, board: chess.Board) -> int:
172
+ """
173
+ Mobility evaluation (number of legal moves)
174
+
175
+ Args:
176
+ board: chess.Board object
177
+
178
+ Returns:
179
+ Mobility score
180
+ """
181
+ current_mobility = board.legal_moves.count()
182
+
183
+ # Flip turn to count opponent mobility
184
+ board.push(chess.Move.null())
185
+ opponent_mobility = board.legal_moves.count()
186
+ board.pop()
187
+
188
+ # Mobility difference
189
+ return (current_mobility - opponent_mobility) * 5
190
+
191
+ def get_model_size_mb(self) -> float:
192
+ """Get model size in MB"""
193
+ return self.model_path.stat().st_size / (1024 * 1024)