Guide 07: DER scenario planning.
Guide 07
Prefer not to install anything? Click the badge above to open this guide as a runnable notebook in Google Colab. Sign in with any Google account, then use Runtime → Run all to execute every cell, or step through them one at a time.
What You Will Learn
How many rooftop solar panels will be installed in your service territory by 2030? How many electric vehicles? Nobody knows for certain. DER scenario planning uses simulation to explore a range of possible futures and identify which feeders are most likely to run into capacity problems. In this guide you will:
- Understand the SP&L scenario configurations (Baseline 2025, High DER 2030, EV Adoption 2030)
- Build a Monte Carlo simulation that models uncertain solar and EV adoption rates
- Distribute new DER installations across the 104 SP&L feeders
- Identify which feeders exceed capacity thresholds under different scenarios
- Visualize the probability distribution of outcomes
What is Monte Carlo simulation? Instead of predicting a single future, Monte Carlo simulation runs your model thousands of times with slightly different random inputs. Each run produces a different outcome. By looking at all the outcomes together, you get a probability distribution—not just "what might happen" but "how likely is each outcome." It is named after the famous casino because it relies on randomness.
SP&L Data You Will Use
- scenarios/baseline_2025.json — current-state system with 21% DER penetration
- scenarios/high_der_2030.json — 35% solar penetration scenario
- scenarios/ev_adoption_2030.json — 20% residential EV penetration
- timeseries/pv_generation.parquet — existing PV output profiles
- timeseries/ev_charging.parquet — EV charging load shapes
- timeseries/substation_load_hourly.parquet — baseline feeder loads
Additional Libraries
Nothing beyond the base prerequisites: pandas, numpy, matplotlib. Also pyarrow for Parquet files.
Which terminal should I use? On Windows, open Anaconda Prompt from the Start Menu (or PowerShell / Command Prompt if Python is already in your PATH). On macOS, open Terminal from Applications → Utilities. On Linux, open your default terminal. All pip install commands work the same across platforms.
import json
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
DATA_DIR = "sisyphean-power-and-light/"
with open(DATA_DIR + "scenarios/baseline_2025.json") as f:
baseline = json.load(f)
with open(DATA_DIR + "scenarios/high_der_2030.json") as f:
high_der = json.load(f)
with open(DATA_DIR + "scenarios/ev_adoption_2030.json") as f:
ev_scenario = json.load(f)
print("Baseline 2025:")
print(f" Solar penetration: {baseline['solar_penetration_pct']}%")
print(f" EV penetration: {baseline['ev_penetration_pct']}%")
print(f"\nHigh DER 2030:")
print(f" Solar penetration: {high_der['solar_penetration_pct']}%")
print(f"\nEV Adoption 2030:")
print(f" EV penetration: {ev_scenario['ev_penetration_pct']}%")
load = pd.read_parquet(DATA_DIR + "timeseries/substation_load_hourly.parquet")
feeder_summary = load.groupby("feeder_id").agg(
peak_mw=("total_load_mw", "max"),
avg_mw=("total_load_mw", "mean"),
customer_count=("customer_count", "first")
).reset_index()
feeder_summary["rated_capacity_mw"] = feeder_summary["peak_mw"] * 1.2
feeder_summary["headroom_mw"] = (feeder_summary["rated_capacity_mw"]
- feeder_summary["peak_mw"])
print(feeder_summary.to_string(index=False))
What is headroom? Headroom is the difference between a feeder's rated capacity and its current peak load. If a feeder has 1 MW of headroom, it can absorb up to 1 MW of additional load (like EV charging) before it hits its limit. Feeders with little headroom are at risk of overload as DER adoption grows.
N_SIMULATIONS = 1000
N_FEEDERS = 104
TOTAL_CUSTOMERS = 238000
results = []
np.random.seed(42)
for sim in range(N_SIMULATIONS):
solar_pct = np.random.uniform(0.15, 0.40)
ev_pct = np.random.uniform(0.08, 0.25)
solar_kw = np.random.uniform(5, 10)
ev_kw = np.random.uniform(6, 12)
for _, feeder in feeder_summary.iterrows():
fid = feeder["feeder_id"]
customers = feeder["customer_count"]
n_solar = int(customers * solar_pct)
n_ev = int(customers * ev_pct)
solar_mw = (n_solar * solar_kw) / 1000
ev_coincidence = 0.25
ev_mw = (n_ev * ev_kw * ev_coincidence) / 1000
new_peak = feeder["peak_mw"] + ev_mw
exceeds_capacity = new_peak > feeder["rated_capacity_mw"]
results.append({
"sim": sim, "feeder_id": fid,
"solar_pct": solar_pct, "ev_pct": ev_pct,
"new_peak_mw": new_peak,
"rated_capacity_mw": feeder["rated_capacity_mw"],
"exceeds_capacity": exceeds_capacity,
"solar_generation_mw": solar_mw,
"ev_load_mw": ev_mw
})
results_df = pd.DataFrame(results)
print(f"Total simulation runs: {len(results_df):,}")
risk_by_feeder = results_df.groupby("feeder_id")["exceeds_capacity"].mean()
risk_by_feeder = risk_by_feeder.sort_values(ascending=False)
print("Probability of exceeding capacity by 2030:\n")
for fid, prob in risk_by_feeder.items():
bar = "#" * int(prob * 50)
print(f" {fid}: {prob:>5.1%} {bar}")
fig, ax = plt.subplots(figsize=(10, 6))
risk_by_feeder.plot(kind="bar", color="#5FCCDB", ax=ax)
ax.axhline(y=0.5, color="red", linestyle="--", label="50% probability threshold")
ax.set_ylabel("Probability of Overload")
ax.set_title("Feeder Overload Risk by 2030 (Monte Carlo)")
ax.legend()
plt.tight_layout()
plt.show()
highest_risk_feeder = risk_by_feeder.idxmax()
feeder_results = results_df[results_df["feeder_id"] == highest_risk_feeder]
rated_cap = feeder_results["rated_capacity_mw"].iloc[0]
fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(feeder_results["new_peak_mw"], bins=50, color="#5FCCDB",
edgecolor="white", alpha=0.8)
ax.axvline(x=rated_cap, color="red", linewidth=2, linestyle="--",
label=f"Rated Capacity ({rated_cap:.1f} MW)")
ax.set_xlabel("Projected Peak Load (MW)")
ax.set_ylabel("Number of Simulations")
ax.set_title(f"Peak Load Distribution — Feeder {highest_risk_feeder} (2030)")
ax.legend()
plt.tight_layout()
plt.show()
scenarios = {
"Baseline 2025": {"solar_pct": 0.14, "ev_pct": 0.03},
"Moderate 2030": {"solar_pct": 0.25, "ev_pct": 0.12},
"High DER 2030": {"solar_pct": 0.35, "ev_pct": 0.20},
}
fig, axes = plt.subplots(1, 3, figsize=(16, 5), sharey=True)
for ax, (name, params) in zip(axes, scenarios.items()):
peaks = []
for _, feeder in feeder_summary.iterrows():
ev_mw = (feeder["customer_count"] * params["ev_pct"] * 8 * 0.25) / 1000
new_peak = feeder["peak_mw"] + ev_mw
peaks.append({
"feeder": feeder["feeder_id"],
"peak": new_peak,
"capacity": feeder["rated_capacity_mw"]
})
pdf = pd.DataFrame(peaks)
colors = ["#fc8181" if r["peak"] > r["capacity"] else "#5FCCDB"
for _, r in pdf.iterrows()]
ax.bar(pdf["feeder"], pdf["peak"], color=colors)
ax.set_title(name)
ax.set_xlabel("Feeder")
if ax == axes[0]:
ax.set_ylabel("Peak Load (MW)")
ax.tick_params(axis="x", rotation=45)
plt.suptitle("Feeder Peak Loads Under Different DER Scenarios", fontsize=14)
plt.tight_layout()
plt.show()
- Loaded the three pre-built SP&L scenario configurations
- Calculated feeder headroom from historical peak loads
- Ran 1,000 Monte Carlo simulations with random adoption rates
- Identified which feeders are most likely to exceed capacity by 2030
- Visualized the probability distribution of peak loads
- Compared deterministic scenario outcomes side by side
Ideas to Try Next
- Add spatial clustering: Model EV adoption as spatially clustered (neighbors influence each other) rather than uniform
- Include time-of-day: Use actual EV charging shapes from
timeseries/ev_charging.parquet to model coincident peak demand
- Analyze daytime minimum load: Model midday scenarios where high solar generation creates reverse power flow and voltage rise issues
- Add battery storage: Simulate community storage installations that offset peak EV charging load
- Connect to hosting capacity: Feed Monte Carlo solar results into the hosting capacity analysis from Guide 03
- Use the extreme weather scenario: Load
scenarios/extreme_weather.json and stress-test against 10-year storm events
Ready to Level Up?
In the advanced guide, you'll build stochastic optimization models for grid upgrade planning with cost-benefit analysis and investment roadmaps.
Go to Advanced DER Planning →
— Adam · adam@sgridworks.com