The evaluation script is provided below.

```python
# Evaluation script for OP-ACO problem
import os 
import sys
import traceback
import argparse
from typing import NamedTuple, Tuple, List, Annotated, Dict, Any
import numpy as np
import logging
import torch
from torch.distributions import Categorical
from torch import Tensor
import seed_solution as solution_module  # Note: solution module script is generated and saved on the fly


# =====Load function to evolve=====
problem = "op_aco"
heuristics = getattr(solution_module, "heuristics")  # Get function to evolve


# =====Utility functions=====
class OPInstance(NamedTuple):
    n: int
    coordinate: torch.Tensor
    distance: torch.Tensor
    prize: torch.Tensor
    maxlen: float

def gen_prizes(coordinates: Tensor):
    depot_coor = coordinates[0]
    distances = (coordinates - depot_coor).norm(p=2, dim=-1)
    prizes = 1 + torch.floor(99 * distances / distances.max())
    prizes /= prizes.max()
    return prizes

def gen_distance_matrix(coordinates):
    '''
    Args:
        _coordinates: torch tensor [n_nodes, 2] for node coordinates
    Returns:
        distance_matrix: torch tensor [n_nodes, n_nodes] for EUC distances
    '''
    n_nodes = len(coordinates)
    distances = torch.norm(coordinates[:, None] - coordinates, dim=2, p=2)
    distances[torch.arange(n_nodes), torch.arange(n_nodes)] = 1e9 # note here
    return distances

def get_max_len(n: int) -> float:
    threshold_list = [50, 100, 200, 300]
    maxlen = [3.0,4.0,5.0,6.0]
    for threshold, result in zip(threshold_list, maxlen):
        if n<=threshold:
            return result
    return 7.0

def load_dataset(fp) -> list[OPInstance]:
    data = np.load(fp)
    coordinates = data['coordinates']
    instances = []
    n = coordinates[0].shape[0]
    maxlen = get_max_len(n)
    for coord_np in coordinates:
        coord = torch.from_numpy(coord_np)
        distance = gen_distance_matrix(coord)
        prize = gen_prizes(coord)
        instance = OPInstance(n, coord, distance, prize, maxlen)
        instances.append(instance)
    return instances


# =====ACO class=====
class ACO():
    def __init__(self,
                 prizes,
                 distances,
                 max_len,
                 heuristic,
                 n_ants=20,
                 decay=0.9,
                 alpha=1,
                 beta=1,
                 device='cpu',
                 ):
        self.n = len(prizes)
        self.distances = distances
        self.prizes = prizes
        self.max_len = max_len
        
        self.n_ants = n_ants
        self.decay = decay
        self.alpha = alpha
        self.beta = beta
        
        self.pheromone = torch.ones_like(self.distances)
        self.heuristic = heuristic
        
        self.Q = 1 / prizes.sum()
        
        self.alltime_best_sol = None
        self.alltime_best_obj = 0

        self.device = device
        self.add_dummy_node()
        
    def add_dummy_node(self):
        '''
        One has to sparsify the graph first before adding dummy node
        distance: 
                [[1e9 , x   , x   , 0  ],
                [x   , 1e9 , x   , 0  ],
                [x   , x   , 1e9 , 0  ],
                [1e10, 1e10, 1e10, 0  ]]
        pheromone: [1]
        heuristic: [>0]
        prizes: [x,x,...,0]
        '''
        self.prizes = torch.cat((self.prizes, torch.tensor([1e-10], device=self.device)))
        distances = torch.cat((self.distances, 1e10 * torch.ones(size=(1, self.n), device=self.device)), dim=0)
        self.distances = torch.cat((distances, 1e-10 + torch.zeros(size=(self.n+1, 1), device=self.device)), dim=1)

        self.heuristic = torch.cat((self.heuristic, torch.zeros(size=(1, self.n), device=self.device)), dim=0) # cannot reach other nodes from dummy node
        self.heuristic = torch.cat((self.heuristic, torch.ones(size=(self.n+1, 1), device=self.device)), dim=1)

        self.pheromone = torch.ones_like(self.distances)
        self.distances[self.distances == 1e-10] = 0
        self.prizes[-1] = 0

    @torch.no_grad()
    def run(self, n_iterations):
        for _ in range(n_iterations):
            sols = self.gen_sol()
            objs = self.gen_sol_obj(sols)
            sols = sols.T
            best_obj, best_idx = objs.max(dim=0)
            if best_obj > self.alltime_best_obj:
                self.alltime_best_obj = best_obj
                self.alltime_best_sol = sols[best_idx]
            self.update_pheronome(sols, objs, best_obj, best_idx)
        return self.alltime_best_obj, self.alltime_best_sol
       
    @torch.no_grad()
    def update_pheronome(self, sols, objs, best_obj, best_idx):
        self.pheromone = self.pheromone * self.decay
        for i in range(self.n_ants):
            sol = sols[i]
            obj = objs[i]
            self.pheromone[sol[:-1], torch.roll(sol, shifts=-1)[:-1]] += self.Q * obj
                
    @torch.no_grad()
    def gen_sol_obj(self, solutions):
        '''
        Args:
            solutions: (max_len, n_ants)
        '''
        objs = self.prizes[solutions.T].sum(dim=1)
        return objs

    def gen_sol(self):
        '''
        Solution contruction for all ants
        '''
        solutions = []
        solutions = [torch.zeros(size=(self.n_ants,), device=self.device, dtype=torch.int64)]
        mask = torch.ones(size=(self.n_ants, self.n+1), device=self.device)
        done = torch.zeros(size=(self.n_ants,), device=self.device)
        travel_dis = torch.zeros(size=(self.n_ants,), device=self.device)
        cur_node = torch.zeros(size=(self.n_ants,), dtype=torch.int64, device=self.device)
        
        mask = self.update_mask(travel_dis, cur_node, mask)
        done = self.check_done(mask)
        # construction
        while not done:
            nxt_node = self.pick_node(mask, cur_node) # pick action
            # update solution and log_probs
            solutions.append(nxt_node) 
            # update travel_dis, cur_node and mask
            travel_dis += self.distances[cur_node, nxt_node]
            cur_node = nxt_node
            mask = self.update_mask(travel_dis, cur_node, mask)
            # check done
            done = self.check_done(mask)
        return torch.stack(solutions)
    
    def pick_node(self, mask, cur_node):
        pheromone = self.pheromone[cur_node] # shape: (n_ants, p_size+1)
        heuristic = self.heuristic[cur_node] # shape: (n_ants, p_size+1)
        dist = ((pheromone ** self.alpha) * (heuristic ** self.beta) * mask)
        dist = Categorical(dist)
        item = dist.sample()
        return item  # (n_ants,)
    
    def update_mask(self, travel_dis, cur_node, mask):
        '''
        Args:
            travel_dis: (n_ants,)
            cur_node: (n_ants,)
            mask: (n_ants, n+1)
        '''
        mask[torch.arange(self.n_ants), cur_node] = 0

        for ant_id in range(self.n_ants):
            if cur_node[ant_id] != self.n: # if not at dummy node
                _mask = mask[ant_id]
                candidates = torch.nonzero(_mask).squeeze()
                # after going to candidate node from cur_node, can it return to depot?
                trails = travel_dis[ant_id] + self.distances[cur_node[ant_id], candidates] + self.distances[candidates, 0]
                fail_idx = candidates[trails > self.max_len]
                _mask[fail_idx] = 0
                
        mask[:, -1] = 0 # mask the dummy node for all ants
        go2dummy = (mask[:, :-1] == 0).all(dim=1) # unmask the dummy node for these ants
        mask[go2dummy, -1] = 1
        return mask
    
    def check_done(self, mask):
        # is all masked ?
        return (mask[:, :-1] == 0).all()


# =====Evaluation function=====
N_ITERATIONS = 50
N_ANTS = 20

def evaluate_heuristic(inst: OPInstance):
    heu = heuristics(np.array(inst.prize), np.array(inst.distance), inst.maxlen) + 1e-9
    assert tuple(heu.shape) == (inst.n, inst.n)
    heu[heu < 1e-9] = 1e-9
    heu = torch.from_numpy(heu)
    aco = ACO(inst.prize, inst.distance, inst.maxlen, heu, N_ANTS)
    obj, _ = aco.run(N_ITERATIONS)
    return obj


# =====Helper functions=====
def get_feature(metrics: Dict[int, float]) -> Tuple[int, ...]:
    """
    Convert the metrics dict to a feature vector

    Args:
        metrics (dict): A mapping of test problem size (int) to a score (float).

    Returns:
        (tuple): a tuple of discretized scores sorted by problem size
    """
    scores = metrics.values()
    features = tuple([int(x) for x in scores])
    return features
    
def get_score(metrics: Dict[int, float]) -> float:
    """
    Convert the metrics dict to a score

    Args:
        metrics (dict): A mapping of test problem size (int) to a score (float).

    Returns:
        (float): a score
    """
    return sum(metrics.values()) / len(metrics)


# =====Main function=====
if __name__ == "__main__":
    # -----Parse command line arguments (same for all problems)-----
    parser = argparse.ArgumentParser(description='Evaluation script.')
    parser.add_argument(
        '--root_dir',
        type=str,
        default=os.getcwd(),
        help='Project root directory for loading data (default: current working directory)'
    )
    parser.add_argument(
        '--file_output_prefix',
        type=str,
        default='',
        help='Output file prefix for saving evaluation results. '
             'Absolute path recommended. Files saved as {prefix}filename '
             '(default: empty string, saves to current directory)')
    parser.add_argument(
        '--mode',
        type=str,
        default='val',
        choices=['train', 'val'],
        help='Execution mode: train or val (default: val)'
    )
    parser.add_argument(
        '--problem_size',
        type=int,
        default=50,  # Customize this to your needs
        help='Problem size parameter'
    )
    # Parse arguments
    args = parser.parse_args()
    root_dir = args.root_dir
    file_output_prefix = args.file_output_prefix
    mode = args.mode
    problem_size = args.problem_size
    method = 'aco'
    # Print parsed arguments for verification
    print(f"root_dir: {root_dir}")
    print(f"file_output_prefix: {file_output_prefix}")
    print(f"mode: {mode}")
    #print(f"problem_size: {problem_size}")
    
    # -----Run the evaluation-----
    # Run instances 50, 100, 200; execution time: 
    try:
        basepath = os.path.join(root_dir, "problems", problem)
        if not os.path.isfile(os.path.join(basepath, f"dataset/train50_dataset.npz")):
            raise FileNotFoundError(f"Dataset not found: {basepath}")
        
        if mode == 'train':
            dataset_path = os.path.join(basepath, f"dataset/{mode}{problem_size}_dataset.npz")
            dataset = load_dataset(dataset_path)
            n_instances = len(dataset)

            print(f"[*] Dataset loaded: {dataset_path} with {n_instances} instances.")
            
            objs = []
            for i, instance in enumerate(dataset):
                obj = evaluate_heuristic(instance)
                print(f"[*] Instance {i}: {obj}")
                objs.append(obj.item())
            
            print("[*] Average:")
            print(np.mean(objs))

        else: # mode == 'val'
            metrics = {}
            for problem_size in [50, 100]:  # options: 50, 100, 200
                dataset_path = os.path.join(basepath, f"dataset/{mode}{problem_size}_dataset.npz")
                dataset = load_dataset(dataset_path)
                logging.info(f"[*] Evaluating {dataset_path}")

                objs = []
                for i, instance in enumerate(dataset):
                    obj = evaluate_heuristic(instance)
                    objs.append(obj.item())
                
                print(f"[*] Average for {problem_size}: {np.mean(objs)}")
                metrics[problem_size] = np.mean(objs)
                
        if metrics:
            features = get_feature(metrics)
            score = get_score(metrics)
        else:
            features = None
            score = None

        # -----Print results to stdout (same for all problems)-----
        print('__SANDBOX_RESULT__')        
        print('__METRICS_START__')
        print(repr(metrics))
        print('__METRICS_END__')
        
        print('__FEATURES_START__')
        print(repr(features))
        print('__FEATURES_END__')
        
        print('__SCORE_START__')
        print(repr(score))
        print('__SCORE_END__')
        
        print('__SANDBOX_SUCCESS__')
        
    except Exception as e:
        print('__SANDBOX_ERROR__:')
        print(f'Error type: {type(e).__name__}')
        print(f'Error message: {str(e)}')
        print('Full traceback:')
        traceback.print_exc()
```