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, (eg. 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.