| | """ |
| | 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) |