SolarSys2025's picture
Upload 30 files
55da406 verified
# pg_evaluate.py
import os
import sys
import time
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
from datetime import datetime
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from solar_sys_environment import SolarSys
from PG.trainer.pg import PGAgent
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def compute_jains_fairness(values: np.ndarray) -> float:
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()
return num / den
def main():
# User parameters
MODEL_PATH = "/path/to/project/pg_pennsylvania_10agents_10000eps/logs"
DATA_PATH = "/path/to/project/testing/10houses_30days_TEST.csv"
DAYS_TO_EVALUATE = 30
model_path = MODEL_PATH
data_path = DATA_PATH
days_to_evaluate = DAYS_TO_EVALUATE
SOLAR_THRESHOLD = 0.5
state_match = re.search(r"pg_(oklahoma|colorado|pennsylvania)_", model_path)
if not state_match:
raise ValueError(
"Could not automatically detect the state (oklahoma, colorado, or pennsylvania) "
"from the model path. Please ensure your model's parent folder is named correctly, "
"e.g., 'pg_oklahoma_...'"
)
detected_state = state_match.group(1)
print(f"--- Detected state: {detected_state.upper()} ---")
# Env setup
env = SolarSys(
data_path=data_path,
state=detected_state,
time_freq="15T"
)
eval_steps = env.num_steps
house_ids = env.house_ids
num_agents = env.num_agents
# Generate a unique eval run folder
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
run_name = f"eval_pg_{num_agents}agents_{days_to_evaluate}days_{timestamp}"
output_folder = os.path.join("runs_with_battery", 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)
print(f"Saving evaluation outputs to: {output_folder}")
local_dim = env.observation_space.shape[1]
act_dim = env.action_space.shape[1]
# Initialize PG agents
pg_agents = []
for i in range(num_agents):
agent = PGAgent(
state_dim=local_dim,
action_dim=act_dim,
lr=2e-4,
gamma=0.95,
)
# Load individual agent model
agent_model_path = os.path.join(model_path, f"best_model_agent_{i}.pth")
if os.path.exists(agent_model_path):
agent.load(agent_model_path)
print(f"Loaded model for agent {i}")
else:
print(f"WARNING: Model file not found for agent {i}: {agent_model_path}")
# Alternative: try loading a single model for all agents
single_model_path = os.path.join(model_path, "best_model.pth")
if os.path.exists(single_model_path):
agent.load(single_model_path)
print(f"Loaded single model for agent {i}")
agent.model.to(device).eval()
pg_agents.append(agent)
# Prepare logs
all_logs = []
daily_summaries = []
step_timing_list = []
evaluation_start = time.time()
for day_idx in range(days_to_evaluate):
obs, _ = env.reset() # Using the new reset signature
done = False
step_count = 0
day_logs = []
while not done:
step_start_time = time.time()
# Select actions with PG
actions = []
with torch.no_grad():
for i in range(num_agents):
# Convert observation to tensor and move to device
state = torch.FloatTensor(obs[i]).unsqueeze(0).to(device)
# Get action from actor network
mean, log_std, _ = pg_agents[i].model(state)
# For evaluation, use mean action (deterministic)
action = mean.squeeze(0).cpu().numpy()
# Clip to [0, 1] range
action = np.clip(action, 0.0, 1.0)
actions.append(action)
actions = np.array(actions, dtype=np.float32)
next_obs, rewards, done, info = env.step(actions)
# Consolidated Logging
step_end_time = time.time()
step_duration = step_end_time - step_start_time
# REMOVED: print(f"[Day {day_idx+1}, Step {step_count}] Step time: {step_duration:.6f} seconds")
step_timing_list.append({
"day": day_idx + 1,
"step": step_count,
"step_time_s": step_duration
})
grid_price_now = env.get_grid_price(step_count)
# Use the environment's current total surplus/shortfall to re-calculate peer price
current_demands = env.demands_day[step_count]
current_solars = env.solars_day[step_count]
current_total_surplus = float(np.maximum(current_solars - current_demands, 0.0).sum())
current_total_shortfall = float(np.maximum(current_demands - current_solars, 0.0).sum())
peer_price_now = env.get_peer_price(step_count, current_total_surplus, current_total_shortfall)
for i, hid in enumerate(house_ids):
is_battery_house = hid in env.batteries
p2p_buy = float(info["p2p_buy"][i])
p2p_sell = float(info["p2p_sell"][i])
charge_amount = float(info.get("charge_amount")[i])
discharge_amount = float(info.get("discharge_amount")[i])
day_logs.append({
"day": day_idx + 1,
"step": step_count,
"house": hid,
"grid_import_no_p2p": float(info["grid_import_no_p2p"][i]),
"grid_import_with_p2p": float(info["grid_import_with_p2p"][i]),
"grid_export": float(info.get("grid_export")[i]),
"p2p_buy": p2p_buy,
"p2p_sell": p2p_sell,
"actual_cost": float(info["costs"][i]),
"baseline_cost": float(info["grid_import_no_p2p"][i]) * grid_price_now,
"total_demand": float(env.demands_day[step_count, i]),
"total_solar": float(env.solars_day[step_count, i]),
"grid_price": grid_price_now,
"peer_price": peer_price_now,
"soc": (env.battery_soc[i] / env.battery_max_capacity[i]) if is_battery_house else np.nan,
"degradation_cost": ((charge_amount + discharge_amount) * env.battery_degradation_cost[i]) if is_battery_house else 0.0,
"reward": float(rewards[i]),
})
obs = next_obs
step_count += 1
if step_count >= eval_steps:
break
day_df = pd.DataFrame(day_logs)
all_logs.extend(day_logs)
# Consolidated daily summary calculation (Kept math, removed console output)
grouped_house = day_df.groupby("house").sum(numeric_only=True)
grouped_step = day_df.groupby("step").sum(numeric_only=True)
total_demand = grouped_step["total_demand"].sum()
total_solar = grouped_step["total_solar"].sum()
total_p2p_buy = grouped_house["p2p_buy"].sum()
total_p2p_sell = grouped_house["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_buy = compute_jains_fairness(grouped_house["p2p_buy"].values)
fairness_p2p_sell = compute_jains_fairness(grouped_house["p2p_sell"].values)
fairness_p2p_total = compute_jains_fairness((grouped_house["p2p_buy"] + grouped_house["p2p_sell"]).values)
day_total_degradation_cost = grouped_house["degradation_cost"].sum()
daily_summaries.append({
"day": day_idx + 1, "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": fairness_p2p_buy, "fairness_p2p_sell": fairness_p2p_sell,
"fairness_p2p_total": fairness_p2p_total, "total_degradation_cost": day_total_degradation_cost
})
# Final processing and saving
evaluation_end = time.time()
total_eval_time = evaluation_end - evaluation_start
# REMOVED: print(f"\nEvaluation loop finished. Total time: {total_eval_time:.2f} seconds.")
# REMOVED: print(f"Device used: {device}")
all_days_df = pd.DataFrame(all_logs)
combined_csv_path = os.path.join(logs_dir, "step_logs_all_days.csv")
all_days_df.to_csv(combined_csv_path, index=False)
print(f"Saved combined step-level logs to: {combined_csv_path}")
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)
print(f"Saved step timing logs to: {timing_csv_path}")
house_level_df = all_days_df.groupby("house").sum(numeric_only=True)
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)
print(f"Saved final summary per house to: {house_summary_csv}")
fairness_grid_all = compute_jains_fairness(house_level_df["import_reduction"].values)
fairness_cost_all = compute_jains_fairness(house_level_df["cost_savings"].values)
daily_summary_df = pd.DataFrame(daily_summaries)
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 = daily_summary_df["total_degradation_cost"].sum()
agg_solar_per_step = all_days_df.groupby(['day', 'step'])['total_solar'].sum()
sunny_steps_mask = agg_solar_per_step > (SOLAR_THRESHOLD * num_agents)
sunny_df = all_days_df.set_index(['day', 'step'])[sunny_steps_mask].reset_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 = 0.0
if baseline_import_sunny > 0:
grid_reduction_sunny_pct = (baseline_import_sunny - actual_import_sunny) / baseline_import_sunny
total_p2p_buy = all_days_df['p2p_buy'].sum()
total_actual_grid_import = all_days_df['grid_import_with_p2p'].sum()
total_procured_energy = total_p2p_buy + total_actual_grid_import
community_sourcing_rate_pct = 0.0
if total_procured_energy > 0:
community_sourcing_rate_pct = total_p2p_buy / total_procured_energy
total_p2p_sell = all_days_df['p2p_sell'].sum()
total_grid_export = all_days_df['grid_export'].sum()
total_excess_solar = total_p2p_sell + total_grid_export
solar_sharing_efficiency_pct = 0.0
if total_excess_solar > 0:
solar_sharing_efficiency_pct = total_p2p_sell / total_excess_solar
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
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,
"community_sourcing_rate_pct": community_sourcing_rate_pct, "solar_sharing_efficiency_pct": solar_sharing_efficiency_pct,
"cost_savings_sunny_hours_pct": cost_savings_sunny_pct # Added back for final row saving
}
for col in daily_summary_df.columns:
if col not in final_row:
final_row[col] = np.nan
final_row_df = pd.DataFrame([final_row])
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(f"Saved day-level summary with final multi-day row to: {summary_csv}")
# The rest of the script (plotting) remains unchanged as it doesn't print numerical results to the console.
# Plots
plot_daily_df = daily_summary_df[daily_summary_df["day"] != "ALL_DAYS_SUMMARY"].copy()
plot_daily_df["day"] = plot_daily_df["day"].astype(int)
# Daily Cost Savings Percentage
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()
# Daily Total Demand vs. Solar
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()
# Combined Time Series of Energy Flows
step_group = all_days_df.groupby(["day", "step"]).sum(numeric_only=True).reset_index()
step_group["global_step"] = (step_group["day"] - 1) * env.num_steps + step_group["step"]
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 12), sharex=True)
# Subplot 1: Grid Import vs P2P Buy
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)
# Subplot 2: Grid Export vs P2P Sell
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()
# Stacked Bar of Daily Energy Sources
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()
# Fairness Metrics Over Time
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()
# Per-House Savings and Reductions
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()
# Price Dynamics for a Single Day
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()
# Battery State of Charge for Sample Houses
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("All plots have been generated and saved. Evaluation complete.")
if __name__ == "__main__":
main()