Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Pandapower converter #786

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pypowsybl/network/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@
remove_feeder_bays
)
from .impl.perunit import (PerUnitView, per_unit_view)
from .impl.pandapower_converter import convert_from_pandapower
209 changes: 209 additions & 0 deletions pypowsybl/network/impl/pandapower_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@

import math
from importlib import util
from typing import Dict

import numpy as np
import pandas as pd
import pypowsybl._pypowsybl as _pp
from pandas import Series

from .network import Network
from .network_creation_util import create_empty


def convert_from_pandapower(n_pdp) -> Network:
if util.find_spec("pandapower") is None:
raise _pp.PyPowsyblError("pandapower is not installed")
else:
n = create_empty(n_pdp.name if n_pdp.name else 'network')

# create one giant substation
n.create_substations(id='s')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creating one giant substation will have negative affects on single line diagram from pypowsybl, however here n.create_voltage_levels(id=vl_id, substation_id=substation_id, topology_kind=topology_kind, nominal_v=nominal_v, substation_id is used but substation is not created?

create_buses(n, n_pdp)
create_loads(n, n_pdp)
slack_weight_by_gen_id = {}
create_generators(n, n_pdp, slack_weight_by_gen_id)
create_shunts(n, n_pdp)
create_lines(n, n_pdp)
create_transformers(n, n_pdp)

# create slack bus extension
slack_gen_ids = [key for key, value in slack_weight_by_gen_id.items() if value == 1.0]
generators = n.get_generators(attributes=['voltage_level_id'])
slack_gen = generators.loc[slack_gen_ids[0]]
n.create_extensions(extension_name='slackTerminal', element_id=slack_gen_ids[0], voltage_level_id=slack_gen['voltage_level_id'])

return n


def get_name(df: pd.DataFrame, name: str) -> pd.Series:
name = df[name]
replace_none = np.vectorize(lambda x: '' if x is None else x, otypes=[np.string_])
name_cleaned = replace_none(name)
return name_cleaned.astype(str)


def build_voltage_level_id(bus: Series):
return 'sub_' + bus


def build_bus_id(bus: Series):
return 'bus_' + bus


def build_injection_id(prefix, bus, index):
return "{}_{}_{}".format(prefix, bus, index) # because it is required by grid2op to build IDs like this is case of missing name


def generate_injection_id(df: pd.DataFrame, prefix: str) -> pd.Series:
return df.apply(lambda row: build_injection_id(prefix, row['bus'], row.name), axis=1)


def build_line_id(row, index):
from_bus = row['from_bus']
to_bus = row['to_bus']
return "{}_{}_{}".format(from_bus, to_bus, index) # because it is required by grid2op to build IDs like this is case of missing name


def generate_line_id(df: pd.DataFrame) -> pd.Series:
return df.apply(lambda row: build_line_id(row, row.name), axis=1)


def build_transformer_id(row, index, index_offset: int):
hv_bus = row['hv_bus']
lv_bus = row['lv_bus']
return "{}_{}_{}".format(hv_bus, lv_bus, index_offset + index) # because it is required by grid2op to build IDs like this is case of missing name


def generate_transformer_id(df: pd.DataFrame, index_offset: int) -> pd.Series:
return df.apply(lambda row: build_transformer_id(row, row.name, index_offset), axis=1)


def create_transformers(n, n_pdp):
if len(n_pdp.trafo) > 0:
bus = n_pdp.bus[['vn_kv']]
trafo_and_bus = n_pdp.trafo.merge(bus.rename(columns=lambda x: x + '_lv_bus'), left_on='lv_bus', right_index=True, how='inner')
id = generate_transformer_id(trafo_and_bus, len(n_pdp.line))
name = get_name(trafo_and_bus, 'name')
vl1_id = build_voltage_level_id(trafo_and_bus['hv_bus'].astype(str))
vl2_id = build_voltage_level_id(trafo_and_bus['lv_bus'].astype(str))
bus1_id = build_bus_id(trafo_and_bus['hv_bus'].astype(str))
bus2_id = build_bus_id(trafo_and_bus['lv_bus'].astype(str))
n_tap = np.where(~np.isnan(trafo_and_bus['tap_pos']) & ~np.isnan(trafo_and_bus['tap_neutral']) & ~np.isnan(trafo_and_bus['tap_step_percent']),
1.0 + (trafo_and_bus['tap_pos'] - trafo_and_bus['tap_neutral']) * trafo_and_bus['tap_step_percent'] / 100.0, 1.0)
rated_u1 = np.where(trafo_and_bus['tap_side'] == "hv", trafo_and_bus['vn_hv_kv'] * n_tap, trafo_and_bus['vn_hv_kv'])
rated_u2 = np.where(trafo_and_bus['tap_side'] == "lv", trafo_and_bus['vn_lv_kv'] * n_tap, trafo_and_bus['vn_lv_kv'])
c = n_pdp.sn_mva / n_pdp.trafo['sn_mva']
rk = trafo_and_bus['vkr_percent'] / 100 * c
zk = trafo_and_bus['vk_percent'] / 100 * c
xk = np.sqrt(zk ** 2 - rk ** 2)
ym = trafo_and_bus['i0_percent'] / 100
gm = trafo_and_bus['pfe_kw'] / (trafo_and_bus['sn_mva'] * 1000) / c
bm = - np.sqrt(ym ** 2 - gm ** 2)

zb_tr = (trafo_and_bus['vn_kv_lv_bus'] ** 2) / n_pdp.sn_mva
r = rk * zb_tr / trafo_and_bus['parallel']
x = xk * zb_tr / trafo_and_bus['parallel']
g = gm / zb_tr * trafo_and_bus['parallel']
b = bm / zb_tr * trafo_and_bus['parallel']

n.create_2_windings_transformers(id=id, name=name,
voltage_level1_id=vl1_id, bus1_id=bus1_id,
voltage_level2_id=vl2_id, bus2_id=bus2_id,
rated_u1=rated_u1, rated_u2=rated_u2,
r=r, x=x, g=g, b=b)


def create_lines(n, n_pdp):
if len(n_pdp.line) > 0:
id = generate_line_id(n_pdp.line)
name = get_name(n_pdp.line, 'name')
vl1_id = build_voltage_level_id(n_pdp.line['from_bus'].astype(str))
vl2_id = build_voltage_level_id(n_pdp.line['to_bus'].astype(str))
bus1_id = build_bus_id(n_pdp.line['from_bus'].astype(str))
bus2_id = build_bus_id(n_pdp.line['to_bus'].astype(str))
r = n_pdp.line['length_km'] * n_pdp.line['r_ohm_per_km'] / n_pdp.line['parallel']
x = n_pdp.line['length_km'] * n_pdp.line['x_ohm_per_km'] / n_pdp.line['parallel']
g = n_pdp.line['length_km'] * n_pdp.line['g_us_per_km'] * 1e-6 * n_pdp.line['parallel'] / 2
b = n_pdp.line['length_km'] * n_pdp.line['c_nf_per_km'] * 1e-9 * 2 * math.pi * n_pdp.f_hz * n_pdp.line['parallel'] / 2
n.create_lines(id=id, name=name, voltage_level1_id=vl1_id, bus1_id=bus1_id, voltage_level2_id=vl2_id,
bus2_id=bus2_id, r=r, x=x, g1=g, g2=g, b1=b, b2=b)


def create_shunts(n, n_pdp):
if len(n_pdp.shunt) > 0:
id = generate_injection_id(n_pdp.shunt, 'shunt')
name = get_name(n_pdp.shunt, 'name').tolist()
vl_id = build_voltage_level_id(n_pdp.shunt['bus'].astype(str)).tolist()
bus_id = build_bus_id(n_pdp.shunt['bus'].astype(str)).tolist()
model_type = ['LINEAR'] * len(n_pdp.shunt)
section_count = n_pdp.shunt['step'].tolist()
shunt_df = pd.DataFrame(data={
'name': name,
'voltage_level_id': vl_id,
'bus_id': bus_id,
'model_type': model_type,
'section_count': section_count
}, index=id)
g_per_section = (n_pdp.shunt['p_mw'] / (n_pdp.shunt['vn_kv'] ** 2)).tolist()
b_per_section = (n_pdp.shunt['q_mvar'] / (n_pdp.shunt['vn_kv'] ** 2)).tolist()
max_section_count = n_pdp.shunt['max_step'].tolist()
linear_model_df = pd.DataFrame(data={
'g_per_section': g_per_section,
'b_per_section': b_per_section,
'max_section_count': max_section_count,
}, index=id)
n.create_shunt_compensators(shunt_df=shunt_df, linear_model_df=linear_model_df)


def _create_generators(n, gen, bus, slack_weight_by_gen_id: Dict[str, float], ext_grid: bool):
if len(gen) > 0:
gen_and_bus = gen.merge(bus, left_on='bus', right_index=True, how='inner', suffixes=('', '_x'))
id = generate_injection_id(gen_and_bus, 'gen')
name = get_name(gen_and_bus, 'name')
vl_id = build_voltage_level_id(gen_and_bus['bus'].astype(str))
bus_id = build_bus_id(gen_and_bus['bus'].astype(str))
target_p = [0.0001] * len(gen_and_bus) if ext_grid else gen_and_bus['p_mw']
voltage_regulator_on = [True] * len(gen_and_bus)
target_v = gen_and_bus['vm_pu'] * gen_and_bus['vn_kv']
min_p = [0.0] * len(gen_and_bus) if ext_grid else gen_and_bus['min_p_mw']
max_p = [4999.0] * len(gen_and_bus) if ext_grid else gen_and_bus['max_p_mw']

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4999.0 multiplication should be somehow communicated or parametrable

for index, row in gen_and_bus.iterrows():
slack_weight_by_gen_id[build_injection_id('gen', row['bus'], index)] = row['slack_weight']

n.create_generators(id=id, name=name, voltage_level_id=vl_id, bus_id=bus_id, target_p=target_p,
voltage_regulator_on=voltage_regulator_on,
target_v=target_v, min_p=min_p, max_p=max_p)


def create_generators(n, n_pdp, slack_weight_by_gen_id):
_create_generators(n, n_pdp.gen, n_pdp.bus, slack_weight_by_gen_id, False)
_create_generators(n, n_pdp.ext_grid, n_pdp.bus, slack_weight_by_gen_id, True)


def create_loads(n, n_pdp):
if len(n_pdp.load) > 0:
id = generate_injection_id(n_pdp.load, 'load')
name = get_name(n_pdp.load, 'name')
vl_id = build_voltage_level_id(n_pdp.load['bus'].astype(str))
bus_id = build_bus_id(n_pdp.load['bus'].astype(str))
p0 = n_pdp.load['p_mw']
q0 = n_pdp.load['q_mvar']
n.create_loads(id=id, name=name, voltage_level_id=vl_id, bus_id=bus_id, p0=p0, q0=q0)


def create_buses(n, n_pdp):
if len(n_pdp.bus) > 0:
vl_id = build_voltage_level_id(n_pdp.bus.index.astype(str))
topology_kind = ['BUS_BREAKER'] * len(n_pdp.bus)
nominal_v = n_pdp.bus['vn_kv']
low_voltage_limit = n_pdp.bus['min_vm_pu'] * nominal_v if 'min_vm_pu' in n_pdp.bus.columns else None
high_voltage_limit = n_pdp.bus['max_vm_pu'] * nominal_v if 'max_vm_pu' in n_pdp.bus.columns else None
substation_id = ['s'] * len(n_pdp.bus)
# FIXME topology kind should have a default value
n.create_voltage_levels(id=vl_id, substation_id=substation_id, topology_kind=topology_kind, nominal_v=nominal_v,
low_voltage_limit=low_voltage_limit, high_voltage_limit=high_voltage_limit)
id = build_bus_id(n_pdp.bus.index.astype(str))
name = get_name(n_pdp.bus, 'name')
n.create_buses(id=id, name=name, voltage_level_id=vl_id)
17 changes: 9 additions & 8 deletions pypowsybl/utils/impl/dataframes.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ def _adapt_kwargs(metadata: List[_pp.SeriesMetadata], **kwargs: _Any) -> DataFra
columns = {}
expected_size = None
for key, value in kwargs.items():
col = _to_array(value)
size = col.shape[0]
if expected_size is None:
expected_size = size
elif size != expected_size:
raise ValueError(f'Network elements update: all arguments must have the same size, '
f'got size {size} for series {key}, expected {expected_size}')
columns[key] = col
if value is not None:
col = _to_array(value)
size = col.shape[0]
if expected_size is None:
expected_size = size
elif size != expected_size:
raise ValueError(f'Network elements update: all arguments must have the same size, '
f'got size {size} for series {key}, expected {expected_size}')
columns[key] = col

index = None
if len(index_columns) == 1:
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ networkx
matplotlib==3.9.0; python_version >= "3.9"
matplotlib; python_version <= "3.8"

# optional dependencies
pandapower

# documentation dependencies
sphinx==7.1.2
furo==2024.1.29
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ install_requires =
pandas>=2.0.3; python_version <= "3.8"
networkx

[options.extras_require]
extra = pandapower

[options.package_data]
pypowsybl: py.typed, *.pyi
72 changes: 72 additions & 0 deletions tests/test_pandapower.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pandapower as pdp
import pytest
from pytest import approx

import pypowsybl as pp
import logging

ABS = 0.001


@pytest.fixture(autouse=True)
def setup():
logging.basicConfig()
logging.getLogger('powsybl').setLevel(logging.DEBUG)


def run_and_compare(pdp_n, expected_bus_count: int):
pdp.runpp(pdp_n, numba=True, enforce_q_lims=False, distributed_slack=False, trafo_model="pi")
n = pp.network.convert_from_pandapower(pdp_n)
assert len(n.get_buses()) == expected_bus_count
param = pp.loadflow.Parameters(voltage_init_mode=pp.loadflow.VoltageInitMode.UNIFORM_VALUES,
transformer_voltage_control_on=False,
use_reactive_limits=False,
shunt_compensator_voltage_control_on=False,
phase_shifter_regulation_on=False,
distributed_slack=False)
results = pp.loadflow.run_ac(n, param)
assert pp.loadflow.ComponentStatus.CONVERGED == results[0].status
pdp_v = list(pdp_n.res_bus['vm_pu'] * pdp_n.bus['vn_kv'])
buses = n.get_buses()
v = list(buses['v_mag'])
print()
print(pdp_v)
print(v)
assert pdp_v == approx(v, abs=ABS)


def test_pandapower_case5():
run_and_compare(pdp.networks.case5(), 5)

def test_pandapower_case4gs():
run_and_compare(pdp.networks.case4gs(), 4)

def test_pandapower_case6ww():
run_and_compare(pdp.networks.case6ww(), 6)

def test_pandapower_case9():
run_and_compare(pdp.networks.case9(), 9)

def test_pandapower_case11_iwamoto():
run_and_compare(pdp.networks.case11_iwamoto(), 11)

def test_pandapower_case14():
run_and_compare(pdp.networks.case14(), 14)

def test_pandapower_case30():
run_and_compare(pdp.networks.case30(), 30)

def test_pandapower_case_ieee30():
run_and_compare(pdp.networks.case_ieee30(), 30)

def test_pandapower_case33bw():
run_and_compare(pdp.networks.case33bw(), 33)

def test_pandapower_case39():
run_and_compare(pdp.networks.case39(), 39)

def test_pandapower_panda_four_load_branch():
run_and_compare(pdp.networks.panda_four_load_branch(), 6)

def test_pandapower_four_loads_with_branches_out():
run_and_compare(pdp.networks.four_loads_with_branches_out(), 10)
Loading