|
|
""" |
|
|
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) |
|
|
""" |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
for square, piece in board.piece_map().items(): |
|
|
rank, file = divmod(square, 8) |
|
|
channel = piece_to_channel[piece.piece_type] |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
""" |
|
|
|
|
|
input_tensor = self.fen_to_12_channel_tensor(board) |
|
|
|
|
|
|
|
|
outputs = self.session.run( |
|
|
[self.output_name], |
|
|
{self.input_name: input_tensor} |
|
|
) |
|
|
|
|
|
|
|
|
raw_value = float(outputs[0][0][0]) |
|
|
|
|
|
|
|
|
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_eval = self.evaluate_neural(board) |
|
|
|
|
|
|
|
|
material_eval = self.evaluate_material(board) |
|
|
|
|
|
|
|
|
hybrid_eval = 0.90 * neural_eval + 0.10 * material_eval |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
board.push(chess.Move.null()) |
|
|
opponent_mobility = board.legal_moves.count() |
|
|
board.pop() |
|
|
|
|
|
|
|
|
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) |