PowerFactory load flow with Python, and the results in CSV or Excel
In PowerFactory's Python API a load flow is one call: app.GetFromStudyCase("ComLdf").Execute(), which returns 0 when it converged. The results are then attributes: m:u for a busbar's voltage, c:loading for a line's loading. This page runs it on the Nine-bus example and saves every busbar and branch to CSV and Excel.
What a load flow gives you
A load flow (power flow) solves the steady state of the network for one operating point: the voltage magnitude and angle at every busbar, and from them the active and reactive power, current and loading of every line, transformer and generator. It is the base of almost every other study: contingency analysis repeats it per outage, hosting capacity repeats it per injection, a PV curve repeats it per load level.
In PowerFactory the load flow is a command object, ComLdf, stored in the study case. The study case decides what it solves: which grids are active, which operation scenario and which variations apply. Python only activates the case, sets the command's options, runs it and reads the answers.
The method
- Connect.
powerfactory.GetApplicationExt()from your own Python (PowerFactory starts in the background), orpowerfactory.GetApplication()inside a Python command in PowerFactory. - Activate the project and the study case with
app.ActivateProject(name)and the case'sActivate(). - Get the load flow command with
app.GetFromStudyCase("ComLdf")and set the options of this run:iopt_net(0 balanced AC, 1 unbalanced, 2 DC),iopt_lim(reactive power limits),iopt_at(automatic taps). - Run it.
Execute()returns 0 when the load flow converged. Anything else means there is no valid result to read. - Read the results with
GetAttribute:m:u,m:Ulandm:phiuon terminals (ElmTerm),c:loading,m:P:bus1andc:Plosson lines (ElmLne), with:bushvor:buslvon transformers. - Save them as rows: the
csvmodule for CSV, pandas for Excel. Put the command's options back so the study case stays as it was.
The script
This runs as it is on the Nine-bus System, one of the example projects that come with PowerFactory 2024. Change PROJECT and STUDY_CASE for your own.
load_flow.py
"""Load flow in PowerFactory with Python: run it, read every busbar and branch, save CSV and Excel.
Runs on the "Nine-bus System" example project that comes with PowerFactory 2024.
"""
import csv
import sys
sys.path.append(r"C:\Program Files\DIgSILENT\PowerFactory 2024\Python\3.12")
import powerfactory
PROJECT = "Nine-bus System"
STUDY_CASE = "01- Load Flow"
app = powerfactory.GetApplicationExt() # inside PowerFactory: powerfactory.GetApplication()
app.ActivateProject(PROJECT)
app.GetProjectFolder("study").GetContents(STUDY_CASE + ".IntCase")[0].Activate()
# The study case's load flow command, with the options for this run (put back at the end).
ldf = app.GetFromStudyCase("ComLdf")
saved = {a: ldf.GetAttribute(a) for a in ("iopt_net", "iopt_lim", "iopt_at")}
ldf.SetAttribute("iopt_net", 0) # 0 AC balanced, 1 AC unbalanced (3-phase), 2 DC
ldf.SetAttribute("iopt_lim", 1) # 1 keeps generators within their reactive power limits
ldf.SetAttribute("iopt_at", 0) # 1 lets transformer taps move automatically
try:
if ldf.Execute() != 0:
raise SystemExit("The load flow did not converge")
buses = [{"Busbar": b.loc_name, "Nominal (kV)": round(b.uknom, 2),
"Voltage (p.u.)": b.GetAttribute("m:u"), "Voltage (kV)": b.GetAttribute("m:Ul"),
"Angle (deg)": b.GetAttribute("m:phiu")}
for b in app.GetCalcRelevantObjects("*.ElmTerm")]
branches = []
for o in app.GetCalcRelevantObjects("*.ElmLne") + app.GetCalcRelevantObjects("*.ElmTr2"):
end = "bus1" if o.GetClassName() == "ElmLne" else "bushv" # the end P and Q are read at
branches.append({"Branch": o.loc_name, "Class": o.GetClassName(),
"Loading (%)": o.GetAttribute("c:loading"),
"P (MW)": o.GetAttribute(f"m:P:{end}"), "Q (Mvar)": o.GetAttribute(f"m:Q:{end}"),
"Losses (MW)": o.GetAttribute("c:Ploss")})
finally:
for a, v in saved.items(): # leave the command as it was
ldf.SetAttribute(a, v)
def save_csv(name, rows):
with open(name, "w", newline="") as f:
out = csv.DictWriter(f, fieldnames=list(rows[0]))
out.writeheader()
out.writerows(rows)
save_csv("load_flow_buses.csv", buses)
save_csv("load_flow_branches.csv", branches)
# Excel, one sheet per table: needs pandas and openpyxl (pip install pandas openpyxl).
try:
import pandas as pd
with pd.ExcelWriter("load_flow_results.xlsx") as xl:
pd.DataFrame(buses).to_excel(xl, sheet_name="Busbars", index=False)
pd.DataFrame(branches).to_excel(xl, sheet_name="Branches", index=False)
except ImportError:
print("pandas is not installed: the CSV files are saved, the Excel file is not")
print(f"Saved {len(buses)} busbars and {len(branches)} branches")
Result on the Nine-bus example
The whole script, files included, runs in 0.9 s. The most loaded branch is T2 at 79.6 %, and the lowest voltage is 0.9956 p.u. at Bus 5, the busbar of the largest load.
- Line (ElmLne)
- Transformer (ElmTr2)
| Busbar | Nominal kV | Voltage p.u. | Voltage kV | Angle deg |
|---|---|---|---|---|
| Bus 1 | 16.5 | 1.0400 | 17.16 | 0.00 |
| Bus 2 | 18.0 | 1.0250 | 18.45 | 9.28 |
| Bus 3 | 13.8 | 1.0250 | 14.14 | 4.66 |
| Bus 4 | 230.0 | 1.0258 | 235.93 | 147.78 |
| Bus 5 | 230.0 | 0.9956 | 229.00 | 146.01 |
| Bus 6 | 230.0 | 1.0127 | 232.91 | 146.31 |
| Bus 7 | 230.0 | 1.0258 | 235.93 | 153.72 |
| Bus 8 | 230.0 | 1.0159 | 233.65 | 150.73 |
| Bus 9 | 230.0 | 1.0324 | 237.44 | 151.97 |
| Branch | Class | Loading % | P MW | Q Mvar | Losses MW |
|---|---|---|---|---|---|
| Line 4-5 | ElmLne | 14.2 | -40.7 | -38.7 | 0.26 |
| Line 4-6 | ElmLne | 8.6 | -30.5 | -16.5 | 0.17 |
| Line 5-7 | ElmLne | 21.4 | -84.3 | -11.3 | 2.30 |
| Line 6-9 | ElmLne | 15.4 | 60.8 | -18.1 | 1.35 |
| Line 7-8 | ElmLne | 18.9 | 76.4 | -0.8 | 0.48 |
| Line 8-9 | ElmLne | 8.5 | -24.1 | -24.3 | 0.09 |
| T1 | ElmTr2 | 29.5 | -71.6 | -23.9 | 0.00 |
| T2 | ElmTr2 | 79.6 | -163.0 | 9.2 | -0.00 |
| T3 | ElmTr2 | 55.7 | -85.0 | 15.0 | 0.00 |
P and Q are read at terminal i of each line (:bus1) and at the HV side of each transformer (:bushv); a negative value flows into the branch from the other end. The script also wrote load_flow_buses.csv, load_flow_branches.csv and load_flow_results.xlsx.
Open this in the Builder. The Load flow on every study case Quick Start loads in the PowerFactory Workflow Builder, ready for your project. Change any block and download the Python file.
Open in the BuilderThe full Python script from the Builder
The Builder writes the same study for every study case of a project, with one row per case: its lowest and highest busbar voltage and where they are. On the Nine-bus example its 8 study cases give 8 rows; the one case with no grid in it keeps its row with the reason the load flow did not converge. This is the file it writes for the example (only PROJECT filled in):
load_flow_on_every_study_case.py as the Builder writes it: the settings and the workflow⬇ Download the full file (.py, 1,628 lines)
#!/usr/bin/env python3
# Load flow on every study case
"""Made with the PowerFactory Workflow Builder, https://tool.adjiebrotots.com/powerfactory-scripter/builder/
BEFORE YOU RUN
1. Check the SETTINGS below (lines marked REQUIRED).
2. Close PowerFactory, or run this file from inside PowerFactory (see RUN_MODE).
3. Run it with the Python that matches your PowerFactory, e.g. py -3.12 load_flow_on_every_study_case.py
Results and run.log go to a new folder next to this file (or in OUTPUT_DIR).
"""
# ==== IMPORTS ====
import csv
import difflib
import fnmatch
import glob
import importlib.util
import math
import os
import re
import subprocess
import sys
import time
import traceback
# ==== SETTINGS (edit here) ====
# The name of this workflow: used for the run folder and in the log.
WORKFLOW_NAME = "Load flow on every study case"
# REQUIRED. The PowerFactory project to open, exactly as the Data Manager shows it (letter case
# counts).
PROJECT = "Nine-bus System"
# OPTIONAL. The study case to activate first ("" = keep the active one).
STUDY_CASE = ""
# OPTIONAL. Where each run writes its folder of results and run.log ("" = the folder of this
# script).
OUTPUT_DIR = ""
# OPTIONAL. True: put every value the workflow changed back when it ends.
UNDO_CHANGES = True
# OPTIONAL. How the results tables are saved in the run folder when the run ends: "csv", "xlsx"
# (Excel, needs pandas and openpyxl) or "both".
TABLE_FORMAT = "csv"
# OPTIONAL. Nominal frequency in Hz for the frequency checks: 50, 60 or "auto" (read from the
# grid).
F_NOMINAL = "auto"
# OPTIONAL. "auto", "engine" (start PowerFactory in the background) or "inside" (run from inside
# PowerFactory).
RUN_MODE = "auto"
# OPTIONAL. True: show the PowerFactory window while running (engine mode only).
SHOW_PF = False
# OPTIONAL. True: no screen updates and fewer output window messages while running.
FAST_MODE = True
# OPTIONAL. Seconds to wait for PowerFactory to start.
CONNECT_TIMEOUT = 120
# OPTIONAL. The folder with the powerfactory Python module ("" = find it).
PF_API_PATH = ""
# OPTIONAL. Log in to PowerFactory as this user ("" = your own).
PF_USER = ""
# OPTIONAL. Study case: Study cases named.
STUDY_CASE_PATTERN = "*"
# True: connect, find every element named in this workflow, say what was found, then stop
# without running anything.
CHECK_ONLY = False
# ==== WORKFLOW ====
# Note: This runs the load flow of every study case of the project (or of the cases named on For each study case, for example 0*) and reads the voltage of every busbar in service. Each case gets a row with...
def main():
preflight([])
app = pf_connect()
try:
if CHECK_ONLY:
check_elements(app, [("*.ElmTerm", "all")])
return
# -- Step 1: Start (Project: Nine-bus System) --
# -- Step 2: Study case (Every study case) --
study_case_cases = case_list(app, STUDY_CASE_PATTERN)
if not study_case_cases:
log("Study case: no study case to loop over.", "warn")
for study_case_pass_no, study_case in enumerate(each_case(app, study_case_cases), 1):
ctx_push("Study case", study_case, study_case_pass_no, len(study_case_cases))
try:
# -- Step 3: Run load flow (Settings of the study case) --
ldf = run_load_flow(app, "case", "case", "case")
# Did not converge: skips to the next pass of Study case
if not ldf.ok:
case_skipped("Load flow did not converge")
continue
# -- Step 4: Busbar voltage (m:u of all * (ElmTerm) (load flow results)) --
busbar_voltage = pf_get_all(pf_find(app, "*.ElmTerm", "all"), "m:u", None, False, True)
# -- Step 5: Add row to results table (Into "Load flow", saved at the end automatically) --
table("Load flow").add({
"Lowest voltage (p.u.)": busbar_voltage.min,
"Lowest at": busbar_voltage.at_min,
"Highest voltage (p.u.)": busbar_voltage.max,
"Highest at": busbar_voltage.at_max,
}, status=True)
except StopWorkflow:
raise
except Exception as err:
case_failed(err)
finally:
ctx_pop(["Load flow"])
except StopWorkflow as stop:
log(f"Stopped: {stop}")
finally:
finish(app)
The file goes on with 1,519 lines of helper functions copied from the Builder's runtime (connecting, logging, tables, graphs, putting values back), only those this workflow needs. The download has them all.
How the script works
- SETTINGS at the top are the only lines to edit: the project, the study case to start from, where the run folder goes, and
TABLE_FORMAT("csv","xlsx"or"both"). - Step 2 lists the study cases that match
STUDY_CASE_PATTERNand activates each in turn (each_case). - Step 3,
run_load_flow, runs the case's ownComLdf. When it does not converge,case_skippedkeeps the row with the reason and the loop goes on to the next case. - Step 4 reads
m:uof every busbar in service and energised, and keeps the lowest and highest with where they are. - Step 5 adds the row to the "Load flow" table, saved at the end as CSV or Excel in a new run folder, next to a
run.log. finish(app)puts back every value the workflow changed and the study case it started from.
Pitfalls
- Check
Execute()before reading. 0 means converged. After a failed load flow them:andc:values are not a solution, yetGetAttributestill returns numbers. - Branch results need the end.
m:Pon a line raisesAttributeError: writem:P:bus1orm:P:bus2, andm:P:bushvorm:P:buslvon a 2-winding transformer.c:loadinghas no end. - Angles include transformer phase shifts. Bus 4 of the Nine-bus example reads 147.8 deg because generator transformer T1 shifts the angle by 150 deg. Compare angles on the same side of a transformer.
- Changed options are saved in the project.
SetAttributeonComLdfchanges the study case for good, so the script keeps the old values and puts them back infinally. GetFromStudyCasemakes what is missing. If the study case has noComLdf, it silently creates one with default options. Checkcase.GetContents("*.ComLdf")when the options matter.- Excel needs pandas and openpyxl in the Python that runs the script. The CSV files need nothing beyond the standard library.
Questions
How do I run a load flow in PowerFactory with Python?
Activate the study case, then call app.GetFromStudyCase("ComLdf").Execute(). It returns 0 when the load flow converged.
How do I read busbar voltages after the load flow?
Loop over app.GetCalcRelevantObjects("*.ElmTerm") and read GetAttribute("m:u") for per unit, "m:Ul" for line-line kV and "m:phiu" for the angle in degrees.
How do I export PowerFactory load flow results to Excel?
Read the values into a list of dictionaries, then write it with pandas.DataFrame(rows).to_excel("results.xlsx") (pandas and openpyxl installed), or with the csv module for CSV.
What does iopt_net mean on ComLdf?
The network representation of the load flow: 0 balanced AC (positive sequence), 1 unbalanced three-phase AC, 2 DC load flow.
Related
- Automate PowerFactory studies with Python: every guide
- PowerFactory Python API: getting started
- N-1 contingency analysis
- Short-circuit automation
- Hosting capacity study
- Parameter sweep: PV and QV curves
- RMS and EMT simulation batch runs
Attributes used on this page: ElmTerm, ElmLne, ElmTr2, ElmSym, ElmLod. All attributes.