# If you use IPython/Jupyter:
import sys
from powfacpy.base.active_project import ActiveProject, ActiveProjectCached
sys.path.append(
r"C:\Program Files\DIgSILENT\PowerFactory 2025 SP3\Python\3.13"
) # Adjust the path if necessary.
# Get the PF app
import powerfactory
import powfacpy
from powfacpy.pf_class_protocols import PFApp
app: PFApp = powerfactory.GetApplication()
app.Show()
app.ActivateProject(
r"powfacpy\39_bus_new_england_copy_where_tests_run"
) # 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 140 Stepping 1, 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.1 ms ± 1.82 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_obj` (cached): 39.5 ms ± 4.73 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetContents`: 42.2 ms ± 8.94 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_obj` (app is hidden): 1.65 ms ± 224 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `get_obj` (cached, app is hidden): 887 μs ± 43.7 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetContents` (app is hidden): 836 μs ± 31.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`: 48.8 ms ± 8.99 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `get_calc_relevant_obj` (cached): 51.1 ms ± 6.87 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetCalcRelevantObject`: 59.6 ms ± 8.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_calc_relevant_obj` (app is hidden): 1.54 ms ± 227 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `get_calc_relevant_obj` (cached, app is hidden): 1.46 ms ± 313 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetCalcRelevantObject` (app is hidden): 1.21 ms ± 70.2 μs per loop (mean ± std. dev. of 7 runs, 1,000 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`: 3.57 μs ± 757 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` (cached): 4.07 μs ± 480 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` and path: 130 ms ± 9.84 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_attr` and path (cached): 58.9 ms ± 10.7 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetAttribute`: 3.34 μs ± 211 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` (app is hidden): 2.34 μs ± 98.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` (cached, app is hidden): 2.85 μs ± 260 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` and path (app is hidden): 1.61 ms ± 173 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `get_attr` and path (cached, app is hidden): 926 μs ± 96.3 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetAttribute` (app is hidden): 2.75 μs ± 436 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`: 230 ms ± 19.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `set_attr` (cached): 213 ms ± 38.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `set_attr` and path: 352 ms ± 26.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `set_attr` and path (cached): 289 ms ± 45.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `SetAttribute`: 238 ms ± 48.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `set_attr` (app is hidden): 7.87 ms ± 1.33 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` (cached, app is hidden): 8.26 ms ± 2.31 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (app is hidden): 10.1 ms ± 2.11 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (cached, app is hidden): 10.9 ms ± 1.83 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `SetAttribute` (app is hidden): 5.58 ms ± 1.78 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Execute `SetWriteCacheEnabled`: 301 ns ± 25.1 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
Using `set_attr` (app hidden, cache enabled): 1.52 ms ± 520 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `set_attr` (cached, app hidden, cache enabled): 1.35 ms ± 143 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `set_attr` and path (app hidden, cache enabled): 2.74 ms ± 68.6 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (cached, app hidden, cache enabled): 2.16 ms ± 181 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `SetAttribute` (app hidden, cache enabled): 1.3 ms ± 223 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Execute `WriteChangesToDb`: 1.26 ms ± 132 μ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`: 67.1 ms ± 6.46 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetClassName` after caching: 125 ns ± 22.2 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Using `GetClassName` (app is hidden): 562 μs ± 39 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetClassName` after caching (app is hidden): 62.9 ns ± 1.33 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 accessing 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.