Skip to content

Commit 4d1aac9

Browse files
Examples/contingency ipynb (#13)
* first pass on contingency analysis ipynb, added postprocessing and aux plot functions * subsample of 100 scenarios for faster run and smaller csv files * subsample of 100 scenarios for faster run and smaller csv files * visualization functions * smaller sample size * smaller sample size * moved vals to parameters and heuristic for vmax with mean of bin counts, ipynb cleanup * added the merge of both csv into the contingency notebook * Delete examples/data/contingency_texas/predictions_100_examples.csv too many examples * Added notebook and updated documentation * pre-commit checks --------- Co-authored-by: Celia Cintas <celia.cintas@ibm.com>
1 parent 7e83e22 commit 4d1aac9

31 files changed

+64845
-143
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repos:
1010
rev: v0.12.0
1111
hooks:
1212
- id: ruff-check
13+
args: ["--ignore=E501,E712,E203"]
1314
- id: ruff-format
1415
- repo: https://github.com/PyCQA/flake8
1516
rev: 7.2.0
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
👉 [Link](https://github.com/gridfm/gridfm-graphkit/tree/main/examples/notebooks) to the tutorial notebooks on Github
2+
3+
---
4+
Contingency analysis is a critical process in power system operations used to assess the impact of potential failures (e.g., line outages) on grid stability and reliability. It helps operators prepare for unexpected events by simulating scenarios such as N-1 or N-2 contingencies, where one or more components are removed from service. This analysis ensures that the grid can continue to operate within safe limits even under stressed conditions.
5+
6+
---
7+
8+
## Dataset Generation and Model Evaluation
9+
10+
The dataset used in this study originates from the Texas transmission grid, which includes approximately 2,000 nodes. Using the contingency mode of the `gridfm-datakit`, we simulated N-2 contingencies by removing up to two transmission lines at a time. For each scenario, we first solved the optimal power flow (OPF) problem to determine the generation dispatch. Then, we applied the contingency by removing lines and re-solved the power flow to observe the resulting grid state.
11+
12+
This process generated around 100,000 unique scenarios. Our model, **GridFMv0.1**, was fine-tuned on this dataset to predict power flow outcomes. For demonstration purposes, we selected a subsample of 10 scenarios. The `gridfm-datakit` also computed DC power flow results, enabling a comparison between GridFMv0.1 predictions and traditional DC power flow estimates, specifically in terms of line loading accuracy.
13+
14+
All predictions are benchmarked against the ground truth obtained from AC power flow simulations. Additionally, we analyze bus voltage violations, which GridFM can predict but are not captured by the DC solver, highlighting GridFM’s enhanced capabilities in modeling grid behavior.
15+
16+
17+
18+
```python
19+
from gridfm_graphkit.datasets.postprocessing import (
20+
compute_branch_currents_kA,
21+
compute_loading,
22+
)
23+
from gridfm_graphkit.datasets.postprocessing import create_admittance_matrix
24+
from gridfm_graphkit.utils.utils import compute_cm_metrics
25+
from gridfm_graphkit.utils.visualization import (
26+
plot_mass_correlation_density,
27+
plot_cm,
28+
plot_loading_predictions,
29+
plot_mass_correlation_density_voltage,
30+
)
31+
32+
import os
33+
from tqdm import tqdm
34+
import matplotlib.pyplot as plt
35+
from sklearn.metrics import f1_score
36+
import numpy as np
37+
import pandas as pd
38+
39+
import sys
40+
41+
if "google.colab" in sys.modules:
42+
try:
43+
!git clone https://github.com/gridfm/gridfm-graphkit.git
44+
%cd /content/gridfm-graphkit
45+
!pip install -e .
46+
!pip install -e .[dev,test]
47+
except Exception as e:
48+
49+
print(f"Failed to start Google Collab setup, due to {e}")
50+
```
51+
52+
## Load Data
53+
54+
We load both the ground truth and predicted values of the power flow solution. The predictions are generated using the `gridfm-graphkit` CLI:
55+
56+
```bash
57+
gridfm-graphkit predict ...
58+
```
59+
60+
We then merge the datasets using `scenario` and `bus` as keys, allowing us to align the predicted and actual values for each grid state and bus.
61+
62+
```python
63+
root_pred_folder = "../data/contingency_texas/"
64+
prediction_dir = "prediction_gridfm01"
65+
label_plot = "GridFM_v0.1 Fine-tuned"
66+
67+
pf_node_GT = pd.read_csv(os.path.join(root_pred_folder, "pf_node_10_examples.csv"))
68+
pg_node_predicted = pd.read_csv(
69+
os.path.join(root_pred_folder, "predictions_10_examples.csv")
70+
)
71+
72+
branch_idx_removed = pd.read_csv("{}branch_idx_removed.csv".format(root_pred_folder))
73+
edge_params = pd.read_csv("{}edge_params.csv".format(root_pred_folder))
74+
bus_params = pd.read_csv("{}bus_params.csv".format(root_pred_folder))
75+
76+
pf_node = pg_node_predicted.merge(pf_node_GT, on=["scenario", "bus"], how="left")
77+
```
78+
79+
## Create Admittance matrix
80+
81+
```python
82+
sn_mva = 100
83+
Yf, Yt, Vf_base_kV, Vt_base_kV = create_admittance_matrix(
84+
bus_params, edge_params, sn_mva
85+
)
86+
rate_a = edge_params["rate_a"]
87+
```
88+
89+
## Correct voltage predictions for GridFM and DC
90+
91+
```python
92+
pf_node["Vm_pred_corrected"] = pf_node["VM_pred"]
93+
pf_node["Va_pred_corrected"] = pf_node["VA_pred"]
94+
95+
pf_node.loc[pf_node.PV == 1, "Vm_pred_corrected"] = pf_node.loc[pf_node.PV == 1, "Vm"]
96+
pf_node.loc[pf_node.REF == 1, "Va_pred_corrected"] = pf_node.loc[pf_node.REF == 1, "Va"]
97+
98+
pf_node["Vm_dc_corrected"] = pf_node["Vm_dc"]
99+
pf_node["Va_dc_corrected"] = pf_node["Va_dc"]
100+
101+
pf_node.loc[pf_node.PV == 1, "Vm_dc_corrected"] = pf_node.loc[pf_node.PV == 1, "Vm"]
102+
pf_node.loc[pf_node.REF == 1, "Va_dc_corrected"] = pf_node.loc[pf_node.REF == 1, "Va"]
103+
```
104+
105+
## Compute branch current and line loading
106+
107+
```python
108+
loadings = []
109+
loadings_pred = []
110+
loadings_dc = []
111+
112+
for scenario_idx in tqdm(pf_node.scenario.unique()):
113+
pf_node_scenario = pf_node[pf_node.scenario == scenario_idx]
114+
branch_idx_removed_scenario = (
115+
branch_idx_removed[branch_idx_removed.scenario == scenario_idx]
116+
.iloc[:, 1:]
117+
.values
118+
)
119+
# remove nan
120+
branch_idx_removed_scenario = branch_idx_removed_scenario[
121+
~np.isnan(branch_idx_removed_scenario)
122+
].astype(np.int32)
123+
V_true = pf_node_scenario["Vm"].values * np.exp(
124+
1j * pf_node_scenario["Va"].values * np.pi / 180
125+
)
126+
V_pred = pf_node_scenario["Vm_pred_corrected"].values * np.exp(
127+
1j * pf_node_scenario["Va_pred_corrected"].values * np.pi / 180
128+
)
129+
V_dc = pf_node_scenario["Vm_dc_corrected"].values * np.exp(
130+
1j * pf_node_scenario["Va_dc_corrected"].values * np.pi / 180
131+
)
132+
If_true, It_true = compute_branch_currents_kA(
133+
Yf, Yt, V_true, Vf_base_kV, Vt_base_kV, sn_mva
134+
)
135+
If_pred, It_pred = compute_branch_currents_kA(
136+
Yf, Yt, V_pred, Vf_base_kV, Vt_base_kV, sn_mva
137+
)
138+
If_dc, It_dc = compute_branch_currents_kA(
139+
Yf, Yt, V_dc, Vf_base_kV, Vt_base_kV, sn_mva
140+
)
141+
142+
loading_true = compute_loading(If_true, It_true, Vf_base_kV, Vt_base_kV, rate_a)
143+
loading_pred = compute_loading(If_pred, It_pred, Vf_base_kV, Vt_base_kV, rate_a)
144+
loading_dc = compute_loading(If_dc, It_dc, Vf_base_kV, Vt_base_kV, rate_a)
145+
146+
# remove the branches that are removed from loading
147+
loading_true[branch_idx_removed_scenario] = -1
148+
loading_pred[branch_idx_removed_scenario] = -1
149+
loading_dc[branch_idx_removed_scenario] = -1
150+
151+
loadings.append(loading_true)
152+
loadings_pred.append(loading_pred)
153+
loadings_dc.append(loading_dc)
154+
155+
156+
loadings = np.array(loadings)
157+
loadings_pred = np.array(loadings_pred)
158+
loadings_dc = np.array(loadings_dc)
159+
removed_lines = loadings == -1
160+
removed_lines_pred = loadings_pred == -1
161+
removed_lines_dc = loadings_dc == -1
162+
163+
164+
# assert the same lines are removed
165+
assert (removed_lines == removed_lines_pred).all()
166+
assert (removed_lines == removed_lines_dc).all()
167+
168+
# assert the same number of lines are removed
169+
assert removed_lines.sum() == removed_lines_pred.sum()
170+
assert removed_lines.sum() == removed_lines_dc.sum()
171+
172+
overloadings = loadings[removed_lines == False] > 1.0
173+
overloadings_pred = loadings_pred[removed_lines == False] > 1.0
174+
overloadings_dc = loadings_dc[removed_lines == False] > 1.0
175+
```
176+
177+
## Histogram of true line loadings
178+
179+
```python
180+
plt.hist(loadings[removed_lines == False], bins=100)
181+
plt.xlabel("Line Loadings")
182+
plt.ylabel("Frequency")
183+
# log scale
184+
plt.savefig(f"loadings_histogram_{prediction_dir}.png")
185+
plt.show()
186+
```
187+
<p align="center">
188+
<img src="../figs/hist_true_loadings.png" alt="True loading"/>
189+
<br/>
190+
</p>
191+
192+
193+
## Predicted vs True line loading
194+
195+
```python
196+
# Valid lines
197+
valid_mask = removed_lines == False
198+
199+
# Extract valid values
200+
true_vals = loadings[valid_mask]
201+
gfm_vals = loadings_pred[valid_mask]
202+
dc_vals = loadings_dc[valid_mask]
203+
```
204+
205+
```python
206+
plot_mass_correlation_density(true_vals, gfm_vals, prediction_dir, label_plot)
207+
```
208+
<p align="center">
209+
<img src="../figs/loading_gridfm.png" alt="Loading gridfm"/>
210+
<br/>
211+
</p>
212+
213+
```python
214+
plot_mass_correlation_density(true_vals, dc_vals, "DC", "DC Solver")
215+
```
216+
<p align="center">
217+
<img src="../figs/loading_DC.png" alt="Loading DC"/>
218+
<br/>
219+
</p>
220+
221+
```python
222+
plot_cm(TN_gridfm, FP_gridfm, FN_gridfm, TP_gridfm, prediction_dir, label_plot)
223+
```
224+
<p align="center">
225+
<img src="../figs/confusion_gridfm.png" alt="Confusion gridfm"/>
226+
<br/>
227+
</p>
228+
229+
```python
230+
plot_cm(TN_dc, FP_dc, FN_dc, TP_dc, "DC", "DC Solver")
231+
```
232+
<p align="center">
233+
<img src="../figs/confusion_DC.png" alt="Confusion DC"/>
234+
<br/>
235+
</p>
236+
237+
238+
```python
239+
# Histograms of loadings
240+
plot_loading_predictions(
241+
loadings_pred[removed_lines == False],
242+
loadings_dc[removed_lines == False],
243+
loadings[removed_lines == False],
244+
prediction_dir,
245+
label_plot,
246+
)
247+
```
248+
<p align="center">
249+
<img src="../figs/loading_predictions.png" alt="Loading predictions"/>
250+
<br/>
251+
</p>
252+
253+
254+
```python
255+
# create df from loadings
256+
loadings_df = pd.DataFrame(loadings)
257+
loadings_df.columns = [f"branch_{i}" for i in range(loadings_df.shape[1])]
258+
259+
loadings_pred_df = pd.DataFrame(loadings_pred)
260+
loadings_pred_df.columns = [f"branch_{i}" for i in range(loadings_pred_df.shape[1])]
261+
262+
loadings_df["scenario"] = pf_node["scenario"].unique()
263+
loadings_pred_df["scenario"] = pf_node["scenario"].unique()
264+
265+
# make bar plot of wrongly classified loadings for different bins
266+
bins = np.arange(0, 2.2, 0.2)
267+
mse_pred = []
268+
mse_dc = []
269+
for i in range(len(bins) - 1):
270+
idx_in_bins = (loadings[removed_lines == False] > bins[i]) & (
271+
loadings[removed_lines == False] < bins[i + 1]
272+
)
273+
mse_pred.append(
274+
np.mean(
275+
(
276+
loadings_pred[removed_lines == False][idx_in_bins]
277+
- loadings[removed_lines == False][idx_in_bins]
278+
)
279+
** 2
280+
)
281+
)
282+
mse_dc.append(
283+
np.mean(
284+
(
285+
loadings_dc[removed_lines == False][idx_in_bins]
286+
- loadings[removed_lines == False][idx_in_bins]
287+
)
288+
** 2
289+
)
290+
)
291+
292+
293+
# labels
294+
labels = [f"{bins[i]:.1f}-{bins[i + 1]:.1f}" for i in range(len(bins) - 1)]
295+
plt.bar(labels, mse_pred, label=label_plot, alpha=0.5)
296+
plt.bar(labels, mse_dc, label="DC", alpha=0.5)
297+
plt.legend()
298+
plt.xlabel("Loadings")
299+
plt.ylabel("MSE")
300+
# y log scale
301+
plt.yscale("log")
302+
# rotate x labels
303+
plt.xticks(rotation=45)
304+
plt.savefig(f"loading_mse_{prediction_dir}.png")
305+
plt.show()
306+
```
307+
308+
<p align="center">
309+
<img src="../figs/MSE_loading.png" alt="MSE loading"/>
310+
<br/>
311+
</p>
312+
313+
314+
## Voltage violations
315+
316+
```python
317+
# merge bus_params["vmax"] and bus_params["vmin"] with pf_node on bus_idx
318+
pf_node = pd.merge(pf_node, bus_params[["bus", "vmax", "vmin"]], on="bus", how="left")
319+
320+
plot_mass_correlation_density_voltage(pf_node, prediction_dir, label_plot)
321+
```
322+
323+
<p align="center">
324+
<img src="../figs/correlation_voltage.png" alt="Correlation voltage"/>
325+
<br/>
326+
</p>

0 commit comments

Comments
 (0)