Connecting a new consumer

This demo shows how to use power-grid-model-ds to simulate a new consumer to be connected to the grid

  1. First we create an extension of the Grid objects with properties we want to use in this context

  2. We create a random grid structure for the purpose of the demo

  3. We define functions that add a new consumer to the grid and simulate its impact

from dataclasses import dataclass

import numpy as np
from numpy.typing import NDArray

from power_grid_model_ds import Grid
from power_grid_model_ds.arrays import LineArray, NodeArray


class ExtendedNodeArray(NodeArray):
    """Extends the node array with the simulated voltage and coordinates"""

    _defaults = {"u": 0}

    u: NDArray[np.float64]
    x_coor: NDArray[np.float64]
    y_coor: NDArray[np.float64]

    @property
    def is_overloaded(self):
        return np.logical_or(self.u > 1.1 * self.u_rated, self.u < 0.9 * self.u_rated)


class ExtendedLineArray(LineArray):
    """Extends the line array with current output"""

    _defaults = {"i_from": 0}

    i_from: NDArray[np.float64]

    @property
    def is_overloaded(self):
        return self.i_from > self.i_n


@dataclass
class ExtendedGrid(Grid):
    node: ExtendedNodeArray
    line: ExtendedLineArray
from power_grid_model_ds.generators import RadialGridGenerator

grid_generator = RadialGridGenerator(grid_class=ExtendedGrid, nr_nodes=20, nr_sources=1, nr_nops=10)
grid = grid_generator.run(seed=0)

grid.set_feeder_ids()

grid.node.x_coor = np.random.uniform(100, 500, len(grid.node))
grid.node.y_coor = np.random.uniform(100, 500, len(grid.node))

First we create a new consumer, with a location and a load demand

from power_grid_model import LoadGenType

from power_grid_model_ds.arrays import SymLoadArray
from power_grid_model_ds.enums import NodeType


def create_new_consumer_arrays(
    u_rated: float, x_coor: float, y_coor: float, p_specified: float, q_specified: float
) -> tuple[ExtendedNodeArray, SymLoadArray]:
    new_consumer = ExtendedNodeArray(
        u_rated=[u_rated],
        node_type=[NodeType.UNSPECIFIED],
        x_coor=[x_coor],
        y_coor=[y_coor],
    )
    new_consumer_load = SymLoadArray(
        node=[new_consumer.get_empty_value("id")],
        status=[1],
        type=[LoadGenType.const_power],
        p_specified=[p_specified],
        q_specified=[q_specified],
    )
    return new_consumer, new_consumer_load


new_consumer, new_consumer_load = create_new_consumer_arrays(10_500, 300, 300, 1_000_000, 200_000)

Now lets define some functions that add the new consumer by connecting it to the closest node

from power_grid_model_ds._core.load_flow import PowerGridModelInterface

R_PER_KM = 0.1
X_PER_KM = 0.1


def find_closest_node(grid: ExtendedGrid, x: float, y: float) -> int:
    dist = np.sqrt((grid.node.x_coor - x) ** 2 + (grid.node.y_coor - y) ** 2)
    return np.argmin(dist)


def connect_new_consumer(
    grid: ExtendedGrid,
    new_consumer: ExtendedNodeArray,
    new_consumer_load: SymLoadArray,
):
    closest_node_idx = find_closest_node(
        grid=grid,
        x=new_consumer.x_coor[0],
        y=new_consumer.y_coor[0],
    )
    closest_node = grid.node[closest_node_idx]

    grid.append(new_consumer)
    new_consumer_load.node = new_consumer.id
    grid.append(new_consumer_load)

    dist = np.sqrt((closest_node.x_coor - new_consumer.x_coor) ** 2 + (closest_node.y_coor - new_consumer.y_coor) ** 2)

    new_line = ExtendedLineArray(
        from_node=[closest_node.id],
        to_node=[new_consumer.id],
        from_status=[1],
        to_status=[1],
        r1=[R_PER_KM * dist / 1_000],
        x1=[X_PER_KM * dist / 1_000],
        c1=[0],
        tan1=[0],
        i_n=[200],
    )
    grid.append(new_line)


def update_grid(grid: ExtendedGrid):
    # Set the new feeder ids
    grid.set_feeder_ids()

    # Update the loadflow
    core_interface = PowerGridModelInterface(grid=grid)

    core_interface.create_input_from_grid()
    core_interface.calculate_power_flow()
    core_interface.update_grid()
connect_new_consumer(grid, new_consumer, new_consumer_load)
update_grid(grid)

We can inspect the results

  • The grid has been extended (graph and arrays)

  • Load values have been updated on node and line arrays

  • The feeder ids have been updated for the new consumer

print(grid.node)
 id | u_rated | node_type | feeder_branch_id | feeder_node_id |     u     |  x_coor |  y_coor 
 1  | 10500.0 |     0     |        64        |       61       |10472.048..|348.807..|409.192..
 2  | 10500.0 |     0     |        64        |       61       |10495.233..|428.882..|231.337..
 3  | 10500.0 |     0     |        63        |       61       |10422.156..|405.100..|120.757..
 4  | 10500.0 |     0     |        78        |       61       |10501.497..|140.842..|143.887..
 5  | 10500.0 |     0     |        63        |       61       |10449.524..|273.011..|353.357..
                                      (..12 hidden rows..)                                     
 18 | 10500.0 |     0     |        63        |       61       |10446.029..|352.625..|126.100..
 19 | 10500.0 |     0     |        78        |       61       |10521.244..|451.644..|398.691..
 20 | 10500.0 |     0     |        64        |       61       |10503.398..|460.310..|164.372..
 61 | 10500.0 |     1     |   -2147483648    |  -2147483648   |10499.557..|442.405..|371.468..
 93 | 10500.0 |     0     |        63        |       61       |10482.505..|  300.0  |  300.0  
print(grid.line)
 id | from_node | to_node | from_status | to_status | feeder_branch_id | feeder_node_id | is_feeder |   r1  |   x1  |  c1 | tan1 |   i_n    | i_from 
 63 |     61    |    17   |      1      |     1     |        63        |       61       |    True   |0.110..|0.013..| 0.0 | 0.0  |103.9613..|88.516..
 64 |     61    |    13   |      1      |     1     |        64        |       61       |    True   |0.325..|0.015..| 0.0 | 0.0  |100.4538..|13.217..
 65 |     17    |    5    |      1      |     1     |        63        |       61       |   False   |0.657..|2.575..| 0.0 | 0.0  |1311.550..|29.962..
 66 |     13    |    11   |      1      |     1     |        64        |       61       |   False   |0.213..|0.016..| 0.0 | 0.0  |114.4995..|53.125..
 67 |     5     |    18   |      1      |     1     |        63        |       61       |   False   |0.061..|0.029..| 0.0 | 0.0  |170.8020..|30.276..
                                                                 (..21 hidden rows..)                                                                 
 89 |     1     |    18   |      1      |     0     |   -2147483648    |  -2147483648   |   False   |0.341..|0.012..| 0.0 | 0.0  |221.8579..|  0.0   
 90 |     18    |    2    |      1      |     0     |   -2147483648    |  -2147483648   |   False   |0.122..|0.006..| 0.0 | 0.0  |387.5294..|  0.0   
 91 |     61    |    17   |      1      |     0     |   -2147483648    |  -2147483648   |    True   |0.210..|0.001..| 0.0 | 0.0  |267.6662..|  0.0   
 92 |     3     |    11   |      1      |     0     |   -2147483648    |  -2147483648   |   False   |0.030..|0.050..| 0.0 | 0.0  |272.8941..|  0.0   
 95 |     17    |    93   |      1      |     1     |        63        |       61       |   False   |0.000..|0.000..| 0.0 | 0.0  |  200.0   |56.168..
print(f"Overloaded nodes: {grid.node[grid.node.is_overloaded].id}")
print(f"Overloaded lines: {grid.line[grid.line.is_overloaded].id}")
Overloaded nodes: []
Overloaded lines: []

Now simulate more consumers being added, as to see how this will lead to overloads

for _ in range(10):
    new_consumer, new_consumer_load = create_new_consumer_arrays(
        10_500, np.random.uniform(0, 500), np.random.uniform(0, 500), 1_000_000, 200_000
    )
    connect_new_consumer(grid, new_consumer, new_consumer_load)
update_grid(grid)

print(f"Overloaded nodes: {grid.node[grid.node.is_overloaded].id}")
print(f"Overloaded lines: {grid.line[grid.line.is_overloaded].id}")
Overloaded nodes: []
Overloaded lines: [63 64 66 67 70]