What You Will Learn
When a fault occurs on a distribution feeder, customers lose power. FLISR (Fault Location, Isolation, and Service Restoration) is an automated system that detects the fault, opens switches to isolate the damaged section, and closes other switches to re-energize unaffected customers from an alternate source. In this guide you will:
- Model the SP&L distribution network as a graph using NetworkX
- Simulate a fault and manually walk through the FLISR steps
- Write an algorithm that automatically finds the optimal switching sequence
- Calculate how many Customer Minutes Interrupted (CMI) the automation avoids
- Replay a real storm from the SP&L outage history and measure the improvement
What is a graph in this context? A graph is a mathematical structure made of nodes (buses) connected by edges (line segments and switches). The electrical grid is naturally a graph. Graph algorithms let us answer questions like "which customers are connected to which source?" and "what's the shortest path between two buses?"
SP&L Data You Will Use
- network/lines.dss — 147 line segments defining feeder connectivity
- network/switches.dss — 23 switching devices (reclosers, sectionalizers, tie switches)
- network/loads.dss — customer counts and load at each bus
- outages/outage_events.csv — historical outage events to replay
- outages/crew_dispatch.csv — crew dispatch and restoration times
Additional Libraries
pip install networkx
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 networkx as nx
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
DATA_DIR = "sisyphean-power-and-light/"
lines = []
with open(DATA_DIR + "network/lines.dss") as f:
for line in f:
if line.strip().startswith("New Line"):
bus1 = re.search(r"Bus1=(\S+)", line)
bus2 = re.search(r"Bus2=(\S+)", line)
name = re.search(r"New Line\.(\S+)", line)
if bus1 and bus2 and name:
lines.append({
"name": name.group(1),
"bus1": bus1.group(1).split(".")[0],
"bus2": bus2.group(1).split(".")[0]
})
G = nx.Graph()
for l in lines:
G.add_edge(l["bus1"], l["bus2"], name=l["name"], device_type="line")
print(f"Network graph: {G.number_of_nodes()} buses, {G.number_of_edges()} edges")
switches = pd.read_csv(DATA_DIR + "assets/switches.csv")
for _, sw in switches.iterrows():
bus1, bus2 = sw["bus1"], sw["bus2"]
if G.has_edge(bus1, bus2):
G[bus1][bus2]["has_switch"] = True
G[bus1][bus2]["switch_name"] = sw["switch_id"]
G[bus1][bus2]["scada_controlled"] = sw["scada_controlled"]
loads = pd.read_csv(DATA_DIR + "network/loads_summary.csv")
for _, load in loads.iterrows():
bus = load["bus_name"]
if bus in G:
G.nodes[bus]["customers"] = load["customer_count"]
G.nodes[bus]["load_kw"] = load["load_kw"]
G.nodes["sourcebus"]["is_source"] = True
print(f"Switches found: {len(switches)}")
coords = pd.read_csv(DATA_DIR + "network/coordinates.csv")
pos = {row["bus_name"]: (row["x"], row["y"]) for _, row in coords.iterrows()
if row["bus_name"] in G}
node_colors = [G.nodes[n].get("customers", 0) for n in G.nodes]
fig, ax = plt.subplots(figsize=(12, 10))
nx.draw(G, pos, ax=ax, node_size=20, node_color=node_colors,
cmap=plt.cm.YlOrRd, edge_color="#cccccc", width=0.5)
ax.set_title("SP&L Distribution Network")
plt.tight_layout()
plt.show()
Let's simulate what happens when a line segment fails. We remove the faulted edge from the graph and count how many customers lose power.
fault_line = "f03_line_08"
fault_edge = None
for u, v, data in G.edges(data=True):
if data.get("name") == fault_line:
fault_edge = (u, v)
break
print(f"Simulating fault on: {fault_line}")
print(f"Between buses: {fault_edge[0]} — {fault_edge[1]}")
G_faulted = G.copy()
G_faulted.remove_edge(*fault_edge)
if nx.has_path(G_faulted, "sourcebus", fault_edge[0]):
powered_buses = nx.descendants(G_faulted, "sourcebus") | {"sourcebus"}
else:
powered_buses = set()
de_energized = set(G.nodes()) - powered_buses
affected_customers = sum(
G.nodes[n].get("customers", 0) for n in de_energized
)
print(f"\nBuses de-energized: {len(de_energized)}")
print(f"Customers affected: {affected_customers}")
Now build the automatic FLISR algorithm. The three steps are: (1) Locate the fault, (2) Isolate the faulted section by opening the nearest switches, (3) Restore power to unfaulted sections by closing tie switches.
def flisr_response(G, fault_edge, source="sourcebus"):
"""Simulate FLISR: isolate fault and restore via alternate path."""
print(f"FAULT DETECTED on {fault_edge}")
G_work = G.copy()
G_work.remove_edge(*fault_edge)
isolation_switches = []
for u, v, data in G.edges(data=True):
if data.get("has_switch"):
try:
path = nx.shortest_path(G, fault_edge[0], u)
if len(path) <= 4:
isolation_switches.append((u, v, data["switch_name"]))
except nx.NetworkXNoPath:
pass
print(f"ISOLATING via switches: {[s[2] for s in isolation_switches]}")
components = list(nx.connected_components(G_work))
source_component = [c for c in components if source in c][0]
island_components = [c for c in components if source not in c]
restored_customers = 0
for island in island_components:
customers_in_island = sum(
G.nodes[n].get("customers", 0) for n in island
)
restored_customers += customers_in_island
print(f"RESTORED {customers_in_island} customers via alternate feed")
return restored_customers
restored = flisr_response(G, fault_edge)
print(f"\nTotal customers restored by FLISR: {restored}")
outages = pd.read_csv(DATA_DIR + "outages/outage_events.csv",
parse_dates=["fault_detected", "service_restored"])
crews = pd.read_csv(DATA_DIR + "outages/crew_dispatch.csv",
parse_dates=["dispatch_time", "arrival_time"])
flisr_time_minutes = 1
sample_outages = outages[outages["cause_code"].isin(
["equipment_failure", "vegetation"]
)].head(20)
for _, event in sample_outages.iterrows():
manual_minutes = (event["service_restored"] - event["fault_detected"]).total_seconds() / 60
customers = event["affected_customers"]
cmi_manual = customers * manual_minutes
cmi_flisr = customers * flisr_time_minutes
cmi_saved = cmi_manual - cmi_flisr
print(f"Event: {customers:>4} customers, "
f"Manual: {manual_minutes:>6.0f} min, "
f"CMI saved: {cmi_saved:>10,.0f}")
What is CMI? Customer Minutes Interrupted is the total impact of an outage: number of affected customers multiplied by the duration in minutes. If 500 customers are out for 60 minutes, that's 30,000 CMI. Reducing CMI is the primary goal of FLISR because it directly improves SAIDI (the regulatory reliability metric).
outages["date"] = outages["fault_detected"].dt.date
daily_counts = outages.groupby("date").size()
storm_day = daily_counts.idxmax()
storm_events = outages[outages["date"] == storm_day]
print(f"Storm day: {storm_day}")
print(f"Outage events that day: {len(storm_events)}")
print(f"Total customers affected: {storm_events['affected_customers'].sum():,}")
total_cmi_manual = 0
total_cmi_flisr = 0
for _, event in storm_events.iterrows():
duration = (event["service_restored"] - event["fault_detected"]).total_seconds() / 60
customers = event["affected_customers"]
total_cmi_manual += customers * duration
total_cmi_flisr += customers * min(duration, flisr_time_minutes)
print(f"\nStorm day CMI (manual): {total_cmi_manual:>12,.0f}")
print(f"Storm day CMI (FLISR): {total_cmi_flisr:>12,.0f}")
print(f"CMI reduction: {total_cmi_manual - total_cmi_flisr:>12,.0f}")
print(f"Improvement: {((total_cmi_manual - total_cmi_flisr) / total_cmi_manual * 100):.1f}%")
- Built a graph representation of the SP&L distribution network
- Simulated a fault and identified affected customers
- Implemented a FLISR algorithm that isolates faults and restores service
- Calculated CMI savings from automated switching
- Replayed a real storm event and quantified the reliability improvement
Ideas to Try Next
- Optimize switching order: When multiple faults occur simultaneously, find the sequence that minimizes total CMI
- Add capacity constraints: Check that alternate feeders have enough capacity before closing tie switches
- Compare DER-assisted restoration: Model battery storage as backup sources during outages
- Calculate SAIDI/SAIFI impact: Aggregate CMI savings into annual reliability metrics from
outages/reliability_metrics.csv
Key Terms Glossary
- FLISR — Fault Location, Isolation, and Service Restoration; an automated outage response system
- Graph — nodes (buses) connected by edges (lines/switches); a natural model for electrical networks
- CMI — Customer Minutes Interrupted; the standard measure of outage severity
- SAIDI — System Average Interruption Duration Index; total CMI divided by total customers
- Tie switch — a normally-open switch that can connect two feeders for backup power
- Recloser — an automatic switch that can open on fault current and attempt to re-close
- NetworkX — a Python library for creating and analyzing graphs
Ready to Level Up?
In the advanced guide, you'll apply reinforcement learning for optimal switching and simulate microgrid islanding during emergencies.
Go to Advanced FLISR →