From d7feebab50e7f07b385ab590375aace6fe88f1b9 Mon Sep 17 00:00:00 2001 From: chenxiaodong Date: Tue, 25 Jun 2024 11:02:53 +0800 Subject: [PATCH] llm --- .idea/DRL-for-Energy-Systems.iml | 2 +- .idea/misc.xml | 2 +- AgentPPO/loss_data.pkl | Bin 54098 -> 86 bytes AgentPPO/reward_data.pkl | Bin 53249 -> 76 bytes AgentPPO/test_data.pkl | Bin 9909 -> 9926 bytes PPO.py | 11 +- PPO_llm.py | 6 +- PPO_test.py | 360 +++++++++++++++++++++++++++++++ test.py | 31 +-- tools.py | 17 +- tools备份.py | 285 ++++++++++++++++++++++++ 11 files changed, 681 insertions(+), 33 deletions(-) create mode 100644 PPO_test.py create mode 100644 tools备份.py diff --git a/.idea/DRL-for-Energy-Systems.iml b/.idea/DRL-for-Energy-Systems.iml index ab54ddc..0a5fd4b 100644 --- a/.idea/DRL-for-Energy-Systems.iml +++ b/.idea/DRL-for-Energy-Systems.iml @@ -2,7 +2,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 9aaae3a..94f4964 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/AgentPPO/loss_data.pkl b/AgentPPO/loss_data.pkl index 0a336c80da903eab286cf508844e52852350b65a..ed9087b52cb242ca2e332f272ebfeeddb7344381 100644 GIT binary patch delta 48 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 PD@B8dv)w$c!BE1R-ms?ZS1n?QA9Qm&D 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 A(f|Me diff --git a/PPO.py b/PPO.py index 33986ad..2fd8f0e 100644 --- a/PPO.py +++ b/PPO.py @@ -331,11 +331,12 @@ if __name__ == '__main__': buffer = list() '''init training parameters''' num_episode = args.num_episode - # args.train=False - # args.save_network=False - # args.test_network=False - # args.save_test_data=False - # args.compare_with_gurobi=False + # args.train = False + # args.save_network = False + # args.test_network = False + # args.save_test_data = False + # args.compare_with_gurobi = False + # args.plot_on = False if args.train: for i_episode in range(num_episode): with torch.no_grad(): diff --git a/PPO_llm.py b/PPO_llm.py index cef9381..419afb2 100644 --- a/PPO_llm.py +++ b/PPO_llm.py @@ -140,9 +140,9 @@ class AgentPPO: trajectory_temp = list() last_done = 0 for i in range(target_step): - action = self.get_llm_action(i) - noise = 0 - # action, noise = self.select_action(state) + # action = self.get_llm_action(i) + # noise = 0 + action, noise = self.select_action(state) state, next_state, reward, done, = env.step(np.tanh(action)) # make action between -1 & 1 trajectory_temp.append((state, reward, done, action, noise)) if done: diff --git a/PPO_test.py b/PPO_test.py new file mode 100644 index 0000000..763542e --- /dev/null +++ b/PPO_test.py @@ -0,0 +1,360 @@ +import json +import matplotlib.pyplot as plt +from environment import ESSEnv +import os +import pickle +from copy import deepcopy +import numpy as np +import pandas as pd +import torch +import torch.nn as nn + +os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE' + + +class ActorPPO(nn.Module): + def __init__(self, mid_dim, state_dim, action_dim, layer_norm=False): + super().__init__() + self.layer_norm = layer_norm + self.net = nn.Sequential(nn.Linear(state_dim, mid_dim), nn.ReLU(), + nn.Linear(mid_dim, mid_dim), nn.ReLU(), + nn.Linear(mid_dim, mid_dim), nn.Hardswish(), + nn.Linear(mid_dim, action_dim)) + # the logarithm (log) of standard deviation (std) of action, it is a trainable parameter + self.a_logstd = nn.Parameter(torch.zeros((1, action_dim)) - 0.5, requires_grad=True) + self.sqrt_2pi_log = np.log(np.sqrt(2 * np.pi)) + + if self.layer_norm: + self.apply_layer_norm() + + def apply_layer_norm(self): + def init_weights(layer): + if isinstance(layer, nn.Linear): + nn.init.orthogonal_(layer.weight, 1.0) + nn.init.constant_(layer.bias, 0.0) + + self.net.apply(init_weights) + + def forward(self, state): + return self.net(state).tanh() # action.tanh() limit the data output of action + + def get_action(self, state): + a_avg = self.forward(state) # too big for the action + a_std = self.a_logstd.exp() + + noise = torch.randn_like(a_avg) + action = a_avg + noise * a_std + return action, noise + + def get_logprob_entropy(self, state, action): + a_avg = self.forward(state) + a_std = self.a_logstd.exp() + + delta = ((a_avg - action) / a_std).pow(2) * 0.5 + logprob = -(self.a_logstd + self.sqrt_2pi_log + delta).sum(1) # new_logprob + + dist_entropy = (logprob.exp() * logprob).mean() # policy entropy + return logprob, dist_entropy + + def get_old_logprob(self, _action, noise): # noise = action - a_noise + delta = noise.pow(2) * 0.5 + return -(self.a_logstd + self.sqrt_2pi_log + delta).sum(1) # old_logprob + + +class CriticAdv(nn.Module): + def __init__(self, mid_dim, state_dim, _action_dim, layer_norm=False): + super().__init__() + self.layer_norm = layer_norm + self.net = nn.Sequential(nn.Linear(state_dim, mid_dim), nn.ReLU(), + nn.Linear(mid_dim, mid_dim), nn.ReLU(), + nn.Linear(mid_dim, mid_dim), nn.Hardswish(), + nn.Linear(mid_dim, 1)) + if self.layer_norm: + self.apply_layer_norm() + + def apply_layer_norm(self): + def init_weights(layer): + if isinstance(layer, nn.Linear): + nn.init.orthogonal_(layer.weight, 1.0) + nn.init.constant_(layer.bias, 0.0) + + self.net.apply(init_weights) + + def forward(self, state): + return self.net(state) # Advantage value + + +class AgentPPO: + def __init__(self): + super().__init__() + self.state = None + self.device = None + self.action_dim = None + self.get_obj_critic = None + + self.criterion = torch.nn.SmoothL1Loss() + self.cri = self.cri_target = self.if_use_cri_target = self.cri_optim = self.ClassCri = None + self.act = self.act_target = self.if_use_act_target = self.act_optim = self.ClassAct = None + + '''init modify''' + self.ClassCri = CriticAdv + self.ClassAct = ActorPPO + + self.ratio_clip = 0.2 # ratio.clamp(1 - clip, 1 + clip) + self.lambda_entropy = 0.02 # could be 0.01~0.05 + self.lambda_gae_adv = 0.98 # could be 0.95~0.99, GAE (Generalized Advantage Estimation. ICLR.2016.) + self.get_reward_sum = None # self.get_reward_sum_gae if if_use_gae else self.get_reward_sum_raw + self.trajectory_list = None + + def init(self, net_dim, state_dim, action_dim, learning_rate=1e-4, if_use_gae=False, gpu_id=0, layer_norm=False): + self.device = torch.device(f"cuda:{gpu_id}" if (torch.cuda.is_available() and (gpu_id >= 0)) else "cpu") + self.trajectory_list = list() + # choose whether to use gae or not + self.get_reward_sum = self.get_reward_sum_gae if if_use_gae else self.get_reward_sum_raw + + self.cri = self.ClassCri(net_dim, state_dim, action_dim, layer_norm).to(self.device) + self.act = self.ClassAct(net_dim, state_dim, action_dim, layer_norm).to( + self.device) if self.ClassAct else self.cri + self.cri_target = deepcopy(self.cri) if self.if_use_cri_target else self.cri + self.act_target = deepcopy(self.act) if self.if_use_act_target else self.act + + self.cri_optim = torch.optim.Adam(self.cri.parameters(), learning_rate) + self.act_optim = torch.optim.Adam(self.act.parameters(), learning_rate) if self.ClassAct else self.cri + + def select_action(self, state): + states = torch.as_tensor((state,), dtype=torch.float32, device=self.device) + actions, noises = self.act.get_action(states) + return actions[0].detach().cpu().numpy(), noises[0].detach().cpu().numpy() + + @staticmethod + def get_llm_action(index): + with open('data/llm_action.json', 'r') as file: + data = json.load(file) + data_tensor = torch.tensor(data, dtype=torch.float32) + normalized_index = index % len(data_tensor) + action = data_tensor[normalized_index].detach().cpu().numpy() + return action + + def explore_env(self, env, target_step): + state = self.state # sent state to agent and then agent sent state to method + trajectory_temp = list() + last_done = 0 + for i in range(target_step): + # action = self.get_llm_action(i) + # noise = 0 + action, noise = self.select_action(state) + state, next_state, reward, done, = env.step(np.tanh(action)) # make action between -1 & 1 + trajectory_temp.append((state, reward, done, action, noise)) + if done: + state = env.reset() + last_done = i + else: + state = next_state + self.state = state + + '''splice list''' + # store 0 trajectory information to list + trajectory_list = self.trajectory_list + trajectory_temp[:last_done + 1] + self.trajectory_list = trajectory_temp[last_done:] + return trajectory_list + + def update_net(self, buffer, batch_size, repeat_times, soft_update_tau): + """put data extract and update network together""" + with torch.no_grad(): + buf_len = buffer[0].shape[0] + # decompose buffer data + buf_state, buf_action, buf_noise, buf_reward, buf_mask = [ten.to(self.device) for ten in buffer] + + '''get buf_r_sum, buf_logprob''' + bs = 4096 # set a smaller 'BatchSize' when out of GPU memory: 1024, could change to 4096 + buf_value = [self.cri_target(buf_state[i:i + bs]) for i in range(0, buf_len, bs)] + buf_value = torch.cat(buf_value, dim=0) + buf_logprob = self.act.get_old_logprob(buf_action, buf_noise) + + buf_r_sum, buf_advantage = self.get_reward_sum(buf_len, buf_reward, buf_mask, buf_value) # detach() + # normalize advantage + buf_advantage = (buf_advantage - buf_advantage.mean()) / (buf_advantage.std() + 1e-5) + del buf_noise, buffer[:] + + '''PPO: Surrogate objective of Trust Region''' + obj_critic = obj_actor = None + for _ in range(int(buf_len / batch_size * repeat_times)): + indices = torch.randint(buf_len, size=(batch_size,), requires_grad=False, device=self.device) + + state = buf_state[indices] + action = buf_action[indices] + r_sum = buf_r_sum[indices] + logprob = buf_logprob[indices] + advantage = buf_advantage[indices] + + new_logprob, obj_entropy = self.act.get_logprob_entropy(state, action) # it is obj_actor + ratio = (new_logprob - logprob.detach()).exp() + surrogate1 = advantage * ratio + surrogate2 = advantage * ratio.clamp(1 - self.ratio_clip, 1 + self.ratio_clip) + obj_surrogate = -torch.min(surrogate1, surrogate2).mean() + obj_actor = obj_surrogate + obj_entropy * self.lambda_entropy + self.optim_update(self.act_optim, obj_actor) # update actor + + value = self.cri(state).squeeze(1) # critic network predicts the reward_sum (Q value) of state + # use smoothloss L1 to evaluate the value loss + # obj_critic = self.criterion(value, r_sum) / (r_sum.std() + 1e-6) + obj_critic = self.criterion(value, r_sum) + self.optim_update(self.cri_optim, obj_critic) # calculate and update the back propogation of value loss + # choose whether to use soft update + self.soft_update(self.cri_target, self.cri, soft_update_tau) if self.cri_target is not self.cri else None + + a_std_log = getattr(self.act, 'a_std_log', torch.zeros(1)) + return obj_critic.item(), obj_actor.item(), a_std_log.mean().item() # logging_tuple + + def get_reward_sum_raw(self, buf_len, buf_reward, buf_mask, buf_value) -> (torch.Tensor, torch.Tensor): + buf_r_sum = torch.empty(buf_len, dtype=torch.float32, device=self.device) # reward sum + + pre_r_sum = 0 + for i in range(buf_len - 1, -1, -1): + buf_r_sum[i] = buf_reward[i] + buf_mask[i] * pre_r_sum + pre_r_sum = buf_r_sum[i] + buf_advantage = buf_r_sum - (buf_mask * buf_value[:, 0]) + return buf_r_sum, buf_advantage + + def get_reward_sum_gae(self, buf_len, ten_reward, ten_mask, ten_value) -> (torch.Tensor, torch.Tensor): + buf_r_sum = torch.empty(buf_len, dtype=torch.float32, device=self.device) # old policy value + buf_advantage = torch.empty(buf_len, dtype=torch.float32, device=self.device) # advantage value + + pre_r_sum = 0 + pre_advantage = 0 # advantage value of previous step + for i in range(buf_len - 1, -1, -1): + buf_r_sum[i] = ten_reward[i] + ten_mask[i] * pre_r_sum + pre_r_sum = buf_r_sum[i] + buf_advantage[i] = ten_reward[i] + ten_mask[i] * (pre_advantage - ten_value[i]) # fix a bug here + pre_advantage = ten_value[i] + buf_advantage[i] * self.lambda_gae_adv + return buf_r_sum, buf_advantage + + @staticmethod + def optim_update(optimizer, objective): + optimizer.zero_grad() + objective.backward() + optimizer.step() + + @staticmethod + def soft_update(target_net, current_net, tau): + for tar, cur in zip(target_net.parameters(), current_net.parameters()): + tar.data.copy_(cur.data.__mul__(tau) + tar.data.__mul__(1.0 - tau)) + + +class Arguments: + def __init__(self, agent=None, env=None): + self.agent = agent # Deep Reinforcement Learning algorithm + self.env = env # the environment for training + self.cwd = None # current work directory. None means set automatically + self.if_remove = False # remove the cwd folder? (True, False, None:ask me) + self.visible_gpu = '0' # for example: os.environ['CUDA_VISIBLE_DEVICES'] = '0, 2,' + # self.worker_num = 2 # rollout workers number pre GPU (adjust it to get high GPU usage) + self.num_threads = 32 # cpu_num for evaluate model, torch.set_num_threads(self.num_threads) + + '''Arguments for training''' + self.num_episode = 1000 # to control the train episodes for PPO + self.gamma = 0.995 # discount factor of future rewards + self.learning_rate = 2 ** -14 # 2e-4 + self.soft_update_tau = 2 ** -8 # 2 ** -8 ~= 5e-3 + + self.net_dim = 256 # the network width + self.batch_size = 4096 # num of transitions sampled from replay buffer. + self.repeat_times = 2 ** 3 # collect target_step, then update network + self.target_step = 4096 # repeatedly update network to keep critic's loss small + self.max_memo = self.target_step # capacity of replay buffer + self.if_per_or_gae = False # GAE for on-policy sparse reward: Generalized Advantage Estimation. + + '''Arguments for evaluate''' + self.random_seed = 0 # initialize random seed in self.init_before_training() + # self.random_seed_list = [1234, 2234, 3234, 4234, 5234] + self.random_seed_list = [1234] + self.train = True + self.save_network = True + self.test_network = True + self.save_test_data = True + self.compare_with_gurobi = True + self.plot_on = True + + def init_before_training(self, if_main): + if self.cwd is None: + agent_name = self.agent.__class__.__name__ + self.cwd = f'./{agent_name}' + + if if_main: + import shutil # remove history according to bool(if_remove) + if self.if_remove is None: + self.if_remove = bool(input(f"| PRESS 'y' to REMOVE: {self.cwd}? ") == 'y') + elif self.if_remove: + shutil.rmtree(self.cwd, ignore_errors=True) + print(f"| Remove cwd: {self.cwd}") + os.makedirs(self.cwd, exist_ok=True) + + np.random.seed(self.random_seed) + torch.manual_seed(self.random_seed) + torch.set_num_threads(self.num_threads) + torch.set_default_dtype(torch.float32) + + os.environ['CUDA_VISIBLE_DEVICES'] = str(self.visible_gpu) + + +def update_buffer(_trajectory): + _trajectory = list(map(list, zip(*_trajectory))) # 2D-list transpose, here cut the trajectory into 5 parts + ten_state = torch.as_tensor(_trajectory[0]) # tensor state here + ten_reward = torch.as_tensor(_trajectory[1], dtype=torch.float32) + # _trajectory[2] = done, replace done by mask, save memory + ten_mask = (1.0 - torch.as_tensor(_trajectory[2], dtype=torch.float32)) * gamma + ten_action = torch.as_tensor(_trajectory[3]) + ten_noise = torch.as_tensor(_trajectory[4], dtype=torch.float32) + + buffer[:] = (ten_state, ten_action, ten_noise, ten_reward, ten_mask) # list store tensors + + _steps = ten_reward.shape[0] # how many steps are collected in all trajectories + _r_exp = ten_reward.mean() # the mean reward + return _steps, _r_exp + + +def load_actions_from_json(file_path): + with open(file_path, 'r') as file: + actions = json.load(file) + return actions + + +def simulate_with_llm_actions(env, llm_actions): + states, actions, rewards, unbalances = [], [], [], [] + state = env.reset() + + for action in llm_actions: + next_state, reward, done, info = env.step(action) + states.append(state) + actions.append(action) + rewards.append(reward) + unbalances.append(info.get('unbalance', 0)) + state = next_state + if done: + break + + return states, actions, rewards, unbalances + + +if __name__ == '__main__': + env = ESSEnv() + llm_actions = load_actions_from_json('data/llm_action.json') + states, actions, rewards, unbalances = simulate_with_llm_actions(env, llm_actions) + + fig, ax1 = plt.subplots() + + color = 'tab:blue' + ax1.set_xlabel('Step') + ax1.set_ylabel('Reward', color=color) + ax1.plot(rewards, color=color) + ax1.tick_params(axis='y', labelcolor=color) + + ax2 = ax1.twinx() + color = 'tab:red' + ax2.set_ylabel('Unbalance', color=color) + ax2.plot(unbalances, color=color) + ax2.tick_params(axis='y', labelcolor=color) + + fig.tight_layout() + plt.title('Rewards and Unbalance over Steps') + plt.show() diff --git a/test.py b/test.py index 359def5..fd259a8 100644 --- a/test.py +++ b/test.py @@ -39,17 +39,22 @@ # data = pickle.load(f) # # print(data) -import json -import numpy as np +# import json +# import numpy as np +# +# +# def get_llm_action(index) -> np.ndarray: +# with open('data/llm_action.json', 'r') as file: +# data = json.load(file) +# normalized_index = index % len(data) +# action = np.array(data[normalized_index]) +# return action +# +# data = get_llm_action(2) +# print(data) +from environment import ESSEnv - -def get_llm_action(index) -> np.ndarray: - with open('data/llm_action.json', 'r') as file: - data = json.load(file) - normalized_index = index % len(data) - action = np.array(data[normalized_index]) - return action - - -data = get_llm_action(2) -print(data) +env = ESSEnv() +wind_speed = env.data_manager.get_series_wind_data(1, 1) +wind = [env.wind.step(ws) for ws in wind_speed] +print(wind) diff --git a/tools.py b/tools.py index a4e4926..7e8c17a 100644 --- a/tools.py +++ b/tools.py @@ -7,7 +7,7 @@ import torch from gurobipy import GRB -def optimization_base_result(env, month, day, initial_soc): +def optimization_base_result(env,month,day,initial_soc): price = env.data_manager.get_series_price_data(month, day) load = env.data_manager.get_series_load_cons_data(month, day) temperature = env.data_manager.get_series_temperature_data(month, day) @@ -50,7 +50,7 @@ def optimization_base_result(env, month, day, initial_soc): on_off = m.addVars(NUM_GEN, period, vtype=GRB.BINARY, name='on_off') gen_output = m.addVars(NUM_GEN, period, vtype=GRB.CONTINUOUS, name='output') # 设置充放电约束 - battery_energy_change = m.addVars(period, vtype=GRB.CONTINUOUS, lb=env.battery.max_discharge, + battery_energy_change = m.addVars(period, vtype=GRB.CONTINUOUS, lb=-env.battery.max_charge, ub=env.battery.max_charge, name='battery_action') # 设置外部电网与能源系统交换约束 grid_energy_import = m.addVars(period, vtype=GRB.CONTINUOUS, lb=0, ub=env.grid.exchange_ability, name='import') @@ -86,8 +86,8 @@ def optimization_base_result(env, month, day, initial_soc): m.setObjective((cost_gen + cost_grid_import - cost_grid_export), GRB.MINIMIZE) m.optimize() - output_record = {'pv': [], 'temperature': [], 'irradiance': [], 'wind_speed': [], 'price': [], 'load': [], - 'netload': [], 'soc': [], 'battery_energy_change': [], 'grid_import': [], 'grid_export': [], + output_record = {'pv': [], 'wind': [], 'price': [], 'load': [], 'netload': [], + 'soc': [], 'battery_energy_change': [], 'grid_import': [], 'grid_export': [], 'gen1': [], 'gen2': [], 'gen3': [], 'step_cost': []} for t in range(period): gen_cost = sum((on_off[g, t].x * ( @@ -95,13 +95,11 @@ def optimization_base_result(env, month, day, initial_soc): for g in range(NUM_GEN)) grid_import_cost = grid_energy_import[t].x * price[t] grid_export_cost = grid_energy_export[t].x * price[t] * env.sell_coefficient - output_record['pv'].append(pv[t].x) - output_record['temperature'].append(temperature[t]) - output_record['irradiance'].append(irradiance[t]) - output_record['wind_speed'].append(wind[t]) + output_record['pv'].append(pv[t]) + output_record['wind'].append(wind[t]) output_record['price'].append(price[t]) output_record['load'].append(load[t]) - output_record['netload'].append(load[t] - pv[t].x) + output_record['netload'].append(load[t] - pv[t]) output_record['soc'].append(soc[t].x) output_record['battery_energy_change'].append(battery_energy_change[t].x) output_record['grid_import'].append(grid_energy_import[t].x) @@ -110,7 +108,6 @@ def optimization_base_result(env, month, day, initial_soc): output_record['gen2'].append(gen_output[1, t].x) output_record['gen3'].append(gen_output[2, t].x) output_record['step_cost'].append(gen_cost + grid_import_cost - grid_export_cost) - output_record_df = pd.DataFrame.from_dict(output_record) return output_record_df diff --git a/tools备份.py b/tools备份.py new file mode 100644 index 0000000..99ed610 --- /dev/null +++ b/tools备份.py @@ -0,0 +1,285 @@ +import os +import gurobipy as gp +import numpy as np +import numpy.random as rd +import pandas as pd +import torch +from gurobipy import GRB + + +def optimization_base_result(env, month, day, initial_soc): + price = env.data_manager.get_series_price_data(month, day) + load = env.data_manager.get_series_load_cons_data(month, day) + temperature = env.data_manager.get_series_temperature_data(month, day) + irradiance = env.data_manager.get_series_irradiance_data(month, day) + wind_speed = env.data_manager.get_series_wind_data(month, day) + + pv = [env.solar.step(temp, irr) for temp, irr in zip(temperature, irradiance)] + wind = [env.wind.step(ws) for ws in wind_speed] + + period = env.episode_length + DG_parameters = env.dg_parameters + + def get_dg_info(parameters): + p_max = [] + p_min = [] + ramping_up = [] + ramping_down = [] + a_para = [] + b_para = [] + c_para = [] + + for name, gen_info in parameters.items(): + p_max.append(gen_info['power_output_max']) + p_min.append(gen_info['power_output_min']) + ramping_up.append(gen_info['ramping_up']) + ramping_down.append(gen_info['ramping_down']) + a_para.append(gen_info['a']) + b_para.append(gen_info['b']) + c_para.append(gen_info['c']) + return p_max, p_min, ramping_up, ramping_down, a_para, b_para, c_para + + p_max, p_min, ramping_up, ramping_down, a_para, b_para, c_para = get_dg_info(parameters=DG_parameters) + NUM_GEN = len(DG_parameters.keys()) + battery_capacity = env.battery.capacity + battery_efficiency = env.battery.efficiency + + m = gp.Model("UC") + m.setParam('OutputFlag', 1) + + # 设置系统变量 + on_off = m.addVars(NUM_GEN, period, vtype=GRB.BINARY, name='on_off') + gen_output = m.addVars(NUM_GEN, period, vtype=GRB.CONTINUOUS, name='output') + # 设置充放电约束 + battery_energy_change = m.addVars(period, vtype=GRB.CONTINUOUS, lb=env.battery.max_discharge, + ub=env.battery.max_charge, name='battery_action') + # 设置外部电网与能源系统交换约束 + grid_energy_import = m.addVars(period, vtype=GRB.CONTINUOUS, lb=0, ub=env.grid.exchange_ability, name='import') + grid_energy_export = m.addVars(period, vtype=GRB.CONTINUOUS, lb=0, ub=env.grid.exchange_ability, name='export') + soc = m.addVars(period, vtype=GRB.CONTINUOUS, lb=0.2, ub=0.8, name='SOC') + + # 1. 添加平衡约束 + m.addConstrs(((sum(gen_output[g, t] for g in range(NUM_GEN)) + pv[t] + wind[t] + grid_energy_import[t] >= load[t] + + battery_energy_change[t] + grid_energy_export[t]) for t in range(period)), name='powerbalance') + # 2. 添加发电机最大/最小功率约束 + m.addConstrs((gen_output[g, t] <= on_off[g, t] * p_max[g] for g in range(NUM_GEN) for t in range(period)), + 'gen_output_max') + m.addConstrs((gen_output[g, t] >= on_off[g, t] * p_min[g] for g in range(NUM_GEN) for t in range(period)), + 'gen_output_min') + # 3. 添加上升和下降约束 + m.addConstrs((gen_output[g, t + 1] - gen_output[g, t] <= ramping_up[g] for g in range(NUM_GEN) + for t in range(period - 1)), 'ramping_up') + m.addConstrs((gen_output[g, t] - gen_output[g, t + 1] <= ramping_down[g] for g in range(NUM_GEN) + for t in range(period - 1)), 'ramping_down') + # 4. 添加电池容量约束 + m.addConstr(battery_capacity * soc[0] == battery_capacity * initial_soc + + (battery_energy_change[0] * battery_efficiency), name='soc0') + m.addConstrs((battery_capacity * soc[t] == battery_capacity * soc[t - 1] + + (battery_energy_change[t] * battery_efficiency) for t in range(1, period)), name='soc update') + # 设置成本函数 + # 发电机成本 + cost_gen = gp.quicksum( + (a_para[g] * gen_output[g, t] * gen_output[g, t] + b_para[g] * gen_output[g, t] + c_para[g] * on_off[g, t]) for + t in range(period) for g in range(NUM_GEN)) + cost_grid_import = gp.quicksum(grid_energy_import[t] * price[t] for t in range(period)) + cost_grid_export = gp.quicksum(grid_energy_export[t] * price[t] * env.sell_coefficient for t in range(period)) + + m.setObjective((cost_gen + cost_grid_import - cost_grid_export), GRB.MINIMIZE) + m.optimize() + + m.computeIIS() + + output_record = {'pv': [], 'wind': [], 'price': [], 'load': [], + 'netload': [], 'soc': [], 'battery_energy_change': [], 'grid_import': [], 'grid_export': [], + 'gen1': [], 'gen2': [], 'gen3': [], 'step_cost': []} + for t in range(period): + gen_cost = sum((on_off[g, t].x * ( + a_para[g] * gen_output[g, t].x * gen_output[g, t].x + b_para[g] * gen_output[g, t].x + c_para[g])) + for g in range(NUM_GEN)) + grid_import_cost = grid_energy_import[t].x * price[t] + grid_export_cost = grid_energy_export[t].x * price[t] * env.sell_coefficient + output_record['pv'].append(pv[t]) + # output_record['temperature'].append(temperature[t]) + # output_record['irradiance'].append(irradiance[t]) + output_record['wind'].append(wind[t]) + output_record['price'].append(price[t]) + output_record['load'].append(load[t]) + output_record['netload'].append(load[t] - pv[t]) + output_record['soc'].append(soc[t].x) + output_record['battery_energy_change'].append(battery_energy_change[t].x) + output_record['grid_import'].append(grid_energy_import[t].x) + output_record['grid_export'].append(grid_energy_export[t].x) + output_record['gen1'].append(gen_output[0, t].x) + output_record['gen2'].append(gen_output[1, t].x) + output_record['gen3'].append(gen_output[2, t].x) + output_record['step_cost'].append(gen_cost + grid_import_cost - grid_export_cost) + output_record_df = pd.DataFrame.from_dict(output_record) + return output_record_df + + +class Arguments: + """revise here for our own purpose""" + + def __init__(self, agent=None, env=None): + self.agent = agent # Deep Reinforcement Learning algorithm + self.env = env # the environment for training + # self.plot_shadow_on = False # control do we need to plot all shadow figures + self.cwd = None # current work directory. None means set automatically + self.if_remove = False # remove the cwd folder? (True, False, None:ask me) + self.visible_gpu = '0,1,2,3' # for example: os.environ['CUDA_VISIBLE_DEVICES'] = '0, 2,' + # self.worker_num = 2 # rollout workers number pre GPU (adjust it to get high GPU usage) + self.num_threads = 32 # cpu_num for evaluate model, torch.set_num_threads(self.num_threads) + + '''Arguments for training''' + self.num_episode = 1000 + self.gamma = 0.995 # discount factor of future rewards + # self.reward_scale = 1 # an approximate target reward usually be closed to 256 + self.learning_rate = 2 ** -14 # 2 ** -14 ~= 6e-5 + self.soft_update_tau = 2 ** -8 # 2 ** -8 ~= 5e-3 + self.net_dim = 256 # the network width 256 + self.batch_size = 4096 # num of transitions sampled from replay buffer. + self.repeat_times = 2 ** 5 # repeatedly update network to keep critic's loss small + self.target_step = 4096 # collect target_step experiences, then update network, 1024 + self.max_memo = 500000 # capacity of replay buffer + self.if_per_or_gae = False # PER for off-policy sparse reward: Prioritized Experience Replay. + + '''Arguments for evaluate''' + # self.eval_gap = 2 ** 6 # evaluate the agent per eval_gap seconds + # self.eval_times = 2 # number of times that get episode return in first + self.random_seed = 0 # initialize random seed in self.init_before_training() + # self.random_seed_list = [1234, 2234, 3234, 4234, 5234] + self.random_seed_list = [1234] + '''Arguments for save and plot issues''' + self.train = True + self.save_network = True + self.test_network = True + self.save_test_data = True + self.compare_with_gurobi = True + self.plot_on = True + + def init_before_training(self, if_main): + if self.cwd is None: + agent_name = self.agent.__class__.__name__ + self.cwd = f'./{agent_name}' + + if if_main: + import shutil # remove history according to bool(if_remove) + if self.if_remove is None: + self.if_remove = bool(input(f"| PRESS 'y' to REMOVE: {self.cwd}? ") == 'y') + elif self.if_remove: + shutil.rmtree(self.cwd, ignore_errors=True) + print(f"| Remove cwd: {self.cwd}") + os.makedirs(self.cwd, exist_ok=True) + + np.random.seed(self.random_seed) + torch.manual_seed(self.random_seed) + torch.set_num_threads(self.num_threads) + torch.set_default_dtype(torch.float32) + + os.environ['CUDA_VISIBLE_DEVICES'] = str(self.visible_gpu) # control how many GPU is used   + + +def test_one_episode(env, act, device): + """to get evaluate information, here record the unbalance of after taking action""" + record_state = [] + record_action = [] + record_reward = [] + record_output = [] + record_cost = [] + record_unbalance = [] + record_system_info = [] # [time price,netload,action,real action, output*4,soc,unbalance(exchange+penalty)] + record_init_info = [] # include month,day,time,intial soc + env.TRAIN = False + state = env.reset() + record_init_info.append([env.month, env.day, env.current_time, env.battery.current_capacity]) + print(f'current testing month is {env.month}, day is {env.day},initial_soc is {env.battery.current_capacity}') + for i in range(24): + s_tensor = torch.as_tensor((state,), device=device) + a_tensor = act(s_tensor) + action = a_tensor.detach().cpu().numpy()[0] # not need detach(), because with torch.no_grad() outside + real_action = action + state, next_state, reward, done = env.step(action) + + record_system_info.append([state[0], state[1], state[3], action, real_action, env.battery.SOC(), + env.battery.energy_change, next_state[4], next_state[5], next_state[6], + next_state[7], next_state[8], env.unbalance, env.operation_cost]) + record_state.append(state) + record_action.append(real_action) + record_reward.append(reward) + record_output.append(env.current_output) + record_unbalance.append(env.unbalance) + state = next_state + # add information of last step dg1, dh2, dg3, soc + record_system_info[-1][7:10] = [env.final_step_outputs[0], env.final_step_outputs[1], env.final_step_outputs[2]] + record_system_info[-1][5] = env.final_step_outputs[3] + record = {'init_info': record_init_info, 'system_info': record_system_info, 'state': record_state, + 'action': record_action, 'reward': record_reward, 'cost': record_cost, 'unbalance': record_unbalance, + 'record_output': record_output} + return record + + +def get_episode_return(env, act, device): + episode_return = 0.0 # sum of rewards in an episode + episode_unbalance = 0.0 + state = env.reset() + for i in range(24): + s_tensor = torch.as_tensor((state,), device=device) + a_tensor = act(s_tensor) + action = a_tensor.detach().cpu().numpy()[0] # not need detach(), because with torch.no_grad() outside + state, next_state, reward, done, = env.step(action) + state = next_state + episode_return += reward + episode_unbalance += env.real_unbalance + if done: + break + return episode_return, episode_unbalance + + +class ReplayBuffer: + def __init__(self, max_len, state_dim, action_dim, gpu_id=0): + self.now_len = 0 + self.next_idx = 0 + self.if_full = False + self.max_len = max_len + self.data_type = torch.float32 + self.action_dim = action_dim + self.device = torch.device(f"cuda:{gpu_id}" if (torch.cuda.is_available() and (gpu_id >= 0)) else "cpu") + + other_dim = 1 + 1 + self.action_dim + self.buf_other = torch.empty(size=(max_len, other_dim), dtype=self.data_type, device=self.device) + + if isinstance(state_dim, int): # state is pixel + self.buf_state = torch.empty((max_len, state_dim), dtype=torch.float32, device=self.device) + elif isinstance(state_dim, tuple): + self.buf_state = torch.empty((max_len, *state_dim), dtype=torch.uint8, device=self.device) + else: + raise ValueError('state_dim') + + def extend_buffer(self, state, other): # CPU array to CPU array + size = len(other) + next_idx = self.next_idx + size + + if next_idx > self.max_len: + self.buf_state[self.next_idx:self.max_len] = state[:self.max_len - self.next_idx] + self.buf_other[self.next_idx:self.max_len] = other[:self.max_len - self.next_idx] + self.if_full = True + + next_idx = next_idx - self.max_len + self.buf_state[0:next_idx] = state[-next_idx:] + self.buf_other[0:next_idx] = other[-next_idx:] + else: + self.buf_state[self.next_idx:next_idx] = state + self.buf_other[self.next_idx:next_idx] = other + self.next_idx = next_idx + + def sample_batch(self, batch_size) -> tuple: + indices = rd.randint(self.now_len - 1, size=batch_size) + r_m_a = self.buf_other[indices] + return (r_m_a[:, 0:1], + r_m_a[:, 1:2], + r_m_a[:, 2:], + self.buf_state[indices], + self.buf_state[indices + 1]) + + def update_now_len(self): + self.now_len = self.max_len if self.if_full else self.next_idx