← Back to All Guides
Guide 05

FLISR & Restoration Optimization with Graph Algorithms

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.

1

Build the Network Graph

import networkx as nx import pandas as pd import numpy as np import matplotlib.pyplot as plt import re # 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/" # Parse the lines.dss file to extract connectivity # Each line has: Bus1=xxx Bus2=yyy 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] }) # Build a NetworkX graph 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")
2

Add Switching Devices and Customer Counts

# Parse switches from switches.dss switches = pd.read_csv(DATA_DIR + "assets/switches.csv") # Mark edges that have switching devices 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"] # Add customer count to each bus (from loads file) 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"] # Mark substation bus as the source G.nodes["sourcebus"]["is_source"] = True print(f"Switches found: {len(switches)}")
3

Visualize the Network

# Load bus coordinates for layout 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} # Color nodes by customer count 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()
4

Simulate a Fault

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.

# Simulate a fault on a specific line fault_line = "f03_line_08" # Find the edge with this name 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]}") # Remove the faulted edge G_faulted = G.copy() G_faulted.remove_edge(*fault_edge) # Which buses can still reach the source? 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 # Count affected customers 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}")
5

Implement FLISR Logic

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.""" # Step 1: LOCATE — identify the faulted segment print(f"FAULT DETECTED on {fault_edge}") # Step 2: ISOLATE — find nearest upstream and downstream switches G_work = G.copy() G_work.remove_edge(*fault_edge) # Find switches adjacent to the fault zone isolation_switches = [] for u, v, data in G.edges(data=True): if data.get("has_switch"): # Check if this switch borders the de-energized zone 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]}") # Step 3: RESTORE — find alternate feed paths # Check if any tie switch can reconnect de-energized buses 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}")
6

Calculate CMI Savings

# Load real outage data for this feeder 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"]) # For a typical outage, compare manual vs FLISR restoration time # Manual: wait for crew to arrive and switch # FLISR: automated switching in ~60 seconds flisr_time_minutes = 1 # automated switching # Calculate average manual restoration time 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).

7

Replay a Storm Event

# Find a Major Event Day (MED) in the outage data 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():,}") # Calculate total CMI without FLISR vs with FLISR 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}%")

What You Built and Next Steps

  1. Built a graph representation of the SP&L distribution network
  2. Simulated a fault and identified affected customers
  3. Implemented a FLISR algorithm that isolates faults and restores service
  4. Calculated CMI savings from automated switching
  5. 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 →