|
|
import os |
|
|
import sys |
|
|
import time |
|
|
from datetime import datetime |
|
|
import re |
|
|
import numpy as np |
|
|
import torch |
|
|
import pandas as pd |
|
|
import matplotlib.pyplot as plt |
|
|
import glob |
|
|
|
|
|
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) |
|
|
|
|
|
from cluster import InterClusterCoordinator, InterClusterLedger |
|
|
from Environment.cluster_env_wrapper import make_vec_env |
|
|
from mappo.trainer.mappo import MAPPO |
|
|
|
|
|
|
|
|
def compute_jains_fairness(values: np.ndarray) -> float: |
|
|
"""Compute Jain's fairness index.""" |
|
|
if len(values) == 0: |
|
|
return 0.0 |
|
|
if np.all(values == 0): |
|
|
return 1.0 |
|
|
num = (values.sum())**2 |
|
|
den = len(values) * (values**2).sum() + 1e-8 |
|
|
return float(num / den) |
|
|
|
|
|
|
|
|
def main(): |
|
|
|
|
|
|
|
|
DATA_PATH = "./data/testing/test_data.csv" |
|
|
MODEL_DIR = "./training_models/hierarchical_region_a_500agents_10size_final/models" |
|
|
|
|
|
|
|
|
state_match = re.search(r"hierarchical_(region_a|region_b|region_c)_", MODEL_DIR) |
|
|
if not state_match: |
|
|
state_match = re.search(r"mappo_(region_a|region_b|region_c)_", MODEL_DIR) |
|
|
|
|
|
if not state_match: |
|
|
raise ValueError( |
|
|
"Could not detect the state (region_a, region_b, or region_c) " |
|
|
"from the model directory path." |
|
|
) |
|
|
detected_state = state_match.group(1) |
|
|
|
|
|
|
|
|
|
|
|
cluster_size_match = re.search(r'(\d+)size_', MODEL_DIR) |
|
|
if not cluster_size_match: |
|
|
raise ValueError("Could not detect the cluster size from the model directory path.") |
|
|
detected_cluster_size = int(cluster_size_match.group(1)) |
|
|
|
|
|
|
|
|
DAYS_TO_EVALUATE = 30 |
|
|
SOLAR_THRESHOLD = 0.1 |
|
|
MAX_TRANSFER_KWH = 1000000.0 |
|
|
|
|
|
W_COST_SAVINGS = 1.0 |
|
|
W_GRID_PENALTY = 0.5 |
|
|
W_P2P_BONUS = 0.2 |
|
|
|
|
|
|
|
|
cluster_env = make_vec_env( |
|
|
data_path=DATA_PATH, |
|
|
time_freq="15T", |
|
|
cluster_size=detected_cluster_size, |
|
|
state=detected_state |
|
|
) |
|
|
n_clusters = cluster_env.num_envs |
|
|
sample_subenv = cluster_env.cluster_envs[0] |
|
|
eval_num_steps = sample_subenv.num_steps |
|
|
|
|
|
|
|
|
|
|
|
n_agents_per_cluster = sample_subenv.num_agents |
|
|
local_dim = sample_subenv.observation_space.shape[-1] |
|
|
global_dim = n_agents_per_cluster * local_dim |
|
|
act_dim = sample_subenv.action_space[0].shape[-1] |
|
|
|
|
|
|
|
|
low_agents = [] |
|
|
for i in range(n_clusters): |
|
|
agent = MAPPO( |
|
|
n_agents=n_agents_per_cluster, |
|
|
local_dim=local_dim, |
|
|
global_dim=global_dim, |
|
|
act_dim=act_dim, |
|
|
lr=2e-4, gamma=0.95, lam=0.95, clip_eps=0.2, k_epochs=4, batch_size=512, episode_len=96 |
|
|
) |
|
|
ckpt_pattern = os.path.join(MODEL_DIR, f"low_cluster{i}_ep*.pth") |
|
|
ckpts_low = glob.glob(ckpt_pattern) |
|
|
if not ckpts_low: |
|
|
raise FileNotFoundError(f"No checkpoint found for cluster {i}.") |
|
|
latest_low = sorted(ckpts_low, key=lambda x: int(re.search(r'ep(\d+)\.pth$', x).group(1)))[-1] |
|
|
|
|
|
agent.load(latest_low) |
|
|
agent.actor.eval() |
|
|
agent.critic.eval() |
|
|
low_agents.append(agent) |
|
|
|
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
|
num_agents = sum(subenv.num_agents for subenv in cluster_env.cluster_envs) |
|
|
run_name = f"eval_vectorized_{num_agents}agents_{DAYS_TO_EVALUATE}days_{timestamp}" |
|
|
output_folder = os.path.join("runs_final_vectorized_eval", run_name) |
|
|
logs_dir = os.path.join(output_folder, "logs") |
|
|
plots_dir = os.path.join(output_folder, "plots") |
|
|
for d in (logs_dir, plots_dir): |
|
|
os.makedirs(d, exist_ok=True) |
|
|
|
|
|
|
|
|
|
|
|
OBS_DIM_HI_LOCAL = 7 |
|
|
act_dim_inter = 2 |
|
|
OBS_DIM_HI_GLOBAL = n_clusters * OBS_DIM_HI_LOCAL |
|
|
|
|
|
|
|
|
|
|
|
inter_agent = MAPPO( |
|
|
n_agents=n_clusters, |
|
|
local_dim=OBS_DIM_HI_LOCAL, |
|
|
global_dim=OBS_DIM_HI_GLOBAL, |
|
|
act_dim=act_dim_inter, |
|
|
lr=2e-4, gamma=0.95, lam=0.95, clip_eps=0.2, k_epochs=4, batch_size=512, episode_len=96 |
|
|
) |
|
|
|
|
|
ckpts_inter = glob.glob(os.path.join(MODEL_DIR, "inter_ep*.pth")) |
|
|
if not ckpts_inter: |
|
|
raise FileNotFoundError(f"No high-level checkpoints in {MODEL_DIR}") |
|
|
latest_inter = sorted(ckpts_inter, key=lambda x: int(re.search(r'ep(\d+)\.pth$', x).group(1)))[-1] |
|
|
|
|
|
inter_agent.load(latest_inter) |
|
|
inter_agent.actor.eval() |
|
|
inter_agent.critic.eval() |
|
|
|
|
|
|
|
|
ledger = InterClusterLedger() |
|
|
coordinator = InterClusterCoordinator( |
|
|
cluster_env, inter_agent, ledger, max_transfer_kwh=MAX_TRANSFER_KWH, |
|
|
w_cost_savings=W_COST_SAVINGS, w_grid_penalty=W_GRID_PENALTY, w_p2p_bonus=W_P2P_BONUS |
|
|
) |
|
|
|
|
|
|
|
|
all_logs = [] |
|
|
daily_summaries = [] |
|
|
step_timing_list = [] |
|
|
|
|
|
|
|
|
evaluation_start = time.time() |
|
|
for day in range(1, DAYS_TO_EVALUATE + 1): |
|
|
obs_clusters, _ = cluster_env.reset() |
|
|
done_all = False |
|
|
step_count = 0 |
|
|
day_logs = [] |
|
|
|
|
|
while not done_all and step_count < eval_num_steps: |
|
|
step_start_time = time.time() |
|
|
step_count += 1 |
|
|
|
|
|
|
|
|
inter_cluster_obs_local_list = [coordinator.get_cluster_state(se, step_count) for se in cluster_env.cluster_envs] |
|
|
inter_cluster_obs_local = np.array(inter_cluster_obs_local_list) |
|
|
inter_cluster_obs_global = inter_cluster_obs_local.flatten() |
|
|
|
|
|
with torch.no_grad(): |
|
|
high_level_action, _ = inter_agent.select_action(inter_cluster_obs_local, inter_cluster_obs_global) |
|
|
|
|
|
|
|
|
current_reports = {i: {'export_capacity': cluster_env.get_export_capacity(i), 'import_capacity': cluster_env.get_import_capacity(i)} for i in range(n_clusters)} |
|
|
exports, imports = coordinator.build_transfers(high_level_action, current_reports) |
|
|
|
|
|
|
|
|
batch_global_obs = obs_clusters.reshape(n_clusters, -1) |
|
|
with torch.no_grad(): |
|
|
low_level_actions_list = [] |
|
|
for c_idx in range(n_clusters): |
|
|
agent = low_agents[c_idx] |
|
|
local_obs_cluster = obs_clusters[c_idx] |
|
|
global_obs_cluster = batch_global_obs[c_idx] |
|
|
actions, _ = agent.select_action(local_obs_cluster, global_obs_cluster) |
|
|
low_level_actions_list.append(actions) |
|
|
low_level_actions = np.stack(low_level_actions_list) |
|
|
|
|
|
|
|
|
next_obs, rewards, done_all, step_info = cluster_env.step( |
|
|
low_level_actions, exports=exports, imports=imports |
|
|
) |
|
|
|
|
|
|
|
|
obs_clusters = next_obs |
|
|
|
|
|
|
|
|
step_duration = time.time() - step_start_time |
|
|
|
|
|
step_timing_list.append({"day": day, "step": step_count, "step_time_s": step_duration}) |
|
|
|
|
|
|
|
|
infos = step_info.get("cluster_infos") |
|
|
for c_idx, subenv in enumerate(cluster_env.cluster_envs): |
|
|
grid_price_now = subenv.get_grid_price(step_count - 1) |
|
|
peer_price_now = step_info.get("peer_price_global") |
|
|
if peer_price_now is None: |
|
|
demands_step = subenv.demands_day[step_count-1] |
|
|
solars_step = subenv.solars_day[step_count-1] |
|
|
surplus = np.maximum(solars_step - demands_step, 0.0).sum() |
|
|
shortfall = np.maximum(demands_step - solars_step, 0.0).sum() |
|
|
peer_price_now = subenv.get_peer_price(step_count -1, surplus, shortfall) |
|
|
|
|
|
for i, hid in enumerate(subenv.house_ids): |
|
|
is_battery_house = hid in subenv.batteries |
|
|
charge = infos["charge_amount"][c_idx][i] |
|
|
discharge = infos["discharge_amount"][c_idx][i] |
|
|
day_logs.append({ |
|
|
"day": day, "step": step_count - 1, "house": hid, "cluster": c_idx, |
|
|
"grid_import_no_p2p": infos["grid_import_no_p2p"][c_idx][i], |
|
|
"grid_import_with_p2p": infos["grid_import_with_p2p"][c_idx][i], |
|
|
"grid_export": infos["grid_export"][c_idx][i], |
|
|
"p2p_buy": infos["p2p_buy"][c_idx][i], "p2p_sell": infos["p2p_sell"][c_idx][i], |
|
|
"actual_cost": infos["costs"][c_idx][i], |
|
|
"baseline_cost": infos["grid_import_no_p2p"][c_idx][i] * grid_price_now, |
|
|
"total_demand": subenv.demands_day[step_count-1, i], |
|
|
"total_solar": subenv.solars_day[step_count-1, i], |
|
|
"grid_price": grid_price_now, "peer_price": peer_price_now, |
|
|
"soc": (subenv.battery_soc[i] / subenv.battery_max_capacity[i]) if is_battery_house and subenv.battery_max_capacity[i] > 0 else np.nan, |
|
|
"degradation_cost": (charge + discharge) * subenv.battery_degradation_cost[i] if is_battery_house else 0.0, |
|
|
"reward": infos["agent_rewards"][c_idx][i], |
|
|
}) |
|
|
|
|
|
step_duration = time.time() - step_start_time |
|
|
|
|
|
|
|
|
df_day = pd.DataFrame(day_logs) |
|
|
if df_day.empty: continue |
|
|
all_logs.extend(day_logs) |
|
|
|
|
|
grouped_house = df_day.groupby("house").sum(numeric_only=True) |
|
|
grouped_step = df_day.groupby("step").sum(numeric_only=True) |
|
|
|
|
|
total_demand = grouped_step["total_demand"].sum() |
|
|
total_solar = grouped_step["total_solar"].sum() |
|
|
total_p2p_buy = df_day['p2p_buy'].sum() |
|
|
total_p2p_sell = df_day['p2p_sell'].sum() |
|
|
|
|
|
baseline_cost_per_house = grouped_house["baseline_cost"] |
|
|
actual_cost_per_house = grouped_house["actual_cost"] |
|
|
cost_savings_per_house = baseline_cost_per_house - actual_cost_per_house |
|
|
day_total_cost_savings = cost_savings_per_house.sum() |
|
|
|
|
|
overall_cost_savings_pct = day_total_cost_savings / baseline_cost_per_house.sum() if baseline_cost_per_house.sum() > 0 else 0.0 |
|
|
|
|
|
baseline_import_per_house = grouped_house["grid_import_no_p2p"] |
|
|
actual_import_per_house = grouped_house["grid_import_with_p2p"] |
|
|
import_reduction_per_house = baseline_import_per_house - actual_import_per_house |
|
|
day_total_import_reduction = import_reduction_per_house.sum() |
|
|
|
|
|
overall_import_reduction_pct = day_total_import_reduction / baseline_import_per_house.sum() if baseline_import_per_house.sum() > 0 else 0.0 |
|
|
|
|
|
fairness_cost_savings = compute_jains_fairness(cost_savings_per_house.values) |
|
|
fairness_import_reduction = compute_jains_fairness(import_reduction_per_house.values) |
|
|
fairness_rewards = compute_jains_fairness(grouped_house["reward"].values) |
|
|
fairness_p2p_total = compute_jains_fairness((grouped_house["p2p_buy"] + grouped_house["p2p_sell"]).values) |
|
|
|
|
|
daily_summaries.append({ |
|
|
"day": day, "day_total_demand": total_demand, "day_total_solar": total_solar, |
|
|
"day_p2p_buy": total_p2p_buy, "day_p2p_sell": total_p2p_sell, |
|
|
"cost_savings_abs": day_total_cost_savings, "cost_savings_pct": overall_cost_savings_pct, |
|
|
"fairness_cost_savings": fairness_cost_savings, "grid_reduction_abs": day_total_import_reduction, |
|
|
"grid_reduction_pct": overall_import_reduction_pct, |
|
|
"fairness_grid_reduction": fairness_import_reduction, "fairness_reward": fairness_rewards, |
|
|
"fairness_p2p_buy": compute_jains_fairness(grouped_house["p2p_buy"].values), |
|
|
"fairness_p2p_sell": compute_jains_fairness(grouped_house["p2p_sell"].values), |
|
|
"fairness_p2p_total": fairness_p2p_total, |
|
|
}) |
|
|
|
|
|
|
|
|
evaluation_end = time.time() |
|
|
total_eval_time = evaluation_end - evaluation_start |
|
|
|
|
|
all_days_df = pd.DataFrame(all_logs) |
|
|
if not all_days_df.empty: |
|
|
|
|
|
combined_csv_path = os.path.join(logs_dir, "step_logs_all_days.csv") |
|
|
all_days_df.to_csv(combined_csv_path, index=False) |
|
|
|
|
|
|
|
|
step_timing_df = pd.DataFrame(step_timing_list) |
|
|
timing_csv_path = os.path.join(logs_dir, "step_timing_log.csv") |
|
|
step_timing_df.to_csv(timing_csv_path, index=False) |
|
|
|
|
|
|
|
|
house_level_df = all_days_df.groupby("house").agg({ |
|
|
"baseline_cost": "sum", "actual_cost": "sum", "grid_import_no_p2p": "sum", |
|
|
"grid_import_with_p2p": "sum", "degradation_cost": "sum" |
|
|
}) |
|
|
house_level_df["cost_savings"] = house_level_df["baseline_cost"] - house_level_df["actual_cost"] |
|
|
house_level_df["import_reduction"] = house_level_df["grid_import_no_p2p"] - house_level_df["grid_import_with_p2p"] |
|
|
house_summary_csv = os.path.join(logs_dir, "summary_per_house.csv") |
|
|
house_level_df.to_csv(house_summary_csv) |
|
|
|
|
|
|
|
|
daily_summary_df = pd.DataFrame(daily_summaries) |
|
|
fairness_grid_all = compute_jains_fairness(house_level_df["import_reduction"].values) |
|
|
fairness_cost_all = compute_jains_fairness(house_level_df["cost_savings"].values) |
|
|
total_cost_savings_all = daily_summary_df["cost_savings_abs"].sum() |
|
|
total_baseline_cost_all = all_days_df.groupby('day')['baseline_cost'].sum().sum() |
|
|
pct_cost_savings_all = total_cost_savings_all / total_baseline_cost_all if total_baseline_cost_all > 0 else 0.0 |
|
|
total_grid_reduction_all = daily_summary_df["grid_reduction_abs"].sum() |
|
|
total_baseline_import_all = all_days_df.groupby('day')['grid_import_no_p2p'].sum().sum() |
|
|
pct_grid_reduction_all = total_grid_reduction_all / total_baseline_import_all if total_baseline_import_all > 0 else 0.0 |
|
|
total_degradation_cost_all = all_days_df["degradation_cost"].sum() |
|
|
agg_solar_per_step = all_days_df.groupby(['day', 'step'])['total_solar'].sum() |
|
|
num_agents_total = len(all_days_df['house'].unique()) |
|
|
sunny_steps_mask = agg_solar_per_step > (SOLAR_THRESHOLD * num_agents_total) |
|
|
sunny_df = all_days_df[all_days_df.set_index(['day', 'step']).index.isin(sunny_steps_mask[sunny_steps_mask].index)] |
|
|
baseline_import_sunny = sunny_df['grid_import_no_p2p'].sum() |
|
|
actual_import_sunny = sunny_df['grid_import_with_p2p'].sum() |
|
|
grid_reduction_sunny_pct = (baseline_import_sunny - actual_import_sunny) / baseline_import_sunny if baseline_import_sunny > 0 else 0.0 |
|
|
baseline_cost_sunny = sunny_df['baseline_cost'].sum() |
|
|
actual_cost_sunny = sunny_df['actual_cost'].sum() |
|
|
cost_savings_sunny_pct = (baseline_cost_sunny - actual_cost_sunny) / baseline_cost_sunny if baseline_cost_sunny > 0 else 0.0 |
|
|
total_p2p_buy = all_days_df['p2p_buy'].sum() |
|
|
total_actual_grid_import = all_days_df['grid_import_with_p2p'].sum() |
|
|
community_sourcing_rate_pct = total_p2p_buy / (total_p2p_buy + total_actual_grid_import) if (total_p2p_buy + total_actual_grid_import) > 0 else 0.0 |
|
|
total_p2p_sell = all_days_df['p2p_sell'].sum() |
|
|
total_grid_export = all_days_df['grid_export'].sum() |
|
|
solar_sharing_efficiency_pct = total_p2p_sell / (total_p2p_sell + total_grid_export) if (total_p2p_sell + total_grid_export) > 0 else 0.0 |
|
|
|
|
|
|
|
|
final_row = { |
|
|
"day": "ALL_DAYS_SUMMARY", "cost_savings_abs": total_cost_savings_all, "cost_savings_pct": pct_cost_savings_all, |
|
|
"grid_reduction_abs": total_grid_reduction_all, "grid_reduction_pct": pct_grid_reduction_all, |
|
|
"fairness_cost_savings": fairness_cost_all, "fairness_grid_reduction": fairness_grid_all, |
|
|
"total_degradation_cost": total_degradation_cost_all, |
|
|
"grid_reduction_sunny_hours_pct": grid_reduction_sunny_pct, |
|
|
"cost_savings_sunny_hours_pct": cost_savings_sunny_pct, |
|
|
"community_sourcing_rate_pct": community_sourcing_rate_pct, |
|
|
"solar_sharing_efficiency_pct": solar_sharing_efficiency_pct, |
|
|
} |
|
|
final_row_df = pd.DataFrame([final_row]) |
|
|
|
|
|
if not daily_summary_df.empty: |
|
|
daily_summary_df = pd.concat([daily_summary_df, final_row_df], ignore_index=True) |
|
|
|
|
|
summary_csv = os.path.join(logs_dir, "summary_per_day.csv") |
|
|
daily_summary_df.to_csv(summary_csv, index=False) |
|
|
print("Evaluation run completed. All data logs (CSVs) and plots saved to disk.") |
|
|
|
|
|
|
|
|
|
|
|
plot_daily_df = daily_summary_df[daily_summary_df["day"] != "ALL_DAYS_SUMMARY"].copy() |
|
|
plot_daily_df["day"] = plot_daily_df["day"].astype(int) |
|
|
|
|
|
|
|
|
plt.figure(figsize=(12, 6)) |
|
|
plt.bar(plot_daily_df["day"], plot_daily_df["cost_savings_pct"] * 100, color='skyblue') |
|
|
plt.xlabel("Day") |
|
|
plt.ylabel("Cost Savings (%)") |
|
|
plt.title("Daily Community Cost Savings Percentage") |
|
|
plt.xticks(plot_daily_df["day"]) |
|
|
plt.grid(axis='y', linestyle='--', alpha=0.7) |
|
|
plt.savefig(os.path.join(plots_dir, "daily_cost_savings_percentage.png")) |
|
|
plt.close() |
|
|
|
|
|
|
|
|
plt.figure(figsize=(12, 6)) |
|
|
bar_width = 0.4 |
|
|
days = plot_daily_df["day"] |
|
|
plt.bar(days - bar_width/2, plot_daily_df["day_total_demand"], width=bar_width, label="Total Demand", color='coral') |
|
|
plt.bar(days + bar_width/2, plot_daily_df["day_total_solar"], width=bar_width, label="Total Solar Generation", color='gold') |
|
|
plt.xlabel("Day") |
|
|
plt.ylabel("Energy (kWh)") |
|
|
plt.title("Total Community Demand vs. Solar Generation Per Day") |
|
|
plt.xticks(days) |
|
|
plt.legend() |
|
|
plt.grid(axis='y', linestyle='--', alpha=0.7) |
|
|
plt.savefig(os.path.join(plots_dir, "daily_demand_vs_solar.png")) |
|
|
plt.close() |
|
|
|
|
|
|
|
|
step_group = all_days_df.groupby(["day", "step"]).sum(numeric_only=True).reset_index() |
|
|
step_group["global_step"] = (step_group["day"] - 1) * eval_num_steps + step_group["step"] |
|
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 12), sharex=True) |
|
|
|
|
|
ax1.plot(step_group["global_step"], step_group["grid_import_with_p2p"], label="Grid Import (with P2P)", color='r') |
|
|
ax1.plot(step_group["global_step"], step_group["p2p_buy"], label="P2P Buy", color='g') |
|
|
ax1.set_ylabel("Energy (kWh)") |
|
|
ax1.set_title("Community Energy Consumption: Grid Import vs. P2P Buy") |
|
|
ax1.legend() |
|
|
ax1.grid(True, linestyle='--', alpha=0.6) |
|
|
|
|
|
ax2.plot(step_group["global_step"], step_group["grid_export"], label="Grid Export", color='orange') |
|
|
ax2.plot(step_group["global_step"], step_group["p2p_sell"], label="P2P Sell", color='b') |
|
|
ax2.set_xlabel("Global Timestep") |
|
|
ax2.set_ylabel("Energy (kWh)") |
|
|
ax2.set_title("Community Energy Generation: Grid Export vs. P2P Sell") |
|
|
ax2.legend() |
|
|
ax2.grid(True, linestyle='--', alpha=0.6) |
|
|
|
|
|
plt.tight_layout() |
|
|
plt.savefig(os.path.join(plots_dir, "combined_energy_flows_timeseries.png")) |
|
|
plt.close() |
|
|
|
|
|
|
|
|
daily_agg = all_days_df.groupby("day").sum(numeric_only=True) |
|
|
|
|
|
plt.figure(figsize=(12, 7)) |
|
|
plt.bar(daily_agg.index, daily_agg["grid_import_with_p2p"], label="Grid Import (with P2P)", color='crimson') |
|
|
plt.bar(daily_agg.index, daily_agg["p2p_buy"], bottom=daily_agg["grid_import_with_p2p"], label="P2P Buy", color='limegreen') |
|
|
plt.plot(daily_agg.index, daily_agg["grid_import_no_p2p"], label="Baseline Grid Import (No P2P)", color='blue', linestyle='--', marker='o') |
|
|
|
|
|
plt.xlabel("Day") |
|
|
plt.ylabel("Energy (kWh)") |
|
|
plt.title("Daily Energy Procurement: Baseline vs. P2P+Grid") |
|
|
plt.xticks(daily_agg.index) |
|
|
plt.legend() |
|
|
plt.grid(axis='y', linestyle='--', alpha=0.7) |
|
|
plt.savefig(os.path.join(plots_dir, "daily_energy_procurement_stacked.png")) |
|
|
plt.close() |
|
|
|
|
|
|
|
|
plt.figure(figsize=(12, 6)) |
|
|
plt.plot(plot_daily_df["day"], plot_daily_df["fairness_cost_savings"], label="Cost Savings Fairness", marker='o') |
|
|
plt.plot(plot_daily_df["day"], plot_daily_df["fairness_grid_reduction"], label="Grid Reduction Fairness", marker='s') |
|
|
plt.plot(plot_daily_df["day"], plot_daily_df["fairness_reward"], label="Reward Fairness", marker='^') |
|
|
plt.xlabel("Day") |
|
|
plt.ylabel("Jain's Fairness Index") |
|
|
plt.title("Daily Fairness Metrics") |
|
|
plt.xticks(plot_daily_df["day"]) |
|
|
plt.ylim(0, 1.05) |
|
|
plt.legend() |
|
|
plt.grid(True, linestyle='--', alpha=0.7) |
|
|
plt.savefig(os.path.join(plots_dir, "daily_fairness_metrics.png")) |
|
|
plt.close() |
|
|
|
|
|
|
|
|
fig, ax1 = plt.subplots(figsize=(15, 7)) |
|
|
house_ids_str = house_level_df.index.astype(str) |
|
|
bar_width = 0.4 |
|
|
index = np.arange(len(house_ids_str)) |
|
|
color1 = 'tab:green' |
|
|
ax1.set_xlabel('House ID') |
|
|
ax1.set_ylabel('Total Cost Savings ($)', color=color1) |
|
|
ax1.bar(index - bar_width/2, house_level_df["cost_savings"], bar_width, label='Cost Savings', color=color1) |
|
|
ax1.tick_params(axis='y', labelcolor=color1) |
|
|
ax1.set_xticks(index) |
|
|
ax1.set_xticklabels(house_ids_str, rotation=45, ha="right") |
|
|
ax2 = ax1.twinx() |
|
|
color2 = 'tab:blue' |
|
|
ax2.set_ylabel('Total Grid Import Reduction (kWh)', color=color2) |
|
|
ax2.bar(index + bar_width/2, house_level_df["import_reduction"], bar_width, label='Import Reduction', color=color2) |
|
|
ax2.tick_params(axis='y', labelcolor=color2) |
|
|
plt.title(f'Total Cost Savings & Grid Import Reduction Per House (over {DAYS_TO_EVALUATE} days)') |
|
|
fig.tight_layout() |
|
|
plt.savefig(os.path.join(plots_dir, "per_house_summary.png")) |
|
|
plt.close() |
|
|
|
|
|
|
|
|
day1_prices = all_days_df[all_days_df['day'] == 1][['step', 'grid_price', 'peer_price']].drop_duplicates() |
|
|
plt.figure(figsize=(12, 6)) |
|
|
plt.plot(day1_prices['step'], day1_prices['grid_price'], label='Grid Price', color='darkorange') |
|
|
plt.plot(day1_prices['step'], day1_prices['peer_price'], label='P2P Price', color='teal') |
|
|
plt.xlabel("Timestep of Day") |
|
|
plt.ylabel("Price ($/kWh)") |
|
|
plt.title("Price Dynamics on Day 1") |
|
|
plt.legend() |
|
|
plt.grid(True, linestyle='--', alpha=0.6) |
|
|
plt.savefig(os.path.join(plots_dir, "price_dynamics_day1.png")) |
|
|
plt.close() |
|
|
|
|
|
|
|
|
day1_df = all_days_df[all_days_df['day'] == 1] |
|
|
battery_houses = day1_df.dropna(subset=['soc'])['house'].unique() |
|
|
|
|
|
if len(battery_houses) > 0: |
|
|
sample_houses = battery_houses[:min(4, len(battery_houses))] |
|
|
plt.figure(figsize=(12, 6)) |
|
|
for house in sample_houses: |
|
|
house_df = day1_df[day1_df['house'] == house] |
|
|
plt.plot(house_df['step'], house_df['soc'] * 100, label=f'House {house}') |
|
|
|
|
|
plt.xlabel("Timestep of Day") |
|
|
plt.ylabel("State of Charge (%)") |
|
|
plt.title("Battery SoC on Day 1 for Sample Houses") |
|
|
plt.legend() |
|
|
plt.grid(True, linestyle='--', alpha=0.6) |
|
|
plt.savefig(os.path.join(plots_dir, "soc_dynamics_day1.png")) |
|
|
plt.close() |
|
|
|
|
|
print("Evaluation run completed. All data logs (CSVs) and plots saved to disk.") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |