| import random |
| import numpy as np |
| from collections import deque |
|
|
| import torch |
| import torch.nn as nn |
| import torch.optim as optim |
| from safetensors.torch import load_file as load_safetensors |
| from huggingface_hub import hf_hub_download |
|
|
|
|
| |
| |
| |
|
|
| class QNetwork(nn.Module): |
| """ |
| Deep Q-Network |
| """ |
|
|
| def __init__(self, state_dim, action_dim): |
| super(QNetwork, self).__init__() |
|
|
| self.model = nn.Sequential( |
| nn.Linear(state_dim, 128), |
| nn.ReLU(), |
|
|
| nn.Linear(128, 128), |
| nn.ReLU(), |
|
|
| nn.Linear(128, action_dim) |
| ) |
|
|
| def forward(self, x): |
| return self.model(x) |
|
|
|
|
| |
| |
| |
|
|
| class RLAgent: |
| """ |
| Deep Q-Learning Agent for: |
| Market Arbitrage + Grid-Aware Control |
| """ |
|
|
| def __init__( |
| self, |
| state_dim, |
| action_dim=21, |
| gamma=0.99, |
| lr=1e-3, |
| epsilon_start=1.0, |
| epsilon_min=0.05, |
| epsilon_decay=0.995, |
| buffer_size=100000, |
| batch_size=256, |
| target_update_freq=500 |
| ): |
|
|
| self.state_dim = state_dim |
| self.action_dim = action_dim |
|
|
| self.gamma = gamma |
| self.lr = lr |
|
|
| self.epsilon = epsilon_start |
| self.epsilon_min = epsilon_min |
| self.epsilon_decay = epsilon_decay |
|
|
| self.batch_size = batch_size |
| self.target_update_freq = target_update_freq |
|
|
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
|
|
| |
| self.action_values = np.linspace(-1, 1, action_dim) |
|
|
| |
| self.memory = deque(maxlen=buffer_size) |
|
|
| |
| self.policy_net = QNetwork(state_dim, action_dim).to(self.device) |
| self.target_net = QNetwork(state_dim, action_dim).to(self.device) |
| self.target_net.load_state_dict(self.policy_net.state_dict()) |
| self.target_net.eval() |
|
|
| self.optimizer = optim.Adam(self.policy_net.parameters(), lr=self.lr) |
| self.loss_fn = nn.MSELoss() |
|
|
| self.learn_step_counter = 0 |
|
|
| |
|
|
| def act(self, state): |
| """ |
| Epsilon-greedy action selection. |
| """ |
|
|
| if np.random.rand() < self.epsilon: |
| action_idx = random.randrange(self.action_dim) |
| else: |
| state = torch.FloatTensor(state).unsqueeze(0).to(self.device) |
| with torch.no_grad(): |
| q_values = self.policy_net(state) |
| action_idx = torch.argmax(q_values).item() |
|
|
| return self.action_values[action_idx] |
|
|
| |
|
|
| def store(self, state, action, reward, next_state, done): |
| """ |
| Store experience in replay buffer. |
| """ |
|
|
| action_idx = np.argmin(np.abs(self.action_values - action)) |
|
|
| self.memory.append((state, action_idx, reward, next_state, done)) |
|
|
| |
|
|
| def learn(self): |
| """ |
| Sample mini-batch and perform learning step. |
| """ |
|
|
| if len(self.memory) < self.batch_size: |
| return |
|
|
| batch = random.sample(self.memory, self.batch_size) |
| states, actions, rewards, next_states, dones = zip(*batch) |
|
|
| states = torch.FloatTensor(states).to(self.device) |
| actions = torch.LongTensor(actions).unsqueeze(1).to(self.device) |
| rewards = torch.FloatTensor(rewards).unsqueeze(1).to(self.device) |
| next_states = torch.FloatTensor(next_states).to(self.device) |
| dones = torch.FloatTensor(dones).unsqueeze(1).to(self.device) |
|
|
| |
| q_values = self.policy_net(states).gather(1, actions) |
|
|
| |
| with torch.no_grad(): |
| max_next_q = self.target_net(next_states).max(1)[0].unsqueeze(1) |
| q_target = rewards + (1 - dones) * self.gamma * max_next_q |
|
|
| loss = self.loss_fn(q_values, q_target) |
|
|
| self.optimizer.zero_grad() |
| loss.backward() |
| self.optimizer.step() |
|
|
| |
| self.learn_step_counter += 1 |
| if self.learn_step_counter % self.target_update_freq == 0: |
| self.target_net.load_state_dict(self.policy_net.state_dict()) |
|
|
| |
| if self.epsilon > self.epsilon_min: |
| self.epsilon *= self.epsilon_decay |
|
|
| |
|
|
| def save(self, path): |
| torch.save(self.policy_net.state_dict(), path) |
|
|
| |
|
|
| def load(self, path, use_safetensors=True): |
| if "/" in path and not os.path.exists(path): |
| |
| print(f"📥 Downloading model from Hugging Face: {path}") |
| repo_id = path |
| filename = "dqn_energy_agent.safetensors" if use_safetensors else "dqn_energy_agent.pth" |
| path = hf_hub_download(repo_id=repo_id, filename=filename) |
| |
| if path.endswith(".safetensors") or use_safetensors: |
| state_dict = load_safetensors(path) |
| else: |
| state_dict = torch.load(path, map_location=self.device) |
| |
| self.policy_net.load_state_dict(state_dict) |
| self.target_net.load_state_dict(self.policy_net.state_dict()) |
|
|