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

  1. Connect. powerfactory.GetApplicationExt() from your own Python (PowerFactory starts in the background), or powerfactory.GetApplication() inside a Python command in PowerFactory.
  2. Activate the project and the study case with app.ActivateProject(name) and the case's Activate().
  3. 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).
  4. Run it. Execute() returns 0 when the load flow converged. Anything else means there is no valid result to read.
  5. Read the results with GetAttribute: m:u, m:Ul and m:phiu on terminals (ElmTerm), c:loading, m:P:bus1 and c:Ploss on lines (ElmLne), with :bushv or :buslv on transformers.
  6. Save them as rows: the csv module 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.

Loading of every line and transformer in the Nine-bus example after the load flow020406080100Line 4-5: 14.2 %Line 4-5Line 4-6: 8.61 %Line 4-6Line 5-7: 21.4 %Line 5-7Line 6-9: 15.4 %Line 6-9Line 7-8: 18.9 %Line 7-8Line 8-9: 8.46 %Line 8-9T1: 29.5 %T1T2: 79.6 %T2T3: 55.7 %T3Loading (%)
  • Line (ElmLne)
  • Transformer (ElmTr2)
Read with c:loading after the load flow.
BusbarNominal kVVoltage p.u.Voltage kVAngle deg
Bus 116.51.040017.160.00
Bus 218.01.025018.459.28
Bus 313.81.025014.144.66
Bus 4230.01.0258235.93147.78
Bus 5230.00.9956229.00146.01
Bus 6230.01.0127232.91146.31
Bus 7230.01.0258235.93153.72
Bus 8230.01.0159233.65150.73
Bus 9230.01.0324237.44151.97
BranchClassLoading %P MWQ MvarLosses MW
Line 4-5ElmLne14.2-40.7-38.70.26
Line 4-6ElmLne8.6-30.5-16.50.17
Line 5-7ElmLne21.4-84.3-11.32.30
Line 6-9ElmLne15.460.8-18.11.35
Line 7-8ElmLne18.976.4-0.80.48
Line 8-9ElmLne8.5-24.1-24.30.09
T1ElmTr229.5-71.6-23.90.00
T2ElmTr279.6-163.09.2-0.00
T3ElmTr255.7-85.015.00.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 Builder

The 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_PATTERN and activates each in turn (each_case).
  • Step 3, run_load_flow, runs the case's own ComLdf. When it does not converge, case_skipped keeps the row with the reason and the loop goes on to the next case.
  • Step 4 reads m:u of 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 the m: and c: values are not a solution, yet GetAttribute still returns numbers.
  • Branch results need the end. m:P on a line raises AttributeError: write m:P:bus1 or m:P:bus2, and m:P:bushv or m:P:buslv on a 2-winding transformer. c:loading has 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. SetAttribute on ComLdf changes the study case for good, so the script keeps the old values and puts them back in finally.
  • GetFromStudyCase makes what is missing. If the study case has no ComLdf, it silently creates one with default options. Check case.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

Attributes used on this page: ElmTerm, ElmLne, ElmTr2, ElmSym, ElmLod. All attributes.