| """ |
| Nexus-Nano Evaluator |
| Ultra-lightweight 2.8M parameter CNN |
| |
| Research: MobileNet architecture principles for efficiency |
| """ |
|
|
| import onnxruntime as ort |
| import numpy as np |
| import chess |
| import logging |
| from pathlib import Path |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class NexusNanoEvaluator: |
| """ |
| Lightweight evaluator for Nexus-Nano |
| Optimized for speed over accuracy |
| """ |
| |
| 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 = 1): |
| """Initialize with single-threaded ONNX session for speed""" |
| |
| 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-Nano model...") |
| 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_tensor(self, board: chess.Board) -> np.ndarray: |
| """ |
| Fast 12-channel tensor conversion |
| Optimized for minimal overhead |
| """ |
| tensor = np.zeros((1, 12, 8, 8), dtype=np.float32) |
| |
| |
| piece_channels = { |
| 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_channels[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: |
| """ |
| Fast neural evaluation |
| Single forward pass, minimal post-processing |
| """ |
| input_tensor = self.fen_to_tensor(board) |
| outputs = self.session.run([self.output_name], {self.input_name: input_tensor}) |
| |
| |
| raw_value = float(outputs[0][0][0]) |
| |
| |
| return raw_value * 300.0 |
| |
| def evaluate_material(self, board: chess.Board) -> int: |
| """Fast material count""" |
| material = 0 |
| |
| for piece_type, value in self.PIECE_VALUES.items(): |
| if piece_type == chess.KING: |
| continue |
| white = len(board.pieces(piece_type, chess.WHITE)) |
| black = len(board.pieces(piece_type, chess.BLACK)) |
| material += (white - black) * value |
| |
| return material |
| |
| def evaluate_hybrid(self, board: chess.Board) -> float: |
| """ |
| Fast hybrid: 85% neural + 15% material |
| Higher material weight for stability in fast games |
| """ |
| neural = self.evaluate_neural(board) |
| material = self.evaluate_material(board) |
| |
| hybrid = 0.85 * neural + 0.15 * material |
| |
| if board.turn == chess.BLACK: |
| hybrid = -hybrid |
| |
| return hybrid |
| |
| def get_model_size_mb(self) -> float: |
| """Get model size""" |
| return self.model_path.stat().st_size / (1024 * 1024) |