Power Flow Example
In this notebook we will walk through an example of power flow calculation of power-grid-model.
The following points are covered
Construction of the model
Run power flow once, and its relevant function arguments
Update (change) of the model
Run power flow in batch calculations, and its relevant function arguments
Error handling
It 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. As shown below:
-----------------------line_8---------------
| |
node_1 ---line_3--- node_2 ----line_5---- node_6
| | |
source_10 sym_load_4 sym_load_7
The 3 nodes are connected in a triangular way by 3 lines.
# some basic imports
import numpy as np
import pandas as pd
from power_grid_model import (
AttributeType,
CalculationMethod,
CalculationType,
ComponentAttributeFilterOptions,
ComponentType,
DatasetType,
LoadGenType,
PowerGridModel,
attribute_dtype,
initialize_array,
)
Input Dataset
We create an input dataset by using the helper function initialize_array.
The units of all input are standard SI unit without any prefix, i.e. the unit of voltage is volt (V), not kV.
The attributes are of numpy array type.
AttributeType.idfield is a unique field across theinput_datadictionary. The keys ofinput-dataare predefined.
Please refer to Components for detailed explanation of all component types and their input/output attributes.
# node
node = initialize_array(DatasetType.input, ComponentType.node, 3)
node[AttributeType.id] = np.array([1, 2, 6])
node[AttributeType.u_rated] = [10.5e3, 10.5e3, 10.5e3]
# line
line = initialize_array(DatasetType.input, ComponentType.line, 3)
line[AttributeType.id] = [3, 5, 8]
line[AttributeType.from_node] = [1, 2, 1]
line[AttributeType.to_node] = [2, 6, 6]
line[AttributeType.from_status] = [1, 1, 1]
line[AttributeType.to_status] = [1, 1, 1]
line[AttributeType.r1] = [0.25, 0.25, 0.25]
line[AttributeType.x1] = [0.2, 0.2, 0.2]
line[AttributeType.c1] = [10e-6, 10e-6, 10e-6]
line[AttributeType.tan1] = [0.0, 0.0, 0.0]
line[AttributeType.i_n] = [1000, 1000, 1000]
# load
sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 2)
sym_load[AttributeType.id] = [4, 7]
sym_load[AttributeType.node] = [2, 6]
sym_load[AttributeType.status] = [1, 1]
sym_load[AttributeType.type] = [LoadGenType.const_power, LoadGenType.const_power]
sym_load[AttributeType.p_specified] = [20e6, 10e6]
sym_load[AttributeType.q_specified] = [5e6, 2e6]
# source
source = initialize_array(DatasetType.input, ComponentType.source, 1)
source[AttributeType.id] = [10]
source[AttributeType.node] = [1]
source[AttributeType.status] = [1]
source[AttributeType.u_ref] = [1.0]
# all
input_data = {
ComponentType.node: node,
ComponentType.line: line,
ComponentType.sym_load: sym_load,
ComponentType.source: source,
}
It is also possible to specify a component input data in a columnar data format.
A columnar data format better integrates with most databases. In addition, it may bring memory and, in some cases, even computational performance improvements, because unused attribute columns can be omitted.
Make sure to provide the correct dtype to the numpy arrays, exposed for each dataset type, component and attribute via the helper function attribute_dtype function.
source_columns = {
AttributeType.id: np.array([10], dtype=attribute_dtype(DatasetType.input, ComponentType.source, AttributeType.id)),
AttributeType.node: np.array(
[1], dtype=attribute_dtype(DatasetType.input, ComponentType.source, AttributeType.node)
),
AttributeType.status: np.array(
[1], dtype=attribute_dtype(DatasetType.input, ComponentType.source, AttributeType.status)
),
AttributeType.u_ref: np.array(
[1.0], dtype=attribute_dtype(DatasetType.input, ComponentType.source, AttributeType.u_ref)
),
# We're not creating columns for u_ref_angle, sk, ... Instead, the default values are used. This saves us memory.
}
input_data = {
ComponentType.node: node,
ComponentType.line: line,
ComponentType.sym_load: sym_load,
ComponentType.source: source_columns,
}
We can print the input dataset by converting the numpy array to dataframe.
print(pd.DataFrame(input_data[ComponentType.sym_load]))
id node status type p_specified q_specified
0 4 2 1 0 20000000.0 5000000.0
1 7 6 1 0 10000000.0 2000000.0
(Assignment 1)
Validation (optional)
For efficiency reasons, most of the data is not explicitly validated in the power grid model. However, in most cases, a power flow calculation will fail/crash if the data is invalid. Often with a low level error message that is hard to relate to the original objects. Therfore, it is recommended to always validate your data before constructing a PowerGridModel instance.
The simplest and most effective way to validate your data is by using assert_valid_input_data() which will throw an error if it encounters any invalid data. See the Error Handling section in this example notebook and Validation Examples for more detailed information on the validation functions.
from power_grid_model.validation import assert_valid_input_data
assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow)
(Assignment 2)
Construction
The construction of the model is just calling the constructor of PowerGridModel. The default system_frequency is 50.0 Hz.
model = PowerGridModel(input_data)
# model = PowerGridModel(input_data, system_frequency=60.0) # if you have another system frequency
(Assignment 3)
One-time 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.
The following command executes a one-time power flow calculation with (they are also the default values for those arguments)
Symmetric calculation
Newton-Raphson method
Error tolerance: 1e-8
Maximum number of iteration: 20
output_data = model.calculate_power_flow(
symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson
)
Result Dataset
We can also print a result dataset of node and line by converting the array to dataframe.
print("------node result------")
print(pd.DataFrame(output_data[ComponentType.node]))
print("------line result------")
print(pd.DataFrame(output_data[ComponentType.line]))
------node result------
id energized u_pu u u_angle p q
0 1 1 0.998988 10489.375043 -0.003039 3.121451e+07 6.991358e+06
1 2 1 0.952126 9997.325181 -0.026031 -2.000000e+07 -5.000000e+06
2 6 1 0.962096 10102.012975 -0.021895 -1.000000e+07 -2.000000e+06
------line result------
id energized loading p_from q_from i_from \
0 3 1 0.985666 1.736010e+07 4.072097e+06 981.460041
1 5 1 0.205940 -3.365614e+06 -1.178649e+06 205.939917
2 8 1 0.783206 1.385441e+07 2.919262e+06 779.311446
s_from p_to q_to i_to s_to
0 1.783129e+07 -1.663439e+07 -3.821351e+06 985.666324 1.706768e+07
1 3.566030e+06 3.396558e+06 8.861080e+05 200.617323 3.510241e+06
2 1.415863e+07 -1.339656e+07 -2.886108e+06 783.206396 1.370392e+07
Select components / attributes and format of result dataset
By default power-grid-model calculates the result attributes for all components. If you do not need all the component types in your results, for example if you are only interested in node, you can specify the list of the component types you want in the result dataset in the argument output_component_types.
The following code calculate the power flow and only generate the output attributes for ComponentType.node.
output_data = model.calculate_power_flow(
symmetric=True,
error_tolerance=1e-8,
max_iterations=20,
calculation_method=CalculationMethod.newton_raphson,
output_component_types=[ComponentType.node],
)
print("List of component types in result dataset")
print(list(output_data.keys()))
print("------node result------")
print(pd.DataFrame(output_data[ComponentType.node]))
List of component types in result dataset
[node]
------node result------
id energized u_pu u u_angle p q
0 1 1 0.998988 10489.375043 -0.003039 3.121451e+07 6.991358e+06
1 2 1 0.952126 9997.325181 -0.026031 -2.000000e+07 -5.000000e+06
2 6 1 0.962096 10102.012975 -0.021895 -1.000000e+07 -2.000000e+06
You can also request the output as a dictionary of attribute and their values columns for each component type.
output_data = model.calculate_power_flow(
symmetric=True,
error_tolerance=1e-8,
max_iterations=20,
calculation_method=CalculationMethod.newton_raphson,
output_component_types={
ComponentType.line: [AttributeType.id, AttributeType.p_from], # line output columns id and p_from
},
)
print("List of component types in result dataset")
print(list(output_data))
print("List of attribute types in line result")
print(list(output_data[ComponentType.line]))
print("------line result------")
print(output_data[ComponentType.line])
List of component types in result dataset
[line]
List of attribute types in line result
[id, p_from]
------line result------
{id: array([3, 5, 8], dtype=int32), p_from: array([17360100.20222374, -3365613.74450156, 13854413.52498137])}
You can also mix output types between components. In this example:
Nonerequests row-based dataa list of attribute names requests columns for those attributes (as before)
ComponentAttributeFilterOptions.everythingrequests columns for all supported attributes of a component.
output_data = model.calculate_power_flow(
symmetric=True,
error_tolerance=1e-8,
max_iterations=20,
calculation_method=CalculationMethod.newton_raphson,
output_component_types={
# node output as row-based
ComponentType.node: None,
# line output columns id and p_from
ComponentType.line: [AttributeType.id, AttributeType.p_from],
# all sym_load attributes as columns
ComponentType.sym_load: ComponentAttributeFilterOptions.everything,
},
)
print("List of component types in result dataset")
print(list(output_data.keys()))
print("------node result------")
print(output_data[ComponentType.node].dtype.names)
print("------line result attributes------")
print(list(output_data[ComponentType.line].keys()))
print("------sym_load result attributes------")
print(list(output_data[ComponentType.sym_load].keys()))
List of component types in result dataset
[node, line, sym_load]
------node result------
(id, energized, u_pu, u, u_angle, p, q)
------line result attributes------
[id, p_from]
------sym_load result attributes------
[id, energized, p, q, i, s, pf]
Finally, it is possible to request columnar data for all attributes using the ComponentAttributeFilterOptions.everything as a shorthand.
output_data = model.calculate_power_flow(
symmetric=True,
error_tolerance=1e-8,
max_iterations=20,
calculation_method=CalculationMethod.newton_raphson,
# all attributes for all component types as columns
output_component_types=ComponentAttributeFilterOptions.everything,
)
print("List of component types in result dataset")
print(list(output_data.keys()))
print("------node result------")
print(list(output_data[ComponentType.node].keys()))
print("------line result attributes------")
print(list(output_data[ComponentType.line].keys()))
print("------sym_load result attributes------")
print(list(output_data[ComponentType.sym_load].keys()))
List of component types in result dataset
[node, line, sym_load, source]
------node result------
[id, energized, u_pu, u, u_angle, p, q]
------line result attributes------
[id, energized, loading, p_from, q_from, i_from, s_from, p_to, q_to, i_to, s_to]
------sym_load result attributes------
[id, energized, p, q, i, s, pf]
Calculation Method
There are four calculation methods available. They are listed in the CalculationMethod enumeration as follows:
Iterative methods: These Iterative methods converge to the true solution. You specify the
error_toleranceandmax_iterationsas the iteration stopping condition. If the maximum number of iteration is reached and the error is still higher than the error tolerance, an exception will be thrown.CalculationMethod.newton_raphson: traditional Newton-Raphson method.CalculationMethod.iterative_current: a novel jacobi like method.
Linear methods: linearization approximation methods. In this case the
error_toleranceandmax_iterationsare not relevant here. They are also garanteed to have a solution.CalculationMethod.linear: It treats all loads as constant impedances.CalculationMethod.linear_current: It is essentially a single iteration of Iterative current.
Following command calculates the power flow with linear method. Note the difference of result for node compared to the results from Newton-Raphson method.
output_data = model.calculate_power_flow(symmetric=True, calculation_method=CalculationMethod.linear)
print("------node result------")
print(pd.DataFrame(output_data[ComponentType.node]))
------node result------
id energized u_pu u u_angle p q
0 1 1 0.999087 10490.414234 -0.002789 2.862536e+07 6.264884e+06
1 2 1 0.956325 10041.412991 -0.023873 -1.829115e+07 -4.572788e+06
2 6 1 0.965236 10134.973932 -0.020204 -9.316798e+06 -1.863360e+06
(Assignment 4)
Update Model
One can mutate the model by providing a dictionary of update data. This is similar to creating an input dataset. The only difference is to initialize the array with the DatasetType.update key.
Refer to Components for detailed explanation of all mutable attributes per component type.
NOTE: the initialized the array contains default NULL values for everything. If you do not need to update an attribute, you do not have to specify it. In the C++ core, the default NULL value will be treated as “not updating” for this attribute.
Create Update Dataset
The following code creates an update dataset which changes the two loads and switches Line #3 off at the from side.
update_sym_load = initialize_array(DatasetType.update, ComponentType.sym_load, 2)
update_sym_load[AttributeType.id] = [4, 7] # same ID
update_sym_load[AttributeType.p_specified] = [30e6, 15e6] # change active power
# leave reactive power the same, no need to specify
update_line = initialize_array(DatasetType.update, ComponentType.line, 1)
update_line[AttributeType.id] = [3] # change line ID 3
update_line[AttributeType.from_status] = [0] # switch off at from side
# leave to-side swiching status the same, no need to specify
update_data = {ComponentType.sym_load: update_sym_load, ComponentType.line: update_line}
If you are updating all components of the same type (uniformly) in the same order as in the input data, providing IDs is optional.
update_sym_load_no_id = initialize_array(DatasetType.update, ComponentType.sym_load, 2)
update_sym_load_no_id[AttributeType.p_specified] = [30e6, 15e6] # change active power for both sym_loads
# leave reactive power the same, no need to specify
update_line_no_id = initialize_array(DatasetType.update, ComponentType.line, 3)
update_line_no_id[AttributeType.from_status] = [0, 1, 1] # switch off at from side for line #3
update_data_no_id = {ComponentType.sym_load: update_sym_load_no_id, ComponentType.line: update_line_no_id}
Columnar data is also supported.
columnar_update_line = {
AttributeType.id: np.array(
[3], dtype=attribute_dtype(DatasetType.update, ComponentType.line, AttributeType.id)
), # change line ID 3
AttributeType.from_status: np.array(
[0], dtype=attribute_dtype(DatasetType.update, ComponentType.line, AttributeType.from_status)
), # switch off at from side
}
# leave to-side swiching status the same, no need to specify
update_data_col = {ComponentType.sym_load: update_sym_load, ComponentType.line: columnar_update_line}
Columnar data also supports optional IDs.
columnar_no_ID_update_line = {
# Update IDs are not specified
AttributeType.from_status: np.array(
[0, 1, 1], dtype=attribute_dtype(DatasetType.update, ComponentType.line, AttributeType.from_status)
), # The update for the whole column needs to be specified
}
# leave to-side swiching status the same, no need to specify
update_data_col_no_id = {ComponentType.sym_load: update_sym_load, ComponentType.line: columnar_no_ID_update_line}
Validation (optional)
When performing batch calculation/updating dataset, power-grid-model provides the functions to validate. See the Error Handling section in this example notebook, or Validation Examples for more detailed information on the validation functions.
from power_grid_model.validation import assert_valid_batch_data
assert_valid_batch_data(input_data=input_data, update_data=update_data, calculation_type=CalculationType.power_flow)
assert_valid_batch_data(
input_data=input_data, update_data=update_data_no_id, calculation_type=CalculationType.power_flow
)
assert_valid_batch_data(input_data=input_data, update_data=update_data_col, calculation_type=CalculationType.power_flow)
assert_valid_batch_data(
input_data=input_data, update_data=update_data_col_no_id, calculation_type=CalculationType.power_flow
)
Call Update Method
Call the update function on a copy of the model. Note that you don’t need to make a copy in general, this is only done to keep the original model intact.
model_2 = model.copy()
model_2.update(update_data=update_data)
Re-calculate Power Flow
We calculate power flow again with all default arguments. And print the line results. See the difference of the updated data. Line #3 has zero power flow at the from-side and very small power flow at to-side (due to capacitance).
output_data = model_2.calculate_power_flow()
print("------line result------")
print(pd.DataFrame(output_data[ComponentType.line]))
------line result------
id energized loading p_from q_from i_from \
0 3 1 0.014030 0.000000e+00 0.000000e+00 0.000000
1 5 1 2.268159 -3.000004e+07 -4.812057e+06 2268.158655
2 8 1 3.249928 5.676867e+07 1.571464e+07 3245.829906
s_from p_to q_to i_to s_to
0 0.000000e+00 3.691991e+01 -1.879430e+05 14.030122 1.879430e+05
1 3.038352e+07 3.385470e+07 7.678867e+06 2265.269969 3.471463e+07
2 5.890358e+07 -4.885470e+07 -9.678867e+06 3249.927645 4.980424e+07
Batch Calculation
We can use the same method calculate_power_flow to calculate a number of scenarios in one go. To do this, you need to supply an update_data argument. This argument contains a dictionary of 2D update arrays (one array per component type).
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.
For each scenario specify all objects that are changed in one or more 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.
Examples
Below we show three examples.
Time Series Profile
The following code creates a load profile with 10 timestamps for the two loads. The two loads are always present for all mutation scenarios.
load_profile = initialize_array(
DatasetType.update, ComponentType.sym_load, (10, 2)
) # note the shape of the array, 10 scenarios, 2 objects (loads)
# below is an assignment of shape (1, 2) array to shape (10, 2) array
# the numpy broadcasting rule ensures that the same object ids are repeated 10 times
# therefore the two objects are present for all the scenarios
load_profile[AttributeType.id] = [[4, 7]]
# this is a scale of load from 0% to 100%
# the array is an (10, 2) shape, each row is a scenario, each column is an object
load_profile[AttributeType.p_specified] = [[30e6, 15e6]] * np.linspace(0, 1, 10).reshape(-1, 1)
time_series_mutation = {ComponentType.sym_load: load_profile}
assert_valid_batch_data(
input_data=input_data, update_data=time_series_mutation, calculation_type=CalculationType.power_flow
)
We can calculate the time series and print the current of the lines.
output_data = model.calculate_power_flow(update_data=time_series_mutation)
print(output_data[ComponentType.line][AttributeType.i_from])
[[ 193.06162675 64.92360593 137.59086941]
[ 248.65360093 72.28087746 185.86691646]
[ 368.12834615 90.73999329 285.46275837]
[ 510.20016036 115.51167381 401.15982574]
[ 662.04311447 143.73983633 523.57045909]
[ 819.63118685 174.08971222 649.95104138]
[ 981.46004118 205.93991655 779.31144601]
[1146.90787963 238.98921914 911.25418138]
[1315.7236725 273.08968816 1045.6266953 ]
[1487.83778526 308.17354058 1182.39546494]]
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 3 dimension data, i.e., \(ids \times batches \times attributes\). 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, lets say 1st batch,
print(pd.DataFrame(output_data[ComponentType.line][0]))
id energized loading p_from q_from i_from \
0 3 1 0.212031 29511.239220 3.508895e+06 193.061627
1 5 1 0.064924 -1267.160636 -1.172098e+06 64.923606
2 8 1 0.156578 19805.345039 2.500724e+06 137.590869
s_from p_to q_to i_to s_to
0 3.509019e+06 1267.160636 -3.827902e+06 212.030847 3.827902e+06
1 1.172099e+06 3574.919119 8.320006e+05 46.000611 8.320083e+05
2 2.500802e+06 -3574.919119 -2.832001e+06 156.577603 2.832003e+06
Or maybe we wish to find result of a single component, (e.g., 1st line) in all batches
print(output_data[ComponentType.line][AttributeType.i_from][:, 0])
[ 193.06162675 248.65360093 368.12834615 510.20016036 662.04311447
819.63118685 981.46004118 1146.90787963 1315.7236725 1487.83778526]
(Assignment 5)
N-1 Scenario while specifying all objects in each scenario
The following code creates a N-1 scenario for all three lines. There are 3 scenarios, in each scenario, the from/to status of one line is switched off. However, all three lines are present in all mutation dataset. This means that for each scenario you will update two lines with the same switching status.
Specifying all objects can bring computational advantages.
line_profile = initialize_array(DatasetType.update, ComponentType.line, (3, 3)) # 3 scenarios, 3 objects (lines)
# below the same broadcasting trick
line_profile[AttributeType.id] = [[3, 5, 8]]
# fully specify the status of all lines, even it is the same as the base scenario
line_profile[AttributeType.from_status] = [[0, 1, 1], [1, 0, 1], [1, 1, 0]]
line_profile[AttributeType.to_status] = [[0, 1, 1], [1, 0, 1], [1, 1, 0]]
n_min_1_mutation_update_all = {ComponentType.line: line_profile}
assert_valid_batch_data(
input_data=input_data, update_data=n_min_1_mutation_update_all, calculation_type=CalculationType.power_flow
)
We can calculate the N-1 and print the current of the lines. It is clear that per scenario one line is disabled.
output_data = model.calculate_power_flow(update_data=n_min_1_mutation_update_all)
print(output_data[ComponentType.line][AttributeType.i_from])
[[ 0. 1352.02947002 1962.69857764]
[1199.97577809 0. 573.32693369]
[1877.3560102 634.81512055 0. ]]
N-1 Scenario where only the changed objects are specified
The following code creates a N-1 scenario for all three lines. There are 3 scenarios, in each scenario, the from/to status of one line is switched off. In this dataset we only specify one line per mutation.
line_profile = initialize_array(
DatasetType.update, ComponentType.line, (3, 1)
) # 3 scenarios, 1 object mutation per scenario
# for each mutation, only one object is specified
line_profile[AttributeType.id] = [[3], [5], [8]]
# specify only the changed status (switch off) of one line
line_profile[AttributeType.from_status] = [[0], [0], [0]]
line_profile[AttributeType.to_status] = [[0], [0], [0]]
n_min_1_mutation_update_specific = {ComponentType.line: line_profile}
assert_valid_batch_data(
input_data=input_data, update_data=n_min_1_mutation_update_specific, calculation_type=CalculationType.power_flow
)
We can calculate the N-1 and print the current of the lines. Note the results are the same as the independent N-1.
output_data = model.calculate_power_flow(update_data=n_min_1_mutation_update_specific)
print(output_data[ComponentType.line][AttributeType.i_from])
[[ 0. 1352.02947002 1962.69857764]
[1199.97577809 0. 573.32693369]
[1877.3560102 634.81512055 0. ]]
Cartesian product of batch datasets
It is possible to conduct a batch calculation of multiple datasets in form of a cartesian product of their scenarios. Assume certain batch datasets with N1, N2, N3, … scenarios. This would give us \(N1 * N2 * N3 * ...\) possible combinations via the cartesian product. The resultant output data is in flat form and it has dimension of N1 * N2 * N3 with first dataset being the highest dimension. This can be beneficial in reducing complexity of implementation of such batch calculation along with keeping the size of such resultant update_data to a minimum.
We can combine the time series mutation for all n-1 contingencies together in following way.
Both the batch datasets are passed together in update_data in a list.
output_data = model.calculate_power_flow(update_data=[n_min_1_mutation_update_specific, time_series_mutation])
print("Output data has shape", output_data[ComponentType.line].shape)
line_output = output_data[ComponentType.line][AttributeType.energized]
print(line_output[0, :])
print(line_output[1, :])
print(line_output[10, :])
print(line_output[11, :])
Output data has shape (30, 3)
[0 1 1]
[0 1 1]
[1 0 1]
[1 0 1]
In addition, one can validate the cartesian product of batch datasets by validating its conforming datasets individually, as it is done in the validation example notebook. Conversely, the data validator in the validation module does not directly support a cartesian product of datasets, as this is not a new data structure, but just a list of datasets.
Caching Topology
If a batch scenario does not change the switching status of branches and sources, the model will re-use the pre-built internal graph/matrices for each calculation. Time-series load profile calculation is a typical use case. This can bring performance benefits.
If a batch scenario changes the switching status of branches and sources, the topology changes and no caching can be done for that scenario. N-1 check is a typical use case.
See the Performance Guide for tips and tricks regarding performance.
Parallel Computing
The batch calculation supports shared memory multi-threading parallel computing. The common internal states and variables are shared as much as possible to save memory usage and avoid copy. In the C++ implementation std::shared_ptr of const variable is used to share the resources between threads (which will not mutate the shared data).
You can set threading parameter to enable/disable parallel computing.
threading=-1, use sequential computing (default)threading=0, use number of threads available from the machine hardware (recommended)threading>0, set the number of threads you want to use
(Assignment 6)
Error Handling
The error handeling of power-grid-model is done with exceptions in all cases.
We also provide an validation mechanism, which validates data structures and values offline. It is recommended to always validate your data before constructing a PowerGridModel instance. An alternative approach would be to validate only when an exception is raised, but be aware that not all data errors will raise exceptions: most of them will just yield invalid results without warning.
Below we give examples of catching different error types in power grid model calculations. Refer to Validation Examples for more detailed information on the validation functions.
Construction
We try to construct a grid with a line connecting two nodes with different rated voltages. An ConflictVoltage error is caught in the construction.
from power_grid_model.errors import ConflictVoltage, PowerGridError
# node
node_error = initialize_array(DatasetType.input, ComponentType.node, 2)
node_error[AttributeType.id] = [1, 2]
node_error[AttributeType.u_rated] = [10.5e3, 150.0e3] # different rated voltages
# line
line_error = initialize_array(DatasetType.input, ComponentType.line, 1)
line_error[AttributeType.id] = [3]
line_error[AttributeType.from_node] = [1]
line_error[AttributeType.to_node] = [2]
line_error[AttributeType.from_status] = [1]
line_error[AttributeType.to_status] = [1]
line_error[AttributeType.r1] = [0.25]
line_error[AttributeType.x1] = [0.2]
line_error[AttributeType.c1] = [10e-6]
line_error[AttributeType.tan1] = [0.0]
line_error[AttributeType.i_n] = [1000]
error_data = {ComponentType.node: node_error, ComponentType.line: line_error}
try:
assert_valid_input_data(error_data, symmetric=True)
model = PowerGridModel(error_data)
output_data = model.calculate_state_estimation(symmetric=True)
except ConflictVoltage as e:
print(e)
Conflicting voltage for line 3
voltage at from node 1 is 10500
voltage at to node 2 is 150000
Try validate_input_data() or validate_batch_data() to validate your data.
Update
We try to update the model with a non-existing line id.
from power_grid_model.errors import IDNotFound
line_update_error = initialize_array(DatasetType.update, ComponentType.line, 1)
line_update_error[AttributeType.id] = [12345] # non-existing
line_update_error[AttributeType.from_status] = [1]
try:
model.update(update_data={ComponentType.line: line_update_error})
except IDNotFound as e:
print(e)
The id cannot be found: 12345
Try validate_input_data() or validate_batch_data() to validate your data.
Error in Calculation
Following command calculates the power flow with Newton-Raphson method that fails to converge, because the error tolerance is too low. It throws a PowerGridError
try:
model.calculate_power_flow(
symmetric=True, error_tolerance=1e-20, max_iterations=20, calculation_method=CalculationMethod.newton_raphson
)
except PowerGridError as e:
print(e)
Iteration failed to converge after 20 iterations! Max deviation: 3.54512293063893e-16, error tolerance: 1e-20.
Try validate_input_data() or validate_batch_data() to validate your data.
Error in Batch Calculation
Because parallel computing is allowed in batch calculation. The program will be terminated if an exception is thrown from the threads. Therefore, all the C++ exceptions are caught within the thread, and the error messages are re-thrown in the main thread and propagate as PowerGridBatchError in the Python side.
There could happen that only some of the scenarios fail in a batch calculation and others succeed. You can choose to specify the argument continue_on_batch_error (default False) if an error should be thrown in this case or let the program continue with partially valid results.
We modify the time-series load profile to add some errors. We add an unknown id for scenario 3, and a too large power in scenario 7.
time_series_mutation[ComponentType.sym_load][AttributeType.id][3] = 1000 # unknown id
time_series_mutation[ComponentType.sym_load][AttributeType.p_specified][7] = 1e100 # large power
# we run the batch calculation with default, it will raise error
from power_grid_model.errors import PowerGridBatchError
try:
output_data = model.calculate_power_flow(
update_data=time_series_mutation,
)
except PowerGridBatchError as e:
print(e)
print(f"Failed scenarios: {e.failed_scenarios}")
print(f"Succeeded scenarios: {e.succeeded_scenarios}")
print(f"Error messages: {e.error_messages}")
There are errors in the batch calculation.
Try validate_input_data() or validate_batch_data() to validate your data.
Failed scenarios: [3 7]
Succeeded scenarios: [0 1 2 4 5 6 8 9]
Error messages: ['The id cannot be found: 1000\n', 'Sparse matrix error, possibly singular matrix!\nIf you get this error from state estimation, it might mean the system is not fully observable, i.e. not enough measurements.\nIt might also mean that you are running into a corner case where PGM cannot resolve yet.\nSee https://github.com/PowerGridModel/power-grid-model/issues/864.']
# we run the batch calculation with continue_on_batch_error=True,
# it will return the results with partially valid data
output_data = model.calculate_power_flow(update_data=time_series_mutation, continue_on_batch_error=True)
# print node data for u_pu, note that the data is rubbish for scenario 3 and 7
print("Node data with invalid results")
print(output_data[ComponentType.node][AttributeType.u_pu])
# we can print the data with only succeeded scenarios
e = model.batch_error
print("Node data with only valid results")
print(output_data[ComponentType.node][AttributeType.u_pu][e.succeeded_scenarios])
Node data with invalid results
[[9.99401170e-001 9.92685785e-001 9.94521366e-001]
[9.99347687e-001 9.86226389e-001 9.89352855e-001]
[9.99288384e-001 9.79654011e-001 9.84095542e-001]
[2.18565566e-312 4.31807050e-316 2.97079411e-313]
[9.99151380e-001 9.66149483e-001 9.73298790e-001]
[9.99073166e-001 9.59205860e-001 9.67750710e-001]
[9.98988099e-001 9.52126208e-001 9.62096474e-001]
[4.31810212e-316 4.31811082e-316 4.31810212e-316]
[9.98796126e-001 9.37530046e-001 9.50447962e-001]
[9.98688504e-001 9.29997471e-001 9.44441670e-001]]
Node data with only valid results
[[0.99940117 0.99268579 0.99452137]
[0.99934769 0.98622639 0.98935286]
[0.99928838 0.97965401 0.98409554]
[0.99915138 0.96614948 0.97329879]
[0.99907317 0.95920586 0.96775071]
[0.9989881 0.95212621 0.96209647]
[0.99879613 0.93753005 0.95044796]
[0.9986885 0.92999747 0.94444167]]
Power flow calculations with automatic tap changing
For power flow calculations with automatic tap changing, please refer to the Transformer example notebook.