In my reinforcement learning algorithm implementation, i need to pass the intermediate reward to the agent, wen we get 'reward = -200' or 'reward >= 90'. 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?. 

In the below existing code i have implemented the reward function, but the implemented reward is only given to the agent after the maximum step size of that current episode, but i need to give intermediate reward with in the episode to guide the agent more effectively.

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:
    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
    
    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
        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]
            
            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']
        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):
	#instantiation         
              
    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')        
   
if __name__ == "__main__":
    # Initialize environment parameters
    # Define the values for bounds, and target metrics

    env = CircuitEnvironment(bounds_low, bounds_high)
    agent = PPOAgent(state_dim, action_space, lr_actor, lr_critic, gamma, gae_lambda, epsilon, policy_clip, epochs, entropy_coef)

    train(env, agent, num_episodes, max_timesteps)