In the below function 'select_action', the 'action_mean', from the computation 'action_mean, action_log_std = actor(state_tensor)' gives the result between -1,1. After that we are computing 'normal_dist' using the 'normal_dist = torch.distributions.Normal(action_mean, action_std), action = normal_dist.sample()', here we are getting the results in wide ranges of negative and positive sides, then after clipping the 'action' from normal distribution leads the action space values only in the boundaries either bounds_low or bounds_high, I am not getting the ranges of values between the bounds for all variables. what should we do now. Is there any method to rectify my existing problem or update me any new approach on actor, critic and select_action functions, to resolve my existing issue.

bounds_low = np.array([0.18e-6, 0.18e-6, 0.18e-6, 0.18e-6, 0.18e-6, 0.5e-6, 0.5e-6, 0.5e-6, 0.5e-6, 0.5e-6, 15e-6, 0.1e-12, 0.8])
bounds_high = np.array([0.2e-6, 0.2e-6, 0.2e-6, 0.2e-6, 0.2e-6, 50e-6, 50e-6, 50e-6, 50e-6, 50e-6, 30e-6, 10e-12, 1.4])
target_metrics = {
    'Area': (1e-12, 1e-10),
    'PowerDissipation': (0.005, 0.25),
    'SlewRate': (20, 60),
    'Gain': (70, 100),
    'Bandwidth3dB': (30e3, 100e3),
    'UnityGainFreq': (30e6, 100e6),
    'PhaseMargin': (65, 90)
}

# Initialize Networks and Optimizers
state_size = 45  # State size, Normalized continuous variables (13), One-hot encoded regions (24), binary saturation state (1), normalized performance metrics (7)
action_size = 13  # Action size to tune the 13 variables
hidden_sizes_actor = [128, 128]
hidden_sizes_critic = [128, 128]
action_std_init = 0.5 
learning_rate_actor = 1e-4 
learning_rate_critic = 1e-3 

# Define the Actor and Critic Networks
class ActorNetwork(nn.Module):
    def __init__(self, state_size, action_size, hidden_sizes, action_std_init):
        super(ActorNetwork, self).__init__()
        self.fc_in = nn.Linear(state_size, hidden_sizes[0])
        self.hidden_layers = nn.ModuleList()
        for i in range(len(hidden_sizes)-1):
            self.hidden_layers.append(nn.Linear(hidden_sizes[i], hidden_sizes[i+1]))
        self.fc_out = nn.Linear(hidden_sizes[-1], action_size)
        self.log_std = nn.Parameter(torch.ones(1, action_size) * torch.log(torch.tensor(action_std_init))) # Use log to ensure std > 0 # Parameter for exploration
        #self.activation = nn.ReLU()
        self.activation = nn.ELU()  # Change ReLU to ELU
        
        # Initialize weights
        self.init_weights()

    def forward(self, state):
        x = self.activation(self.fc_in(state))
        for hidden_layer in self.hidden_layers:
            x = self.activation(hidden_layer(x))
        action_mean = self.fc_out(x)
        log_std = self.log_std.exp().expand_as(action_mean) # Ensure the right shape
        return action_mean, log_std
    
    def init_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight)  # Improved initialization
                nn.init.constant_(m.bias, 0)

class CriticNetwork(nn.Module):
    def __init__(self, state_size, hidden_sizes):
        super(CriticNetwork, self).__init__()
        self.fc_in = nn.Linear(state_size, hidden_sizes[0])
        self.hidden_layers = nn.ModuleList()
        for i in range(len(hidden_sizes)-1):
            self.hidden_layers.append(nn.Linear(hidden_sizes[i], hidden_sizes[i+1]))
        self.fc_out = nn.Linear(hidden_sizes[-1], 1)
        #self.activation = nn.ReLU()
        self.activation = nn.ELU()  # Change ReLU to ELU
        
        # Initialize weights
        self.init_weights()

    def forward(self, state):
        x = self.activation(self.fc_in(state))
        for hidden_layer in self.hidden_layers:
            x = self.activation(hidden_layer(x))
        value = self.fc_out(x)
        return value
    
    def init_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight)  # Improved initialization
                nn.init.constant_(m.bias, 0)

# Select Action Function
def select_action(state, actor, critic, bounds_low, bounds_high, performance_metrics):
    state_tensor = torch.from_numpy(state).float().unsqueeze(0)
    action_mean, action_log_std = actor(state_tensor)
    print("action_mean",action_mean)
    action_std = action_log_std.exp()
    
    # Create a normal distribution and sample an action
    normal_dist = torch.distributions.Normal(action_mean, action_std)
    action = normal_dist.sample()
    print("action normal dist", action)
    log_prob = normal_dist.log_prob(action).sum(dim=1, keepdim=True)
    
    # Clip your actions to their limits
    #action = action.clamp(torch.tensor(bounds_low).unsqueeze(0), torch.tensor(bounds_high).unsqueeze(0))
    action = action.clamp(torch.tensor(bounds_low).unsqueeze(0), torch.tensor(bounds_high).unsqueeze(0) - torch.tensor(bounds_low).unsqueeze(0))      
   
    # Estimate the value of the state using the critic network
    value = critic(state_tensor)
    
    return action.numpy(), log_prob, value.squeeze(), performance_metrics