# If you use IPython/Jupyter:
import sys
import json
with open(r"..\..\settings_local.json") as settings_file:
SETTINGS = json.load(settings_file)
sys.path.append(
SETTINGS["local path to PowerFactory application"] # e.g. r"C:\Program Files\DIgSILENT\PowerFactory 2025 SP5\Python\3.13"
)
# Get the PF app
import powerfactory
from powfacpy.base.active_project import ActiveProject, ActiveProjectCached
from powfacpy.pf_classes.protocols import PFApp
app: PFApp = powerfactory.GetApplication()
app.Show()
app.ActivateProject(
r"powfacpy\39_bus_new_england_copy_to_run_tests"
) # Adjust the project path if required.
act_prj = ActiveProject(app)
act_prj_cached = ActiveProjectCached(app)A Word on Performance
This tutorial evaluates the performance characteristics of the Python API of PowerFactory. The powfacpy wrapper is benchmarked against the native API. Moreover, the performance improvements achieved through the caching mechanisms provided by powfacpy are demonstrated.
We begin by activating the IEEE 39-bus system project.
The following platform is used:
import platform
uname = platform.uname()
print(f"System: {uname.system}")
print(f"Release: {uname.release}")
print(f"Version: {uname.version}")
print(f"Machine: {uname.machine}")
print(f"Processor: {uname.processor}")System: Windows
Release: 11
Version: 10.0.26200
Machine: AMD64
Processor: Intel64 Family 6 Model 181 Stepping 0, GenuineIntel
1 Accessing Objects
We compare the performance of get_obj from powfacpy with the native GetContents method of PowerFactory. Internally, get_obj calls GetContents and augments it with additional functionality such as argument validation and improved exception handling. A moderate overhead is therefore expected.
To improve the performance of powfacpy, one optimization is to use ActiveProjectCached instead of ActiveProject as shown in the examples below. This class caches selected properties (internally using @cached_property). It should be used only when working with a single active PowerFactory project.
Note that any powfacpy application can be initialized with the ActiveProjectCached class. For example, use pfds = DynamicSimulation(cached=True) and the pfds.act_prj attribute will be an instance of the class ActiveProjectCached instead of ActiveProject.
The methods are benchmarked both with the PowerFactory application visible and hidden (non-interactive engine mode). It is good practice to use a try statement to ensure that the application is restored to a visible state even if an exception occurs during execution.
act_prj_obj = app.GetActiveProject()
print("{:<70}".format("Using `get_obj`:"), end="")
%timeit act_prj.get_obj(r"Network Model\Network Data\Grid\*.ElmTerm")
print("{:<70}".format("Using `get_obj` (cached):"), end="")
%timeit act_prj_cached.get_obj(r"Network Model\Network Data\Grid\*.ElmTerm")
print("{:<70}".format("Using `GetContents`:"), end="")
%timeit act_prj_obj.GetContents(r"Network Model\Network Data\Grid\*.ElmTerm")
try:
app.Hide()
print("{:<70}".format("Using `get_obj` (app is hidden):"), end="")
%timeit act_prj.get_obj(r"Network Model\Network Data\Grid\*.ElmTerm")
print("{:<70}".format("Using `get_obj` (cached, app is hidden):"), end="")
%timeit act_prj_cached.get_obj(r"Network Model\Network Data\Grid\*.ElmTerm")
print("{:<70}".format("Using `GetContents` (app is hidden):"), end="")
%timeit act_prj_obj.GetContents(r"Network Model\Network Data\Grid\*.ElmTerm")
finally:
app.Show()Using `get_obj`: 59.8 ms ± 2.94 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_obj` (cached): 28.7 ms ± 549 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetContents`: 28.1 ms ± 1.35 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_obj` (app is hidden): 1.32 ms ± 64.8 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `get_obj` (cached, app is hidden): 712 μs ± 27 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetContents` (app is hidden): 720 μs ± 28.3 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
get_obj is approximately 50% slower than GetContents, but using ActiveProjectCached can mitigate this. However, the dominant performance factor is whether the PowerFactory application is visible or hidden.
We next compare get_calc_relevant_obj with GetCalcRelevantObjects.
print("{:<70}".format("Using `get_calc_relevant_obj`:"), end="")
%timeit act_prj.get_calc_relevant_obj(r"ElmTerm")
print("{:<70}".format("Using `get_calc_relevant_obj` (cached):"), end="")
%timeit act_prj_cached.get_calc_relevant_obj(r"ElmTerm")
print("{:<70}".format("Using `GetCalcRelevantObject`:"), end="")
%timeit app.GetCalcRelevantObjects(r"ElmTerm")
try:
app.Hide()
print("{:<70}".format("Using `get_calc_relevant_obj` (app is hidden):"), end="")
%timeit act_prj.get_calc_relevant_obj(r"ElmTerm")
print("{:<70}".format("Using `get_calc_relevant_obj` (cached, app is hidden):"), end="")
%timeit act_prj_cached.get_calc_relevant_obj(r"ElmTerm")
print("{:<70}".format("Using `GetCalcRelevantObject` (app is hidden):"), end="")
%timeit app.GetCalcRelevantObjects(r"ElmTerm")
finally:
app.Show()Using `get_calc_relevant_obj`: 31.7 ms ± 795 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_calc_relevant_obj` (cached): 31.7 ms ± 612 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetCalcRelevantObject`: 28.7 ms ± 707 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_calc_relevant_obj` (app is hidden): 1.45 ms ± 84.2 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `get_calc_relevant_obj` (cached, app is hidden): 1.77 ms ± 295 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetCalcRelevantObject` (app is hidden): 3.4 ms ± 262 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Here, the difference between the wrapper and the native method is small. Again, the application state has a substantially larger impact than the method choice.
2 Getting Attributes
We now compare attribute access using get_attr with the native GetAttribute method. The wrapper is evaluated in two modes: passing the object directly and passing its path.
terminal = act_prj.get_unique_obj(r"Network Model\Network Data\Grid\Bus 01")
print("{:<70}".format("Using `get_attr`:"), end="")
%timeit act_prj.get_attr(terminal, "uknom")
print("{:<70}".format("Using `get_attr` (cached):"), end="")
%timeit act_prj_cached.get_attr(terminal, "uknom")
print("{:<70}".format("Using `get_attr` and path:"), end="")
%timeit act_prj.get_attr(r"Network Model\Network Data\Grid\Bus 01", "uknom")
print("{:<70}".format("Using `get_attr` and path (cached):"), end="")
%timeit act_prj_cached.get_attr(r"Network Model\Network Data\Grid\Bus 01", "uknom")
print("{:<70}".format("Using `GetAttribute`:"), end="")
%timeit terminal.GetAttribute("uknom")
try:
app.Hide()
print("{:<70}".format("Using `get_attr` (app is hidden):"), end="")
%timeit act_prj.get_attr(terminal, "uknom")
print("{:<70}".format("Using `get_attr` (cached, app is hidden):"), end="")
%timeit act_prj_cached.get_attr(terminal, "uknom")
print("{:<70}".format("Using `get_attr` and path (app is hidden):"), end="")
%timeit act_prj.get_attr(r"Network Model\Network Data\Grid\Bus 01", "uknom")
print("{:<70}".format("Using `get_attr` and path (cached, app is hidden):"), end="")
%timeit act_prj_cached.get_attr(r"Network Model\Network Data\Grid\Bus 01", "uknom")
print("{:<70}".format("Using `GetAttribute` (app is hidden):"), end="")
%timeit terminal.GetAttribute("uknom")
finally:
app.Show()Using `get_attr`: 7.32 μs ± 1.4 μs per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` (cached): 5.69 μs ± 455 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` and path: 109 ms ± 17.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_attr` and path (cached): 51.4 ms ± 4.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetAttribute`: 6.08 μs ± 334 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` (app is hidden): 6.71 μs ± 637 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` (cached, app is hidden): 6.13 μs ± 207 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` and path (app is hidden): 1.45 ms ± 60.7 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `get_attr` and path (cached, app is hidden): 747 μs ± 43.9 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetAttribute` (app is hidden): 5.86 μs ± 209 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
get_attr performs similarly to GetAttribute, except when a path must first be resolved to an object, which introduces additional overhead. Notably, the visibility of the application has only a minor influence on pure read operations.
3 Setting Attributes
We now evaluate write performance using set_attr versus SetAttribute. In addition to hiding the application, we examine the effect of enabling the write cache via SetWriteCacheEnabled.
terminal = act_prj_cached.get_unique_obj(r"Network Model\Network Data\Grid\Bus 01")
print("{:<70}".format("Using `set_attr`:"), end="")
%timeit act_prj.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` (cached):"), end="")
%timeit act_prj_cached.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` and path: "), end="")
%timeit act_prj.set_attr(r"Network Model\Network Data\Grid\Bus 01", {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` and path (cached): "), end="")
%timeit act_prj_cached.set_attr(r"Network Model\Network Data\Grid\Bus 01", {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `SetAttribute`:"), end="")
%timeit terminal.SetAttribute("vmax", 1.06), terminal.SetAttribute("vmin", 0.01)
try:
app.Hide()
print("{:<70}".format("\nUsing `set_attr` (app is hidden):"), end="")
%timeit act_prj.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` (cached, app is hidden):"), end="")
%timeit act_prj_cached.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` and path (app is hidden):"), end="")
%timeit act_prj.set_attr(r"Network Model\Network Data\Grid\Bus 01", {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` and path (cached, app is hidden):"), end="")
%timeit act_prj_cached.set_attr(r"Network Model\Network Data\Grid\Bus 01", {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `SetAttribute` (app is hidden):"), end="")
%timeit terminal.SetAttribute("vmax", 1.06), terminal.SetAttribute("vmin", 0.01)
print("{:<70}".format("\nExecute `SetWriteCacheEnabled`:"), end="")
%timeit app.SetWriteCacheEnabled(1)
print("{:<70}".format("Using `set_attr` (app hidden, cache enabled):"), end="")
%timeit act_prj.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` (cached, app hidden, cache enabled):"), end="")
%timeit act_prj_cached.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` and path (app hidden, cache enabled):"), end="")
%timeit act_prj.set_attr(r"Network Model\Network Data\Grid\Bus 01", {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `set_attr` and path (cached, app hidden, cache enabled):"), end="")
%timeit act_prj_cached.set_attr(r"Network Model\Network Data\Grid\Bus 01", {"vmax": 1.06, "vmin": 0.01})
print("{:<70}".format("Using `SetAttribute` (app hidden, cache enabled):"), end="")
%timeit terminal.SetAttribute("vmax", 1.06), terminal.SetAttribute("vmin", 0.01)
print("{:<70}".format("Execute `WriteChangesToDb`:"), end="")
%timeit app.WriteChangesToDb()
app.SetWriteCacheEnabled(0)
finally:
app.Show()Using `set_attr`: 55.8 ms ± 2.07 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `set_attr` (cached): 55.1 ms ± 1.87 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `set_attr` and path: 105 ms ± 2.57 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `set_attr` and path (cached): 101 ms ± 26.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `SetAttribute`: 83.4 ms ± 5.03 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `set_attr` (app is hidden): 6.71 ms ± 1.35 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` (cached, app is hidden): 6.22 ms ± 576 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (app is hidden): 7.43 ms ± 260 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (cached, app is hidden): 7.84 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `SetAttribute` (app is hidden): 5.56 ms ± 226 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Execute `SetWriteCacheEnabled`: 286 ns ± 19.3 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
Using `set_attr` (app hidden, cache enabled): 1.17 ms ± 35.3 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `set_attr` (cached, app hidden, cache enabled): 1.23 ms ± 81.9 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `set_attr` and path (app hidden, cache enabled): 2.56 ms ± 132 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (cached, app hidden, cache enabled): 1.98 ms ± 56.7 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `SetAttribute` (app hidden, cache enabled): 1.17 ms ± 36.9 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Execute `WriteChangesToDb`: 634 μs ± 21.8 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Write operations are the most computationally expensive. The choice between wrapper and native method has limited influence, whereas the application state again plays a major role. Enabling the write cache reduces execution time by roughly 50%. However, the overhead of calling SetWriteCacheEnabled and WriteChangesToDb makes this strategy most beneficial when writing larger batches of data.
4 Caching Return Values
Caching refers to storing previously computed or frequently accessed data in a temporary storage layer so that subsequent requests can be served more efficiently. For example, the return value of a method without input arguments can be cached. This is appropriate only if the return value is invariant.
from powfacpy.applications.caching import cache_method
terminal = act_prj.get_unique_obj(r"Network Model\Network Data\Grid\Bus 01")
print("{:<70}".format("Using `GetClassName`:"), end="")
%timeit terminal.GetClassName()
cache_method(terminal, "GetClassName")
print("{:<70}".format("Using `GetClassName` after caching:"), end="")
%timeit terminal.GetClassName()
try:
app.Hide()
terminal = act_prj.get_unique_obj(r"Network Model\Network Data\Grid\Bus 01")
print("{:<70}".format("Using `GetClassName` (app is hidden):"), end="")
%timeit terminal.GetClassName()
cache_method(terminal, "GetClassName")
print("{:<70}".format("Using `GetClassName` after caching (app is hidden):"), end="")
%timeit terminal.GetClassName()
finally:
app.Show()Using `GetClassName`: 34.5 ms ± 5.97 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetClassName` after caching: 109 ns ± 1.81 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Using `GetClassName` (app is hidden): 599 μs ± 17.1 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetClassName` after caching (app is hidden): 106 ns ± 2.97 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Caching substantially reduces execution time. This approach improves readability compared to manually storing return values in intermediate variables and reusing them.
5 Further Reported Performance Improvements
This is a collection of potential performance improvements reported by users that cannot be reliably benchmarked or reproduced, since the internal behavior of PowerFactory is not transparent:
- Deactivating and reactivating (large) projects
6 Conclusion
The benchmarks suggest the following practical measures to improve performance:
- Hide the PowerFactory application during computationally intensive operations.
- Use
ActiveProjectCachedto efficiently access stable project properties. - Reuse object references instead of repeatedly resolving paths.
- Batch write operations and enable
SetWriteCacheEnabledwhen modifying larger data sets. - Apply method caching to invariant, argument-free methods.