In my reinforcement learning algorithm implementation, i need to pass the intermediate reward to the agent when the condition 'metric_name == 'gain' and c_value < 0' satisfies or the condition 'c_value == target_value' satisfies for two or more than two metrics. First i need to know whether my intention to implement the intermediate reward is correct or wrong, and i need to know to feedback the intermediate reward to the agent for the particular specific condition conditions satisfies, and i need to know whether we can send both penalty and reward as intermediate reward to the agent or not?. if my intention is good to implement for the above required condition how to implement in the below given code.

# Define Circuit Environment
class CircuitEnvironment:
    PERFORMANCE_METRICS_TARGET_LOW = np.array([45, 10e3, 5e6, 60])
    PERFORMANCE_METRICS_TARGET_HIGH = np.array([55, 30e3, 10e6, 75])
    VERY_CLOSE_REWARD = 40
    LARGE_REWARD = 10
    VERY_HIGH_REWARD = 50
    PENALTY = 20
    SEVERE_PENALTY = 200

    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 
    
    def reset(self, target_metrics=None):
	#existing implementation
        return node_features_tensor, edge_index, performance_metrics    

    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 = self.round_to_discrete_values(performance_metrics)
        rounded_previous_metrics = self.round_to_discrete_values(previous_metrics)
        print("performance_metrics", rounded_performance_metrics)
        
        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 = {k: self.target_metrics[i] for i, k in enumerate(metrics_order)}
        print("target_metrics", target_metrics)
        for metric_name in metrics_order:
            c_value = c_metrics[metric_name]
            p_value = p_metrics[metric_name]
            target_value = target_metrics[metric_name]
            
            # First condition: Penalty if 'gain' metric is negative
            if metric_name == 'gain' and c_value < 0:
                reward -= self.SEVERE_PENALTY
                return reward

            # Second condition: Check if c_value is exactly the target value
            if c_value == target_value:
                reward += self.VERY_HIGH_REWARD
                continue

            # Third condition: Check if c_value is very close to the target value
            if metric_name == 'gain' and abs(c_value - target_value) <= 1:
                reward += self.VERY_CLOSE_REWARD
                continue
            elif metric_name == 'bw' and abs(c_value - target_value) <= 1000:
                reward += self.VERY_CLOSE_REWARD
                continue
            elif metric_name == 'ugf' and abs(c_value - target_value) <= 1000000:
                reward += self.VERY_CLOSE_REWARD
                continue
            elif metric_name == 'pm' and abs(c_value - target_value) <= 1:
                reward += self.VERY_CLOSE_REWARD
                continue

            # Fourth condition: Check if c_value is closer to target value than p_value
            if abs(c_value - target_value) < abs(p_value - target_value):
                reward += self.LARGE_REWARD
            else:
                # Fifth condition: Apply penalty if none of the above conditions are met
                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']
        target_metrics = {k: self.target_metrics[i] for i, k in enumerate(metrics_order)}
        for metric_name in metrics_order:
            if performance_metrics[metric_name] != target_metrics[metric_name]:
                return False
        return True

# PPOAgent definition
class PPOAgent:
    def __init__(self, state_dim, action_space, lr_actor, lr_critic, gamma, gae_lambda, epsilon, policy_clip, epochs, entropy_coef):
        self.gamma = gamma
        self.gae_lambda = gae_lambda
        self.epsilon = epsilon
        self.policy_clip = policy_clip
        self.epochs = epochs
        self.entropy_coef = entropy_coef
        
        gnn_model_actor = CustomGNN(in_channels=state_dim, out_channels=23)
        gnn_model_critic = CustomGNN(in_channels=state_dim, out_channels=23)
        
        self.actor = Actor(gnn_model_actor)
        self.critic = Critic(gnn_model_critic)
        self.optimizer_actor = optim.Adam(self.actor.parameters(), lr=lr_actor)
        self.optimizer_critic = optim.Adam(self.critic.parameters(), lr=lr_critic)
        self.action_space = action_space         
              
    def select_action(self, state, performance_metrics):               
	#existing implementation
        return rounded_actions, log_probs.detach(), performance_metrics

    def compute_gae(self, next_value, rewards, masks, values):
        returns = []
        gae = 0
        for step in reversed(range(len(rewards))):
            delta = rewards[step] + self.gamma * next_value * masks[step] - values[step]
            gae = delta + self.gamma * self.gae_lambda * masks[step] * gae
            returns.insert(0, gae + values[step])
            next_value = values[step]
        return returns

    def update_policy(self, prev_states, prev_actions, prev_log_probs, returns, advantages):
        device = next(self.actor.parameters()).device  
        advantages = torch.tensor(advantages, dtype=torch.float32, device=device)
        returns = torch.tensor(returns, dtype=torch.float32, device=device)
        prev_log_probs = torch.tensor(prev_log_probs, dtype=torch.float32, device=device)        
        
        # Initialize lists to accumulate losses
        actor_losses = []
        critic_losses = []
        
        for _ in range(self.epochs):
            log_probs_batch, state_values_batch, entropies_batch = [], [], []
            for state, action in zip(prev_states, prev_actions):
                node_features_tensor, edge_index = state
                node_features_tensor = node_features_tensor.to(device)
                edge_index = edge_index.to(device) if edge_index is not None else None
                
                action_means, action_stds = self.actor(state)
                
                # Getting the state value prediction from the critic using the flattened state
                state_value = self.critic(node_features_tensor, edge_index).unsqueeze(0)
                
                dist = torch.distributions.Normal(action_means, action_stds)
                action_tensor = torch.tensor(action, dtype=torch.float32, device=action_means.device)
                log_prob = dist.log_prob(action_tensor).sum(axis=-1).unsqueeze(0)                
                log_probs_batch.append(log_prob)
                
                entropy = dist.entropy().sum(axis=-1).unsqueeze(0)
                entropies_batch.append(entropy)
                
                state_values_batch.append(state_value)
             
            # Convert to tensors
            log_probs = torch.cat(log_probs_batch).to(prev_log_probs.device)
            state_values = torch.cat(state_values_batch).squeeze(-1).to(returns.device) 
            entropy = torch.cat(entropies_batch)
            
            # Surrogate function
            ratios = torch.exp(log_probs - prev_log_probs)            
            surr1 = ratios * advantages
            surr2 = torch.clamp(ratios, 1.0 - self.policy_clip, 1.0 + self.policy_clip) * advantages
                        
            actor_loss = - torch.min(surr1, surr2).mean() - self.entropy_coef * entropy.mean()
            critic_loss = F.mse_loss(state_values, returns)
            
            # Update actor
            self.optimizer_actor.zero_grad()
            actor_loss.backward(retain_graph=True)
            torch.nn.utils.clip_grad_norm_(self.actor.parameters(), max_norm=1.0)
            self.optimizer_actor.step()
            
            # Update critic
            self.optimizer_critic.zero_grad()
            critic_loss.backward()
            torch.nn.utils.clip_grad_norm_(self.critic.parameters(), max_norm=1.0)
            self.optimizer_critic.step()
            
            # Append the item loss to the lists
            actor_losses.append(actor_loss.item())  
            critic_losses.append(critic_loss.item())
            
        # Calculate average loss over all epochs
        average_loss_actor = sum(actor_losses) / len(actor_losses)
        average_loss_critic = sum(critic_losses) / len(critic_losses)
        
        return average_loss_actor, average_loss_critic

# Training Function
def train(env, agent, num_episodes, max_timesteps):
    num_targets = 100
    train_targets = CircuitEnvironment.generate_random_targets(num_targets=num_targets, seed=0)
    test_targets = CircuitEnvironment.generate_random_targets(num_targets=20, seed=1)
    
    # Initialize the episode to start from and log lists
    start_episode = 0    
    rewards_log = []
    losses_log = []
    metrics_log = []
    episode_lengths = []
    
    # Attempt to load the saved state
    saved_state = load_training_state()
        
    for episode in range(start_episode, num_episodes):
        if episode >= num_targets:
            break  # Exit the loop if exhausted the number of targets
        
        target = train_targets[episode % len(train_targets)]
        
        node_features_tensor, edge_index, performance_metrics = env.reset(target)
        state = (node_features_tensor, edge_index)
    
        episode_rewards = []
        states = []
        actions = []
        log_probs = []
        values = []
        masks = []
        perf_metrics_log = []

        for t in range(max_timesteps):            
            action, log_prob, perf_metrics = agent.select_action(state, performance_metrics)
            next_node_features_tensor, next_edge_index, reward, done, previous_metrics = env.step(action, perf_metrics, target)
            
            next_state = (next_node_features_tensor, next_edge_index)
    
            episode_rewards.append(reward)
            states.append(state)
            actions.append(action)
            log_probs.append(log_prob.item())            
            values.append(agent.critic(node_features_tensor, edge_index).item())
            masks.append(1.0 - float(done))
            perf_metrics_log.append(perf_metrics)
                
            performance_metrics = previous_metrics
            state = next_state            
  
            if done:
                break
    
        next_value = agent.critic(state[0], state[1]).item() if not done else 0         
        returns = agent.compute_gae(next_value, episode_rewards, masks, values)
    
        advantages = (torch.tensor(returns) - torch.tensor(values)).detach().numpy()
        returns = torch.tensor(returns).detach().numpy()
        average_loss_actor, average_loss_critic = agent.update_policy(states, actions, log_probs, returns, advantages)
    
        agent.save_checkpoint(episode)        

        save_training_state(agent, episode, rewards_log, losses_log, metrics_log, train_targets, test_targets)
    
    agent.save_checkpoint('final')        
   
    logging.info("Training Completed.")