In the below given functions 'calculate_reward' and 'check_done' with in the class 'CircuitEnvironment' i need to compute the,
-where i need to give reward/penalties whether the output meets the target or not(not based on the ranges with in the targets are reached or not)
-by comparing the results of 'c_metrics'(present obtained output performance metrics for present raw action inputs) and 'p_metrics'(previous obtained output performance metrics for previous raw action inputs), if any c_metrics reach the 'target'(which is selected for present training or testing process) based on the count we need to give very high reward else if 'c_metrics' is more nearer to the target than the 'p_metrics' then we give reward else 'p_metrics' is more nearer to the target than the 'c_metrics' then we give high penalty.
- and also we need to modify the existing function 'check_done' by checking all the 'performance_metrics' were reached the target(corresponding to training or testing) then only return true.
in the computation whether we really need the abs(), because the 'gain', and the 'pm' results are some times in negative values if we do abs() will it cause undesired result in the computation process, or can we directly compare the metrics values.


# Define Circuit Environment
class CircuitEnvironment:
    PERFORMANCE_METRICS_TARGET_LOW = np.array([48, 30e3, 10e6, 60])
    PERFORMANCE_METRICS_TARGET_HIGH = np.array([100, 100e3, 100e6, 75])
    LARGE_REWARD = 10
    VERY_HIGH_REWARD = 50
    PENALTY = 20

    def __init__(self, bounds_low, bounds_high):
        # Initialization code: setup bounds
        self.bounds_low = bounds_low
        self.bounds_high = bounds_high
        # Initialize tensors as None
        self.node_features_tensor = None
        self.edge_index = None   
        self.target_metrics = None

    @staticmethod
    def generate_random_targets(num_targets, seed):
        np.random.seed(seed)
        target_ranges = CircuitEnvironment.PERFORMANCE_METRICS_TARGET_HIGH - CircuitEnvironment.PERFORMANCE_METRICS_TARGET_LOW
        random_targets = np.random.rand(num_targets, 4) * target_ranges + CircuitEnvironment.PERFORMANCE_METRICS_TARGET_LOW
        return random_targets.astype(int)
    
   def step(self, raw_actions, previous_metrics, target_metrics):
        raw_actions = np.squeeze(raw_actions)
        performance_metrics, temp_netlist_path = run_simulation(raw_actions)
        node_features_tensor, edge_index = netlist_to_graph(temp_netlist_path)
        self.target_metrics = target_metrics
        
        # Round off the previous and current performance metrics to integer values for comparison
        rounded_performance_metrics = {k: int(round(v)) for k, v in performance_metrics.items()}
        rounded_previous_metrics = {k: int(round(v)) for k, v in previous_metrics.items()}
        
        reward = self.calculate_reward(rounded_performance_metrics, rounded_previous_metrics)
        done = self.check_done(rounded_performance_metrics)
        # Update previous state
        previous_metrics = rounded_performance_metrics 
        self.node_features_tensor = node_features_tensor
        self.edge_index = edge_index
        print(f"Reward:", reward)
        logging.info(f"Reward: {reward}")
        return node_features_tensor, edge_index, reward, done, previous_metrics

    def calculate_reward(self, c_metrics, p_metrics):
        metrics_order = ['gain', 'bw', 'ugf', 'pm']
            
        reward = 0
        
        target_metrics = np.array([self.target_metrics[k] for k in metrics_order])       
        c_metrics_array = np.array([c_metrics[k] for k in metrics_order])
        p_metrics_array = np.array([p_metrics[k] for k in metrics_order])
        
        for idx, metric_name in enumerate(metrics_order):
            c_value = c_metrics_array[idx]
            p_value = p_metrics_array[idx]
            target_value = target_metrics[idx]
            
            if c_value == target_value:
                reward += self.VERY_HIGH_REWARD
            elif abs(c_value - target_value) < abs(p_value - target_value):
                reward += self.LARGE_REWARD
            else:
                reward -= self.PENALTY
                
        return reward

        
    def check_done(self, performance_metrics):
        if self.target_metrics is None:
            return False
        metrics_order = ['gain', 'bw', 'ugf', 'pm']
        for idx, metric_name in enumerate(metrics_order):
            if performance_metrics[metric_name] != self.target_metrics[metric_name]:
                return False
        return True