Rafs-an09002's picture
Create engine/evaluate.py
330cbbe verified
"""
Nexus-Core Position Evaluator
Pure ResNet-20 CNN with 12-channel input
Research References:
- He et al. (2016) - Deep Residual Learning for Image Recognition
- Silver et al. (2017) - AlphaZero position evaluation
"""
import onnxruntime as ort
import numpy as np
import chess
import logging
from pathlib import Path
from typing import Dict
logger = logging.getLogger(__name__)
class NexusCoreEvaluator:
"""
Nexus-Core neural network evaluator
12-channel CNN input (simpler than Synapse-Base)
"""
# Stockfish piece values for material calculation
PIECE_VALUES = {
chess.PAWN: 100,
chess.KNIGHT: 320,
chess.BISHOP: 330,
chess.ROOK: 500,
chess.QUEEN: 900,
chess.KING: 0
}
def __init__(self, model_path: str, num_threads: int = 2):
"""Initialize evaluator with ONNX model"""
self.model_path = Path(model_path)
if not self.model_path.exists():
raise FileNotFoundError(f"Model not found: {model_path}")
# ONNX Runtime session
sess_options = ort.SessionOptions()
sess_options.intra_op_num_threads = num_threads
sess_options.inter_op_num_threads = num_threads
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
logger.info(f"Loading Nexus-Core model from {model_path}...")
self.session = ort.InferenceSession(
str(self.model_path),
sess_options=sess_options,
providers=['CPUExecutionProvider']
)
self.input_name = self.session.get_inputs()[0].name
self.output_name = self.session.get_outputs()[0].name
logger.info(f"✅ Model loaded: {self.input_name} -> {self.output_name}")
def fen_to_12_channel_tensor(self, board: chess.Board) -> np.ndarray:
"""
Convert board to 12-channel tensor
Channels: 6 white pieces + 6 black pieces
Args:
board: chess.Board object
Returns:
numpy array of shape (1, 12, 8, 8)
"""
tensor = np.zeros((1, 12, 8, 8), dtype=np.float32)
piece_to_channel = {
chess.PAWN: 0,
chess.KNIGHT: 1,
chess.BISHOP: 2,
chess.ROOK: 3,
chess.QUEEN: 4,
chess.KING: 5
}
# Fill piece positions
for square, piece in board.piece_map().items():
rank, file = divmod(square, 8)
channel = piece_to_channel[piece.piece_type]
# White pieces: channels 0-5
# Black pieces: channels 6-11
if piece.color == chess.BLACK:
channel += 6
tensor[0, channel, rank, file] = 1.0
return tensor
def evaluate_neural(self, board: chess.Board) -> float:
"""
Neural network evaluation
Args:
board: chess.Board object
Returns:
Evaluation score (centipawns from white's perspective)
"""
# Convert to tensor
input_tensor = self.fen_to_12_channel_tensor(board)
# Run inference
outputs = self.session.run(
[self.output_name],
{self.input_name: input_tensor}
)
# Extract value (tanh output in range [-1, 1])
raw_value = float(outputs[0][0][0])
# Convert to centipawns (scale by 400)
centipawns = raw_value * 400.0
return centipawns
def evaluate_material(self, board: chess.Board) -> int:
"""
Classical material evaluation
Args:
board: chess.Board object
Returns:
Material balance in centipawns
"""
material = 0
for piece_type in [chess.PAWN, chess.KNIGHT, chess.BISHOP,
chess.ROOK, chess.QUEEN]:
white_count = len(board.pieces(piece_type, chess.WHITE))
black_count = len(board.pieces(piece_type, chess.BLACK))
material += (white_count - black_count) * self.PIECE_VALUES[piece_type]
return material
def evaluate_hybrid(self, board: chess.Board) -> float:
"""
Hybrid evaluation: 90% neural + 10% material
Args:
board: chess.Board object
Returns:
Final evaluation score
"""
# Neural evaluation (primary)
neural_eval = self.evaluate_neural(board)
# Material evaluation (safety check)
material_eval = self.evaluate_material(board)
# Blend: 90% neural, 10% material
hybrid_eval = 0.90 * neural_eval + 0.10 * material_eval
# Flip for black's perspective
if board.turn == chess.BLACK:
hybrid_eval = -hybrid_eval
return hybrid_eval
def evaluate_mobility(self, board: chess.Board) -> int:
"""
Mobility evaluation (number of legal moves)
Args:
board: chess.Board object
Returns:
Mobility score
"""
current_mobility = board.legal_moves.count()
# Flip turn to count opponent mobility
board.push(chess.Move.null())
opponent_mobility = board.legal_moves.count()
board.pop()
# Mobility difference
return (current_mobility - opponent_mobility) * 5
def get_model_size_mb(self) -> float:
"""Get model size in MB"""
return self.model_path.stat().st_size / (1024 * 1024)