Subsystems

This tutorial covers the grouping class of powfacpy named SubSystem which extends the functionality of the PowerFactory grouping classes (Zone, Area, Boundary). The SubSystem class is designed to represent coherent groups of elements in the power system and provides methods for analyzing and manipulating these groups. The SubSystem class inherits from the Zone class described in the Topology and Groupings tutorial. It became necessary to define the SubSystem class in powfacpy because the functionality goes way beyond the native PowerFactory classes. Furthermore, there is a class for a group of Subsystems named SubSystemContainer which provides methods to analyze and manipulate groups of SubSystems.

Examples for the functionality of SubSystem include (and will be extended in future):

Examples for the functionality of SubSystemContainer include (and will be extended in future):

We first activate the IEEE 39 bus system project and create a study case for this tutorial. All existing grouping onjects are cleared.

# 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
app = powerfactory.GetApplication()
from powfacpy.base.active_project import ActiveProject

app.Show()
app.ActivateProject(
    r"powfacpy\39_bus_new_england_copy_to_run_tests"
)  # You may change the project path.
act_prj = ActiveProject(app)

study_case = act_prj.copy_single_obj("Study Cases\\2.3 Simulation Fault Bus 31 Stable", "Study Cases", new_name="SubSystems tutorial")
study_case.Activate()
act_prj.clear_folder(act_prj.zones_folder)
act_prj.clear_folder(act_prj.boundaries_folder)
act_prj.clear_folder(act_prj.areas_folder)
act_prj.create_variation("SubSystems tutorial")
<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Variations.IntPrjfolder\SubSystems tutorial.IntScheme</l1>>

1 Subsystems

1.1 Create

We want to create two subsystems which represent the western and eastern part of the system. This is best done by creating two boundaries and then creating zones from these boundaries. The SubSystem class can then be used to create subsystems from these zones.

from powfacpy.applications.topology import Topology
from powfacpy.pf_classes.elm.boundary import Boundary
from powfacpy.pf_classes.elm.zone import Zone
from powfacpy.applications.subsystems import SubSystem

try:
    app.Hide()
    topo = Topology(app)
    bus_branch = {
        r"Network Model\Network Data\Grid\Bus 14": [
            r"Network Model\Network Data\Grid\Line 04 - 14",
            r"Network Model\Network Data\Grid\Line 13 - 14",
        ],
        r"Network Model\Network Data\Grid\Bus 16": [
            r"Network Model\Network Data\Grid\Line 16 - 17"
        ],
    }
    boundary_west = Boundary(topo.create_boundary_from_bus_branch("West", bus_branch))
    zone_west = boundary_west.create_zone()
    boundary_east = Boundary(topo.create_boundary_from_bus_branch("East", bus_branch, to_branch=False),)
    zone_east = boundary_east.create_zone(color=3)
    Zone.show_zones_in_network_graphic()
    subsystems = [SubSystem(zone_east), SubSystem(zone_west)]
finally:
    app.Show()

1.2 Load Flow State

We can get a summary of the load flow state of the subsystem by calling the get_load_flow_state() method. This returns a pandas DataFrame with information on the active and reactive power balance, etc.

try:
    for subsys in subsystems:
        display(subsys.get_load_flow_state())
finally:
    app.Show()        
total_power_loads total_power_generation total_power_exchange
East 2107.100006+ 395.700001j 2350.000000+ 681.456464j 224.540595+ 2.324056j
total_power_loads total_power_generation total_power_exchange
West 3980.800003+1013.200001j 3781.611072+ 661.117299j -224.540595- 2.324056j

The load flow state can be accessed also as attributes, for example:

subsystems[0].load_flow.total_power_generation
(2350.0000000274126+681.4564641218697j)

1.3 Topology

We can access the internal topology of the subsystem, for example the lines and terminals contained in the subsystem.

try:
    app.Hide()
    display(subsystems[0].topology.lines)
    display(subsystems[0].topology.terminals)
finally:
    app.Show()  
[<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 14 - 15.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 15 - 16.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 16 - 17.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 16 - 19.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 16 - 21.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 16 - 24.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 21 - 22.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 22 - 23.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 23 - 24.ElmLne</l1>>]
[<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 14.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 15.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 16.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 19.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 20.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 21.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 22.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 23.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 24.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 33.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 34.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 35.ElmTerm</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Bus 36.ElmTerm</l1>>]

You can also check wheter subsystems are connected to each other:

subsystems[0].topology.is_neighbor(subsystems[1])
True

1.4 Dynamic Models

1.4.1 Synchronous Machines

We can also access information on the synchronous machines and their controllers (governors, AVRs, PSS) contained in the subsystem.

try:
    display(subsystems[0].dynamic_models.synchronous_machines.synchronous_machines)
    display(subsystems[0].dynamic_models.synchronous_machines.governors)
finally:
    app.Show() 
[<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 04.ElmSym</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 05.ElmSym</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 06.ElmSym</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 07.ElmSym</l1>>]
[<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 04.ElmComp\GOV 04.ElmDsl</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 05.ElmComp\GOV 05.ElmDsl</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 06.ElmComp\GOV 06.ElmDsl</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 07.ElmComp\GOV 07.ElmDsl</l1>>]

You can also get more detailled information on dynamic models such as governors. The get_governor_info() method for example returns a dictionary with the block definitions (BlkDef) of the governors as keys and the values are the respective synchronous machines (parent_elms), the parameter values and the average parameter values (weighted by the rated apparent power by default) of the respective DSL models. The information is sorted by block definition, so you can easily compare the parameter values of different synchronous machines with the same block definition.

governor_info = subsystems[0].dynamic_models.synchronous_machines.get_governor_info()
governor_info
{<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Library.IntPrjfolder\User Defined Models.IntPrjfolder\gov_IEEEG1.BlkDef</l1>>: {'dsl_models': [<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 04.ElmComp\GOV 04.ElmDsl</l1>>,
   <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 05.ElmComp\GOV 05.ElmDsl</l1>>,
   <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 06.ElmComp\GOV 06.ElmDsl</l1>>,
   <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Power Plant 07.ElmComp\GOV 07.ElmDsl</l1>>],
  'parameters': {'K': [5.0, 5.0, 5.0, 5.0],
   'T1': [0.2, 0.2, 0.2, 0.2],
   'T2': [1.0, 1.0, 1.0, 1.0],
   'T3': [0.6, 0.6, 0.6, 0.6],
   'K1': [0.3, 0.3, 0.3, 0.3],
   'K2': [0.0, 0.0, 0.0, 0.0],
   'T5': [0.5, 0.5, 0.5, 0.5],
   'K3': [0.25, 0.25, 0.25, 0.25],
   'K4': [0.0, 0.0, 0.0, 0.0],
   'T6': [0.8, 0.8, 0.8, 0.8],
   'K5': [0.3, 0.3, 0.3, 0.3],
   'K6': [0.0, 0.0, 0.0, 0.0],
   'T4': [0.6, 0.6, 0.6, 0.6],
   'T7': [1.0, 1.0, 1.0, 1.0],
   'K7': [0.15, 0.15, 0.15, 0.15],
   'K8': [0.0, 0.0, 0.0, 0.0],
   'PNhp': [0.0, 0.0, 0.0, 0.0],
   'PNlp': [0.0, 0.0, 0.0, 0.0],
   'Uc': [-0.3, -0.3, -0.3, -0.3],
   'Pmin': [0.0, 0.0, 0.0, 0.0],
   'Uo': [0.3, 0.3, 0.3, 0.3],
   'Pmax': [1.0, 1.0, 1.0, 1.0]},
  'parent_elms': [<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 04.ElmSym</l1>>,
   <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 05.ElmSym</l1>>,
   <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 06.ElmSym</l1>>,
   <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 07.ElmSym</l1>>],
  'average': {'K': np.float64(5.0),
   'T1': np.float64(0.2),
   'T2': np.float64(1.0),
   'T3': np.float64(0.6),
   'K1': np.float64(0.3),
   'K2': np.float64(0.0),
   'T5': np.float64(0.5),
   'K3': np.float64(0.25),
   'K4': np.float64(0.0),
   'T6': np.float64(0.8),
   'K5': np.float64(0.3),
   'K6': np.float64(0.0),
   'T4': np.float64(0.6),
   'T7': np.float64(1.0),
   'K7': np.float64(0.15),
   'K8': np.float64(0.0),
   'PNhp': np.float64(0.0),
   'PNlp': np.float64(0.0),
   'Uc': np.float64(-0.3),
   'Pmin': np.float64(0.0),
   'Uo': np.float64(0.3),
   'Pmax': np.float64(1.0)}}}

We can also display the information in a more readable format:

from powfacpy.pf_classes.elm.dsl import display_dsl_models_info
display_dsl_models_info(governor_info)
Block Definition 'gov_IEEEG1': <l3>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Library.IntPrjfolder\User Defined Models.IntPrjfolder\gov_IEEEG1.BlkDef</l3>
   Parent elements:
      G 04 of class ElmSym (<l3>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 04.ElmSym</l3>)
      G 05 of class ElmSym (<l3>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 05.ElmSym</l3>)
      G 06 of class ElmSym (<l3>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 06.ElmSym</l3>)
      G 07 of class ElmSym (<l3>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\G 07.ElmSym</l3>)
   Parameter values:
      K: [5.0, 5.0, 5.0, 5.0]
      T1: [0.2, 0.2, 0.2, 0.2]
      T2: [1.0, 1.0, 1.0, 1.0]
      T3: [0.6, 0.6, 0.6, 0.6]
      K1: [0.3, 0.3, 0.3, 0.3]
      K2: [0.0, 0.0, 0.0, 0.0]
      T5: [0.5, 0.5, 0.5, 0.5]
      K3: [0.25, 0.25, 0.25, 0.25]
      K4: [0.0, 0.0, 0.0, 0.0]
      T6: [0.8, 0.8, 0.8, 0.8]
      K5: [0.3, 0.3, 0.3, 0.3]
      K6: [0.0, 0.0, 0.0, 0.0]
      T4: [0.6, 0.6, 0.6, 0.6]
      T7: [1.0, 1.0, 1.0, 1.0]
      K7: [0.15, 0.15, 0.15, 0.15]
      K8: [0.0, 0.0, 0.0, 0.0]
      PNhp: [0.0, 0.0, 0.0, 0.0]
      PNlp: [0.0, 0.0, 0.0, 0.0]
      Uc: [-0.3, -0.3, -0.3, -0.3]
      Pmin: [0.0, 0.0, 0.0, 0.0]
      Uo: [0.3, 0.3, 0.3, 0.3]
      Pmax: [1.0, 1.0, 1.0, 1.0]
   Average parameter values:
      K: 5.0
      T1: 0.2
      T2: 1.0
      T3: 0.6
      K1: 0.3
      K2: 0.0
      T5: 0.5
      K3: 0.25
      K4: 0.0
      T6: 0.8
      K5: 0.3
      K6: 0.0
      T4: 0.6
      T7: 1.0
      K7: 0.15
      K8: 0.0
      PNhp: 0.0
      PNlp: 0.0
      Uc: -0.3
      Pmin: 0.0
      Uo: 0.3
      Pmax: 1.0

1.4.1.1 CSV Export

Or export the information to csv files:

from powfacpy.pf_classes.elm.dsl import export_dsl_model_info_to_csv

export_dsl_model_info_to_csv(governor_info, r".\output\governor_info")

There is also a method to export the information on all dynamic models (governors, AVRs, PSS) of the subsystem to csv files:

subsystems[0].dynamic_models.synchronous_machines.export_info_to_csv(r".\output\synchronous_machines_info")

1.5 Dynamics

subsystem.dynamics collects dynamics-related aggregate quantities - relevant e.g. for frequency stability and islanding: the inertia of every dynamic unit, the active-power margins, and the load / generation balance.

Inertia is resolved per unit - directly from the type for synchronous machines, and via the template model interfaces for grid-forming converters (this system has none, so the converter rows are empty here):

try:
    app.Hide()
    display(subsystems[0].dynamics.get_inertia_details())
finally:
    app.Show()
name class S_MVA H_s inertia_MWs grid_forming template
0 G 04 ElmSym 800.0 3.575 2859.999847 True ElmSym/TypSym
1 G 05 ElmSym 600.0 4.333 2599.800110 True ElmSym/TypSym
2 G 06 ElmSym 800.0 4.350 3479.999924 True ElmSym/TypSym
3 G 07 ElmSym 700.0 3.771 2639.700103 True ElmSym/TypSym
subsystems[0].dynamics.total_inertia_MWs()
11579.499983787537

The active-power headroom is a load-flow property, so it lives on subsystem.load_flow: get_power_margins() reports the up- and downward headroom per generator (from the operational limits configured on the elements):

try:
    app.Hide()
    display(subsystems[0].load_flow.get_power_margins())
finally:
    app.Show()
name class P_MW P_max_MW P_min_MW upward_margin_MW downward_margin_MW
0 G 04 ElmSym 632.0 680.0 0.0 48.0 632.0
1 G 05 ElmSym 508.0 255.0 0.0 -253.0 508.0
2 G 06 ElmSym 650.0 680.0 0.0 30.0 650.0
3 G 07 ElmSym 560.0 595.0 0.0 35.0 560.0

If you only need the fleet-wide totals rather than the per-generator breakdown, upward_power_margin_MW() / downward_power_margin_MW() sum the respective column:

subsystems[0].load_flow.upward_power_margin_MW(), subsystems[0].load_flow.downward_power_margin_MW()
(-140.00000002741263, 2350.0000000274126)

dynamics.get_state() is a one-row summary of the whole subsystem (it pulls the margins from load_flow):

try:
    app.Hide()
    display(subsystems[0].dynamics.get_state())
finally:
    app.Show()
total_load_MW total_generation_MW power_exchange_MW synchronous_inertia_MWs converter_inertia_MWs total_inertia_MWs inertia_constant_on_generation_s upward_power_margin_MW downward_power_margin_MW
East 2107.100006 2350.0 224.540595 11579.499984 0.0 11579.499984 4.927447 -140.0 2350.0

1.6 Secondary (Power-Frequency) Control

A subsystem intended to run as an island (or otherwise balance its own interchange) needs a secondary controller regulating its own power exchange, not the whole grid’s. load_flow.create_secondary_controller() is a thin wrapper around StaticCalc.create_secondary_controller that sets this up automatically: it creates an ElmSecctrl with iexchange (“Exchange for”) set to Zone/Area as appropriate and pPmeas (“Boundary/Zone/Area”) set to the subsystem’s own grouping object, so the controller measures and regulates this subsystem’s interchange rather than a grid-wide setpoint.

The controlled machines (psym) are assembled from the subsystem’s internal units, one flag per category - here we regulate the internal synchronous machines:

try:
    app.Hide()
    secondary_controller = subsystems[0].load_flow.create_secondary_controller(
        synchronous_machines=True
    )
finally:
    app.Show()
secondary_controller
<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\East secondary controller.ElmSecctrl</l1>>

batteries, wind and pv add the corresponding internal unit categories to psym in the same way. Everything else on the controller (i_net, psetp, Kpf, …) is left at its PowerFactory default - set it explicitly via attr={...}, or afterwards like any other object. Without parent_folder, the controller is created in the grid of the subsystem’s first internal terminal.

2 Subsystem Container

Subsystem containers are designed to group multiple subsystems together and provide methods to analyze these groups.

2.1 Create

For example, you can create a subsystem container for the two subsystems we created in the previous section and one additional zone named ‘North’:

from powfacpy.applications.subsystems import SubSystemContainer

try:
    app.Hide()
    terminals_north = act_prj.get_calc_relevant_obj("Bus 30.ElmTerm, Bus 02.ElmTerm, Bus 25.ElmTerm, Bus 37.ElmTerm")
    zone_north = topo.create_zone("North", terminals_north, color=4)
    ss_container = SubSystemContainer([zone_east, zone_west, zone_north])
    Zone.show_zones_in_network_graphic()
finally:    
    app.Show()    

2.2 Load Flow State

We can get a summary of the load flow states of the subsystems:

try:
    app.Hide()
    display(ss_container.get_load_flow_state())
finally:    
    app.Show()    
total_power_loads total_power_generation total_power_exchange
East 2107.100006+ 395.700001j 2350.000000+ 681.456464j 224.540595+ 2.324056j
West 3756.800003+ 966.000000j 2991.611072+ 514.519195j -782.871862- 98.529796j
North 224.000000+ 47.200001j 790.000000+ 146.598105j 558.331267+ 96.205739j

You can also get a matrix (pandas DataFrame) with the power exchange between the subsystems:

try:
    app.Hide()
    display(ss_container.power_exchange)
finally:    
    app.Show()   
East West North
East NaN+ 0.000000j 224.540595+ 2.324056j NaN+ 0.000000j
West -224.540595- 2.324056j NaN+ 0.000000j -558.331267- 96.205739j
North NaN+ 0.000000j 558.331267+ 96.205739j NaN+ 0.000000j

2.3 Topology

We can also get the neighbors of the subsystems:

try:
    app.Hide()
    for subsys in ss_container.subsystems:
        print(f"{subsys.loc_name} has neighbors: ")
        print([ss.loc_name for ss in ss_container.get_neighboring_subsystems_in_container(subsys)])
finally:    
    app.Show()  
East has neighbors: 
['West']
West has neighbors: 
['East', 'North']
North has neighbors: 
['West']

You can also get the branch elements that physically tie subsystems together - lines, transformers, couplers, … - regardless of the current load flow (unlike the neighbor check above, this does not need a valid power flow, since it only looks at the topology):

try:
    app.Hide()
    display(ss_container.get_tie_lines())
finally:
    app.Show()
element class name subsystems
0 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 04 - 14 (East, West)
1 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 13 - 14 (East, West)
2 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 16 - 17 (East, West)
3 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 01 - 02 (West, North)
4 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 02 - 03 (West, North)
5 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 25 - 26 (West, North)

get_tie_lines() is a shortcut for get_tie_branches(element_classes=["ElmLne"]); omit element_classes to also catch tie transformers, couplers, etc.:

try:
    app.Hide()
    display(ss_container.get_tie_branches())
finally:
    app.Show()
element class name subsystems
0 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 04 - 14 (East, West)
1 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 13 - 14 (East, West)
2 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 16 - 17 (East, West)
3 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 01 - 02 (West, North)
4 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 02 - 03 (West, North)
5 <l3>\seberlein.IntUser\powfacpy\39_bus_new_eng... ElmLne Line 25 - 26 (West, North)

To get just the elements between one specific pair of subsystems:

try:
    app.Hide()
    display(ss_container.get_tie_branches_between(zone_east, zone_west))
finally:
    app.Show()
[<powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 04 - 14.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 13 - 14.ElmLne</l1>>,
 <powerfactory.DataObject <l1>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Line 16 - 17.ElmLne</l1>>]

2.4 Dynamic Models

2.4.1 Synchronous Machines

The SubSystemContainer class also provides a method to export the information on the synchronous machines contained in all subsystems to csv files.

ss_container.export_synchronous_machines_info_to_csv(r".\output\synchronous_machines_info_by_subsystem")
Back to top