# If you use IPython/Jupyter:
import sys
from powfacpy.base.active_project import ActiveProject
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)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.
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("{:<50}".format("Using `get_obj`:"), end="")
%timeit act_prj.get_obj(r"Network Model\Network Data\Grid\*.ElmTerm")
print("{:<50}".format("Using `GetContents`:"), end="")
%timeit act_prj_obj.GetContents(r"Network Model\Network Data\Grid\*.ElmTerm")
try:
app.Hide()
print("{:<50}".format("Using `get_obj` (app is hidden):"), end="")
%timeit act_prj.get_obj(r"Network Model\Network Data\Grid\*.ElmTerm")
print("{:<50}".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`: 132 ms ± 15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetContents`: 67.8 ms ± 5.49 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_obj` (app is hidden): 2.8 ms ± 230 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetContents` (app is hidden): 1.43 ms ± 168 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
get_obj is approximately 50% slower than GetContents. However, the dominant performance factor is whether the PowerFactory application is visible or hidden.
We next compare get_calc_relevant_obj with GetCalcRelevantObjects.
print("{:<50}".format("Using `get_calc_relevant_obj`:"), end="")
%timeit act_prj.get_calc_relevant_obj(r"ElmTerm")
print("{:<50}".format("Using `GetCalcRelevantObject`:"), end="")
%timeit app.GetCalcRelevantObjects(r"ElmTerm")
try:
app.Hide()
print("{:<50}".format("Using `get_calc_relevant_obj` (app is hidden):"), end="")
%timeit act_prj.get_calc_relevant_obj(r"ElmTerm")
print("{:<50}".format("Using `GetCalcRelevantObject` (app is hidden):"), end="")
%timeit app.GetCalcRelevantObjects(r"ElmTerm")
finally:
app.Show()Using `get_calc_relevant_obj`: 80 ms ± 13.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetCalcRelevantObject`: 79.3 ms ± 4.56 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `get_calc_relevant_obj` (app is hidden): 2.53 ms ± 271 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `GetCalcRelevantObject` (app is hidden): 2.12 ms ± 164 μ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("{:<50}".format("Using `get_attr`:"), end="")
%timeit act_prj.get_attr(terminal, "uknom")
print("{:<50}".format("Using `get_attr` and path:"), end="")
%timeit act_prj.get_attr(r"Network Model\Network Data\Grid\Bus 01", "uknom")
print("{:<50}".format("Using `GetAttribute`:"), end="")
%timeit terminal.GetAttribute("uknom")
try:
app.Hide()
print("{:<50}".format("Using `get_attr` (app is hidden):"), end="")
%timeit act_prj.get_attr(terminal, "uknom")
print("{:<50}".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("{:<50}".format("Using `GetAttribute` (app is hidden):"), end="")
%timeit terminal.GetAttribute("uknom")
finally:
app.Show()Using `get_attr`: 5.09 μs ± 589 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` and path: 146 ms ± 26.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetAttribute`: 4.22 μs ± 275 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` (app is hidden): 4.41 μs ± 496 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Using `get_attr` and path (app is hidden): 2.48 ms ± 475 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `GetAttribute` (app is hidden): 3.84 μs ± 763 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.
print("{:<50}".format("Using `set_attr`:"), end="")
%timeit act_prj.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<50}".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("{:<50}".format("Using `SetAttribute`:"), end="")
%timeit terminal.SetAttribute("vmax", 1.06), terminal.SetAttribute("vmin", 0.01)
try:
app.Hide()
print("{:<50}".format("\nUsing `set_attr` (app is hidden):"), end="")
%timeit act_prj.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<50}".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("{:<50}".format("Using `SetAttribute` (app is hidden):"), end="")
%timeit terminal.SetAttribute("vmax", 1.06), terminal.SetAttribute("vmin", 0.01)
print("{:<50}".format("\nExecute `SetWriteCacheEnabled`:"), end="")
%timeit app.SetWriteCacheEnabled(1)
print("{:<50}".format("Using `set_attr` (app hidden, cache enabled):"), end="")
%timeit act_prj.set_attr(terminal, {"vmax": 1.06, "vmin": 0.01})
print("{:<50}".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("{:<50}".format("Using `SetAttribute` (app hidden, cache enabled):"), end="")
%timeit terminal.SetAttribute("vmax", 1.06), terminal.SetAttribute("vmin", 0.01)
print("{:<50}".format("Execute `WriteChangesToDb`:"), end="")
%timeit app.WriteChangesToDb()
app.SetWriteCacheEnabled(0)
finally:
app.Show()Using `set_attr`: 445 ms ± 66.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `set_attr` and path: 591 ms ± 111 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `SetAttribute`: 353 ms ± 40.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Using `set_attr` (app is hidden): 7.57 ms ± 1.22 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (app is hidden): 10.7 ms ± 1.07 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `SetAttribute` (app is hidden): 8.18 ms ± 820 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Execute `SetWriteCacheEnabled`: 354 ns ± 44.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
Using `set_attr` (app hidden, cache enabled): 2.7 ms ± 232 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `set_attr` and path (app hidden, cache enabled):5.4 ms ± 703 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Using `SetAttribute` (app hidden, cache enabled): 2.04 ms ± 247 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Execute `WriteChangesToDb`: 1.48 ms ± 116 μ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
Caching refers to storing previously computed or frequently accessed data in a temporary storage layer so that subsequent requests can be served more efficiently.
powfacpy provides several caching mechanisms. 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("{:<50}".format("Using `GetClassName`:"), end="")
%timeit terminal.GetClassName()
cache_method(terminal, "GetClassName")
print("{:<50}".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("{:<50}".format("Using `GetClassName` (app is hidden):"), end="")
%timeit terminal.GetClassName()
cache_method(terminal, "GetClassName")
print("{:<50}".format("Using `GetClassName` after caching (app is hidden):"), end="")
%timeit terminal.GetClassName()
finally:
app.Show()Using `GetClassName`: 82.1 ms ± 16.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `GetClassName` after caching: 149 ns ± 12.9 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Using `GetClassName` (app is hidden): 1.28 ms ± 115 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `GetClassName` after caching (app is hidden):133 ns ± 5.32 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.
Another optimization is to use ActiveProjectCached instead of ActiveProject. This class caches selected properties (internally using @cached_property). It should be used only when working with a single active PowerFactory project.
from powfacpy.base.active_project import ActiveProjectCached
act_prj_cached = ActiveProjectCached(app)
print("{:<50}".format("Using `study_cases_folder`:"), end="")
%timeit act_prj.study_cases_folder
print("{:<50}".format("Using `study_cases_folder` with cached properties:"), end="")
%timeit act_prj_cached.study_cases_folder
try:
app.Hide()
print("{:<50}".format("Using `study_cases_folder` (app is hidden):"), end="")
%timeit act_prj.study_cases_folder
print("{:<50}".format("Using `study_cases_folder` with cached properties (app is hidden):"), end="")
%timeit act_prj_cached.study_cases_folder
finally:
app.Show()Using `study_cases_folder`: 95.1 ms ± 10.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Using `study_cases_folder` with cached properties:60.1 ns ± 11 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Using `study_cases_folder` (app is hidden): 1.37 ms ± 180 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Using `study_cases_folder` with cached properties (app is hidden):65.7 ns ± 7.3 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
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.
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.
- Consider avoiding powfacpy wrapper overhead for performance critical code.
- Reuse object references instead of repeatedly resolving paths.
- Batch write operations and enable
SetWriteCacheEnabledwhen modifying larger data sets. - Use
ActiveProjectCachedwhen repeatedly accessing stable project properties. - Apply method caching only to invariant, argument-free methods.