← Back to All Guides
Guide 03

Hosting Capacity Analysis with Power Flow

What You Will Learn

Before a utility can approve a new solar installation, it needs to know whether the local grid can handle the extra power. "Hosting capacity" is the maximum amount of distributed generation a feeder can accept before running into voltage or thermal problems. In this guide you will:

  • Understand what hosting capacity means and why it matters
  • Load the SP&L network model into OpenDSS through Python
  • Run a baseline power flow to see normal voltages and currents
  • Incrementally add solar generation and re-run power flow at each step
  • Identify the point where voltage or thermal violations appear
  • Plot a hosting capacity curve for a feeder

What is power flow analysis? Power flow (or "load flow") solves the physics equations of an electrical network. Given the loads and generators, it calculates the voltage at every bus and the current through every wire. It's the bread-and-butter tool of power systems engineering—and OpenDSS does it for you.

SP&L Data You Will Use

  • network/master.dss — the main OpenDSS model file that ties the network together
  • network/lines.dss — line segment definitions with impedance and ampacity
  • network/transformers.dss — transformer ratings and impedance
  • network/loads.dss — load allocations at each bus
  • network/coordinates.csv — bus XY locations for mapping

Additional Libraries

pip install opendssdirect.py

opendssdirect.py is the official Python interface to the OpenDSS power flow engine. It lets you control simulations entirely from Python.

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.

OpenDSS on Windows vs. macOS/Linux: On Windows, OpenDSS also has a standalone installer from EPRI (the COM interface), but you do not need it for this guide—pip install opendssdirect.py works on all platforms. If you already have the Windows COM version installed, opendssdirect.py will still work fine alongside it.

1

Connect to the OpenDSS Model

import opendssdirect as dss 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/" # Compile the OpenDSS model dss.Text.Command(f"Compile [{DATA_DIR}network/master.dss]") # Verify the model loaded print(f"Circuit name: {dss.Circuit.Name()}") print(f"Total buses: {dss.Circuit.NumBuses()}") print(f"Total loads: {dss.Loads.Count()}") print(f"Total lines: {dss.Lines.Count()}")

What does "Compile" do? It reads all the .dss files, builds the network model in memory, and gets ready to solve power flow equations. Think of it as loading a spreadsheet—but for an entire electrical grid.

2

Run Baseline Power Flow

First, solve the network under normal conditions (no added solar) to see what voltages and currents look like.

# Solve the power flow dss.Solution.Solve() # Check if the solution converged print(f"Converged: {dss.Solution.Converged()}") # Collect voltages at every bus bus_names = dss.Circuit.AllBusNames() bus_voltages = dss.Circuit.AllBusMagPu() # Per-unit voltage means 1.0 is nominal. ANSI C84.1 says # service voltage should stay between 0.95 and 1.05 p.u. print(f"\nMin voltage: {min(bus_voltages):.4f} p.u.") print(f"Max voltage: {max(bus_voltages):.4f} p.u.")
Converged: True

Min voltage: 0.9712 p.u.
Max voltage: 1.0243 p.u.

All voltages are within the ANSI limits (0.95–1.05 p.u.) under baseline conditions. Good—this confirms the network starts healthy.

3

Choose a Feeder to Study

# List all buses on Feeder F03 (a mid-sized residential feeder) feeder_buses = [b for b in bus_names if b.startswith("f03")] print(f"Buses on Feeder F03: {len(feeder_buses)}") # Load bus coordinates for mapping coords = pd.read_csv(DATA_DIR + "network/coordinates.csv") feeder_coords = coords[coords["bus_name"].str.startswith("f03")] # Plot the feeder layout plt.figure(figsize=(8, 8)) plt.scatter(feeder_coords["x"], feeder_coords["y"], c="#5FCCDB", s=30) plt.title("Feeder F03 Bus Locations") plt.xlabel("X Coordinate") plt.ylabel("Y Coordinate") plt.tight_layout() plt.show()
4

Incrementally Add Solar and Re-Solve

This is the core of hosting capacity analysis. We add a small amount of solar generation to a bus, solve the power flow, check for violations, add more, solve again, and repeat until something breaks.

# Choose a bus near the end of the feeder (worst case for voltage rise) test_bus = "f03_bus_12" # We'll add solar in 50 kW steps from 0 to 2,000 kW pv_steps = range(0, 2050, 50) results = [] for pv_kw in pv_steps: # Re-compile the model fresh each time dss.Text.Command(f"Compile [{DATA_DIR}network/master.dss]") # Add a PV generator at the test bus if pv_kw > 0: dss.Text.Command( f"New Generator.PV_test Bus1={test_bus} kW={pv_kw} PF=1 Phases=3" ) # Solve power flow dss.Solution.Solve() # Read the voltage at the test bus dss.Circuit.SetActiveBus(test_bus) voltage_pu = dss.Bus.puVmagAngle()[0] # magnitude in p.u. # Check for thermal violation on the connected line # (current as percentage of rated ampacity) dss.Circuit.SetActiveElement("Line.f03_line_12") loading_pct = max(dss.CktElement.NormalAmps(), 0.01) current = max(abs(c) for c in dss.CktElement.CurrentsMagAng()[::2]) thermal_loading = (current / loading_pct) * 100 results.append({ "pv_kw": pv_kw, "voltage_pu": voltage_pu, "thermal_loading_pct": thermal_loading }) hc = pd.DataFrame(results) print(hc.head(10))

What are the violation limits? Two things can go wrong when you add too much solar: Voltage violation—voltage rises above 1.05 p.u. (ANSI C84.1 upper limit). Thermal violation—current exceeds the wire's rated ampacity (100% loading). The hosting capacity is the solar level just before either violation occurs.

5

Find the Hosting Capacity

# Find the first PV level that causes a voltage violation voltage_limit = 1.05 voltage_violation = hc[hc["voltage_pu"] > voltage_limit] # Find the first PV level that causes a thermal violation thermal_limit = 100 thermal_violation = hc[hc["thermal_loading_pct"] > thermal_limit] if not voltage_violation.empty: hc_voltage = voltage_violation.iloc[0]["pv_kw"] print(f"Voltage-limited hosting capacity: {hc_voltage} kW") else: print("No voltage violation up to 2,000 kW") if not thermal_violation.empty: hc_thermal = thermal_violation.iloc[0]["pv_kw"] print(f"Thermal-limited hosting capacity: {hc_thermal} kW") else: print("No thermal violation up to 2,000 kW")
6

Plot the Hosting Capacity Curve

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True) # Voltage plot ax1.plot(hc["pv_kw"], hc["voltage_pu"], color="#5FCCDB", linewidth=2) ax1.axhline(y=1.05, color="red", linestyle="--", label="ANSI Upper Limit (1.05)") ax1.axhline(y=0.95, color="orange", linestyle="--", label="ANSI Lower Limit (0.95)") ax1.set_ylabel("Voltage (p.u.)") ax1.set_title("Hosting Capacity Analysis — Feeder F03, Bus 12") ax1.legend() ax1.grid(True, alpha=0.3) # Thermal loading plot ax2.plot(hc["pv_kw"], hc["thermal_loading_pct"], color="#2D6A7A", linewidth=2) ax2.axhline(y=100, color="red", linestyle="--", label="Thermal Limit (100%)") ax2.set_ylabel("Line Loading (%)") ax2.set_xlabel("Added PV Generation (kW)") ax2.legend() ax2.grid(True, alpha=0.3) plt.tight_layout() plt.show()

The hosting capacity curve shows voltage rising and thermal loading increasing as you add more solar. The point where either line crosses its limit is the hosting capacity for that bus.

7

Test Multiple Buses

Different locations on the feeder will have different hosting capacities. Buses near the substation can handle more solar because the electrical "distance" is short. Buses at the end of long feeders hit voltage limits sooner.

# Run the analysis for several buses along the feeder test_buses = ["f03_bus_02", "f03_bus_05", "f03_bus_08", "f03_bus_12"] hosting_caps = {} for bus in test_buses: for pv_kw in range(0, 2050, 50): dss.Text.Command(f"Compile [{DATA_DIR}network/master.dss]") if pv_kw > 0: dss.Text.Command( f"New Generator.PV_test Bus1={bus} kW={pv_kw} PF=1 Phases=3" ) dss.Solution.Solve() dss.Circuit.SetActiveBus(bus) voltage = dss.Bus.puVmagAngle()[0] if voltage > 1.05: hosting_caps[bus] = pv_kw - 50 # last safe level break else: hosting_caps[bus] = 2000 # no violation found # Display results hc_df = pd.DataFrame(list(hosting_caps.items()), columns=["Bus", "Hosting Capacity (kW)"]) print(hc_df) # Bar chart hc_df.plot(x="Bus", y="Hosting Capacity (kW)", kind="bar", color="#5FCCDB", legend=False) plt.title("Hosting Capacity by Bus Location on Feeder F03") plt.ylabel("Hosting Capacity (kW)") plt.tight_layout() plt.show()

What You Built and Next Steps

You just performed a hosting capacity analysis on a realistic distribution feeder. Here's what you accomplished:

  1. Loaded the SP&L OpenDSS network model into Python
  2. Ran a baseline power flow and confirmed healthy voltages
  3. Incrementally added solar generation and detected violations
  4. Identified both voltage and thermal hosting capacity limits
  5. Plotted hosting capacity curves and compared multiple bus locations

Ideas to Try Next

  • Time-series analysis: Run the analysis across 24 hours using the SP&L load profiles to see how hosting capacity varies by time of day
  • Add smart inverters: Model inverters with Volt-VAR mode to see how they increase hosting capacity
  • Analyze all 12 feeders: Build a full hosting capacity map of the SP&L service territory
  • Include existing solar: Load the PV systems from network/pv_systems.dss before adding new ones
  • Train an ML surrogate: Use the power flow results to train a fast ML model that estimates hosting capacity without running OpenDSS

Key Terms Glossary

  • Hosting capacity — maximum DER generation a feeder can accept without violations
  • Power flow — solving Kirchhoff's laws to find voltages and currents in a network
  • Per-unit (p.u.) — voltage expressed as a fraction of the nominal voltage; 1.0 is "normal"
  • ANSI C84.1 — the US standard defining acceptable voltage ranges (0.95–1.05 p.u.)
  • Thermal limit — the maximum current a conductor can carry before overheating
  • Ampacity — the rated current-carrying capacity of a conductor
  • OpenDSS — open-source distribution system simulator from EPRI

Ready to Level Up?

In the advanced guide, you'll train a surrogate ML model for 100x faster hosting capacity screening with probabilistic estimates.

Go to Advanced Hosting Capacity →