← Back to All Guides
Guide 07

DER Scenario Planning with Monte Carlo Simulation

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 12 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 14% 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.

1

Load Scenario Configurations

import json import pandas as pd import numpy as np import matplotlib.pyplot as plt # Point this to your local clone of the SP&L repo # Windows example: "C:/Users/YourName/Documents/sisyphean-power-and-light/" # macOS example: "/Users/YourName/Documents/sisyphean-power-and-light/" # Tip: Python on Windows accepts forward slashes — no backslashes needed DATA_DIR = "sisyphean-power-and-light/" # Load the three scenario definitions 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']}%")
2

Load Feeder Capacity Data

# Load baseline feeder loads to understand current utilization load = pd.read_parquet(DATA_DIR + "timeseries/substation_load_hourly.parquet") # Calculate peak load and average load per feeder 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() # Assume rated capacity is 20% above historical peak 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.

3

Define the Monte Carlo Parameters

# Uncertain parameters and their ranges # Solar: 15-40% penetration by 2030 (uniform distribution) # EV: 8-25% penetration by 2030 (uniform distribution) # Average solar system size: 5-10 kW # Average EV charger load: 6-12 kW (Level 2) N_SIMULATIONS = 1000 N_FEEDERS = 12 TOTAL_CUSTOMERS = 48000 # Store results results = [] np.random.seed(42) # for reproducibility for sim in range(N_SIMULATIONS): # Draw random adoption rates 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) # Distribute adoptions across feeders (proportional to customers) for _, feeder in feeder_summary.iterrows(): fid = feeder["feeder_id"] customers = feeder["customer_count"] # Number of new solar and EV installations on this feeder n_solar = int(customers * solar_pct) n_ev = int(customers * ev_pct) # Peak impact on the feeder (MW) # Solar reduces net load during day, EV adds load during evening solar_mw = (n_solar * solar_kw) / 1000 ev_mw = (n_ev * ev_kw) / 1000 # Net peak impact: EV adds, solar doesn't help at evening peak 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):,}")
4

Analyze Results: Which Feeders Are at Risk?

# Probability of exceeding capacity per feeder 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}") # Plot 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()
5

Visualize the Uncertainty Distribution

# For the highest-risk feeder, show the distribution of peak loads 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()
6

Compare Scenarios

# Run three deterministic scenarios for comparison 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) / 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()

What You Built and Next Steps

  1. Loaded the three pre-built SP&L scenario configurations
  2. Calculated feeder headroom from historical peak loads
  3. Ran 1,000 Monte Carlo simulations with random adoption rates
  4. Identified which feeders are most likely to exceed capacity by 2030
  5. Visualized the probability distribution of peak loads
  6. 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
  • 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

Key Terms Glossary

  • Monte Carlo simulation — running a model many times with random inputs to estimate outcome distributions
  • DER penetration — the percentage of customers with distributed energy resources (solar, storage, EVs)
  • Headroom — remaining capacity between current peak and the feeder's rated limit
  • Scenario planning — evaluating multiple plausible futures rather than betting on a single forecast
  • Coincident peak — the peak demand that occurs when multiple loads (like EV chargers) operate simultaneously
  • No-regrets investment — an action that pays off regardless of which future materializes

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 →