Asymmetric Calculation Example

In this notebook we will walk through an example of asymmetric calculation using power-grid-model. The following points are covered

  • Construction of the model

  • Run asymmetric power flow calculation once, and its relevant function arguments

  • Run asymmetric power flow in batch calculations, and its relevant function arguments

  • Run state estimation once, and its relevant function arguments

This notebook serves as an example of how to use the Python API. For detailed API documentation, refer to Python API reference and Native Data Interface.

Example Network

We use a simple network with 3 nodes, 1 source, 3 lines, and 2 loads (1 symmetric load, 1 asymmetric load). As shown below:

 -------------------line_8-------------------
 |                                          |
node_1 ---line_3--- node_2 ----line_5---- node_6
 |                    |                     |
source_10          sym_load_4           asym_load_7

The 3 nodes are connected in a triangular way by 3 lines.

NOTE: load_7 is asymmetric in this case.

# some basic imports
import numpy as np
import warnings

with warnings.catch_warnings(action="ignore", category=DeprecationWarning):
    # suppress warning about pyarrow as future required dependency
    import pandas as pd

from power_grid_model import LoadGenType
from power_grid_model import PowerGridModel, CalculationMethod, CalculationType, MeasuredTerminalType
from power_grid_model import initialize_array

Power Flow Calculation

Input Dataset

We create input dataset by using the helper function initialize_array. Note the units of all input are standard SI unit without any prefix, i.e. the unit of voltage is volt (V), not kV.

Please refer Components for detailed explanation of all component types and their input/output attributes.

NOTE: The required attributes of each components can be different for asymmetric calculations.

# node
node = initialize_array("input", "node", 3)
node["id"] = np.array([1, 2, 6])
node["u_rated"] = [10.5e3, 10.5e3, 10.5e3]

# line
line = initialize_array("input", "line", 3)
line["id"] = [3, 5, 8]
line["from_node"] = [1, 2, 1]
line["to_node"] = [2, 6, 6]
line["from_status"] = [1, 1, 1]
line["to_status"] = [1, 1, 1]
line["r1"] = [0.25, 0.25, 0.25]
line["x1"] = [0.2, 0.2, 0.2]
line["c1"] = [10e-6, 10e-6, 10e-6]
line["tan1"] = [0.0, 0.0, 0.0]
line["i_n"] = [1000, 1000, 1000]
line["r0"] = [0.25, 0.25, 0.25]
line["x0"] = [0.2, 0.2, 0.2]
line["c0"] = [10e-6, 10e-6, 10e-6]
line["tan0"] = [0, 0, 0]

# sym load
sym_load = initialize_array("input", "sym_load", 1)
sym_load["id"] = [4]
sym_load["node"] = [2]
sym_load["status"] = [1]
sym_load["type"] = [LoadGenType.const_power]
sym_load["p_specified"] = [20e6]
sym_load["q_specified"] = [5e6]

# asym load
asym_load = initialize_array("input", "asym_load", 1)
asym_load["id"] = [7]
asym_load["node"] = [6]
asym_load["status"] = [1]
asym_load["type"] = [LoadGenType.const_power]
asym_load["p_specified"] = [[10e6, 20e6 , 0]] # the 3 phases may have different loads
asym_load["q_specified"] = [[0, 8e6, 2e6]] # the 3 phases may have different loads

# source
source = initialize_array("input", "source", 1)
source["id"] = [10]
source["node"] = [1]
source["status"] = [1]
source["u_ref"] = [1.0]

# all
asym_input_data = {
    "node": node,
    "line": line,
    "sym_load": sym_load,
    "asym_load": asym_load,
    "source": source
}

One-time Asymmetric Power Flow Calculation

You can call the method calculate_power_flow to do a one-time calculation based on the current network data in the model. In this case you should not specify the argument update_data as it is used for batch calculation.

NOTE: For asymmetric calculations, the argument symmetric of calculate_power_flow and assert_valid_input_data should both be set to False.

# Validation (optional)
from power_grid_model.validation import assert_valid_input_data

assert_valid_input_data(input_data=asym_input_data, calculation_type=CalculationType.power_flow, symmetric=False)

# Construction
asym_model = PowerGridModel(asym_input_data)

# One-time Asymmetric Power Flow Calculation
asym_result = asym_model.calculate_power_flow(
    symmetric=False,  # This enables asymmetric calculations
    error_tolerance=1e-8,
    max_iterations=20,
    calculation_method=CalculationMethod.newton_raphson)

We can print a result dataset of node by converting the array to dataframe and refering a specific attribute. In asymmetric calculations, the results of each phase is presented.

print("------node voltage result------")
print(pd.DataFrame(asym_result["node"]["u"]))
print("------node angle result------")
print(pd.DataFrame(asym_result["node"]["u_angle"]))
------node voltage result------
             0            1            2
0  6054.809261  6033.110279  6054.482030
1  5669.760420  5342.536191  5804.933216
2  5638.571767  5028.415453  5897.375970
------node angle result------
          0         1         2
0 -0.005248 -2.103279  2.092430
1 -0.043049 -2.143908  2.078903
2 -0.054168 -2.157686  2.092568

Batch Asymmetric Power Flow Calculation

As for asymmetric calculations (see the Power Flow Example), we can use the same method calculate_power_flow to calculate a number of asymmetric scenarios in one go. To do this, you need to supply an update_data argument. This argument contains a dictionary of 3D update arrays (one array per component type per phase).

The model uses the current data as the base scenario. For each individual calculation, the model applies each mutation to the base scenario and calculates the power flow.

NOTE: after the batch calculation, the original model will be kept unchanged. Internally the program copies the original model to multiple batch models for the calculation.

Independent Batch Dataset

There are two ways to specify the mutations. For each scenario:

  • only specify the objects that are changed in this scenario; or

  • specify all objects that are changed in one or multiple scenarios.

The latter is called independent batch dataset. Because all relevant objects are specified in each batch, different choices regarding performance optimization may be made in either case.

In general, the following is advised:

  • Use the non-independent batch dataset approach whenever few parameters change per scenario, but the batch samples many different components, e.g. during N-1 tests.

  • Use the independent batch dataset approach when a dense sampling of the parameter space is desired for relatively a few different components, e.g. during time series power flow calculation

See also performance guide for the latest recommendations.

The following code presented here creates a load profile with 10 timestamps for load_7. For N-1 scenario we refer to the Power Flow Example.

# note the shape of the array, 10 scenarios, 1 objects (asymmetric load_7)
load_profile = initialize_array("update", "asym_load", (10, 1))  

# this is a scale of asym_load from 0% to 100%------------------
# the array is an (10, 1, 3) shape, which shows (scenario, object, phase).
# Users can always customize the load_profile in different ways.
load_profile["id"] = [7]
load_profile["p_specified"] = [10e6, 20e6 , 0] * np.linspace(0, 1, 10).reshape(-1, 1, 1)

time_series_mutation = {"asym_load": load_profile}

We can calculate the time series and print the current of the lines.

# Validation (optional)
from power_grid_model.validation import assert_valid_batch_data

assert_valid_batch_data(input_data=asym_input_data, update_data=time_series_mutation, symmetric=False, calculation_type=CalculationType.power_flow)

# Batch Asymmetric Power Flow Calculation
output_data = asym_model.calculate_power_flow(update_data=time_series_mutation, symmetric=False)

Accessing batch data

It may be a bit unintuitive to read the output_data or update_data of a component directly as they are a dictionary of 4 dimension data, ie. \(ids \times batches \times attributes \times phases\). Remember that the output_data or update_data are a dictionary of numpy structured arrays. Hence the component should be indexed first. The index that follows can be indexed with numpy structured arrays.

To read the result of a single batch, e.g. 1st batch,

display(pd.DataFrame(output_data["line"]["p_from"][0]))
0 1 2
0 4.596683e+06 4.779610e+06 4.620493e+06
1 -2.222542e+06 -2.146006e+06 -2.213746e+06
2 2.298865e+06 2.512050e+06 2.310381e+06

Or maybe we wish to find result of a single component, (e.g., 1st line) in all batches

display(pd.DataFrame(output_data["line"]["i_from"][:,0]))
0 1 2
0 778.966268 1011.794128 815.316511
1 842.493451 1121.320184 815.316511
2 906.985877 1239.243741 815.316511
3 972.433947 1364.771826 815.316511
4 1038.839396 1497.459012 815.316511
5 1106.212978 1637.140796 815.316511
6 1174.572956 1783.892394 815.316511
7 1243.944125 1938.010800 815.316511
8 1314.357217 2100.018933 815.316511
9 1385.848583 2270.693567 815.316511

Asymmetric State Estimation

Input Dataset for State Estimation

NOTE: Asymmetric voltage/power sensors should be applied to (at least) asymmetric components.

# sym voltage sensor
sym_voltage_sensor = initialize_array("input", "sym_voltage_sensor", 2)
sym_voltage_sensor["id"] = [11, 12]
sym_voltage_sensor["measured_object"] = [1, 2]
sym_voltage_sensor["u_sigma"] = [100, 10]
sym_voltage_sensor["u_measured"] = [6000, 5500]

# asym voltage sensor
asym_voltage_sensor = initialize_array("input", "asym_voltage_sensor", 1)
asym_voltage_sensor["id"] = [13]
asym_voltage_sensor["measured_object"] = [6]
asym_voltage_sensor["u_sigma"] = [100]
asym_voltage_sensor["u_measured"] = [[5640, 5000, 6000]]


# sym power sensor
sym_power_sensor = initialize_array("input", "sym_power_sensor", 7)
sym_power_sensor["id"] = [14, 15, 16, 17, 18, 19, 20]
sym_power_sensor["measured_object"] = [3, 3, 5, 5, 8, 8, 4]
sym_power_sensor["measured_terminal_type"] = [
    MeasuredTerminalType.branch_from, MeasuredTerminalType.branch_to,
    MeasuredTerminalType.branch_from, MeasuredTerminalType.branch_to,
    MeasuredTerminalType.branch_from, MeasuredTerminalType.branch_to,
    MeasuredTerminalType.load
]
sym_power_sensor["power_sigma"] = [1.0e5, 1.0e4, 1.0e5, 1.0e4, 1.0e4, 1.0e5, 1.0e5]
sym_power_sensor["p_measured"] = [10e6, -20e6, 4e6, -4e6, 25e6, -15e6, 20e6]
sym_power_sensor["q_measured"] = [5e6, -7e6, 2e6, -2e6, 5e6, -5e6, 5e6]

# asym power sensor
asym_power_sensor = initialize_array("input", "asym_power_sensor", 1)
asym_power_sensor["id"] = [21]
asym_power_sensor["measured_object"] = [6]
asym_power_sensor["measured_terminal_type"] = [MeasuredTerminalType.node]
asym_power_sensor["power_sigma"] = [1.0e5]
asym_power_sensor["p_measured"] = [[10e6, 20e6, 0]]
asym_power_sensor["q_measured"] = [[0, 8e6, 2e6]]

# all 
asym_input_data = {
    "node": node,
    "line": line,
    "sym_load": sym_load,
    "asym_load": asym_load,
    "source": source,
    "sym_voltage_sensor": sym_voltage_sensor,
    "asym_voltage_sensor": asym_voltage_sensor,
    "sym_power_sensor": sym_power_sensor,
    "asym_power_sensor": asym_power_sensor,
}

One-time Asymmetric State Estimation

# Validation(optional)
from power_grid_model.validation import assert_valid_input_data
assert_valid_input_data(input_data=asym_input_data, calculation_type=CalculationType.state_estimation, symmetric=False)

# Construction
asym_model = PowerGridModel(asym_input_data)

# Perform one-time asymmetric state estimation calculation
asym_result = asym_model.calculate_state_estimation(symmetric=False, error_tolerance=1e-3)

We can also print a result dataset of node by converting the array to dataframe.

print("------node voltage result------")
display(pd.DataFrame(asym_result["sym_voltage_sensor"]["u_residual"]))

print("------sym_load result------")
display(pd.DataFrame(asym_result["sym_power_sensor"]["p_residual"]))
------node voltage result------
0 1 2
0 -456.470670 -453.494614 -457.679045
1 -96.057029 -96.031740 -95.861330
------sym_load result------
0 1 2
0 -4.822819e+06 -4.778850e+06 -4.842920e+06
1 2.883705e+05 2.554854e+05 3.032475e+05
2 4.827303e+05 4.255387e+05 5.069975e+05
3 -5.005799e+05 -4.454748e+05 -5.240727e+05
4 -8.573380e+05 -8.781561e+05 -8.504175e+05
5 2.711764e+06 2.721883e+06 2.709151e+06
6 5.622326e+05 6.523093e+05 5.230883e+05

For the observability and batch calculation of state estimation, we refer to the State Estimation Example.