Source code for edisgo.tools.powermodels_io

import math

import numpy as np
import pandas as pd
import pypsa

from pypower.idx_brch import *  # noqa: F403
from pypower.idx_bus import *  # noqa: F403
from pypower.idx_cost import *  # noqa: F403
from pypower.idx_gen import *  # noqa: F403


[docs] def to_powermodels(pypsa_net): """ Convert pypsa network to network dictionary format, using the pypower structure as an intermediate steps powermodels network dictionary: https://lanl-ansi.github.io/PowerModels.jl/stable/network-data/ pypower caseformat: https://github.com/rwl/PYPOWER/blob/master/pypower/caseformat.py :param pypsa_net: :return: """ # calculate per unit values pypsa.pf.calculate_dependent_values(pypsa_net) # convert pypsa network to pypower datastructure ppc, loads_t, gens_t = pypsa2ppc(pypsa_net) # conver pypower datastructure to powermodels network dictionary pm = ppc2pm(ppc, pypsa_net) return pm, loads_t, gens_t
[docs] def convert_storage_series(timeseries): if len(timeseries) == 0: return {} else: storage = {"time_horizon": len(timeseries), "storage_data": {}} for i, v in enumerate(timeseries.values): storage["storage_data"][i + 1] = {"p_req": v} return storage
# FIXME: Static storage data is exported from the eDisGo network rather than the PyPSA # network as the capacity of network doesn't seem to be available there. For # consistency with the rest of the conversion, it should be converted from PyPSA as # well. # TODO: This will (probably) not work if there are multiple storage units connected to # the same bus.
[docs] def add_storage_from_edisgo(edisgo_obj, psa_net, pm_dict): """ Read static storage data (position and capacity) from eDisGo and export to Powermodels dict """ # Drop values that are not available storage = pd.DataFrame(edisgo_obj.topology.storage_units_df[["bus", "p_nom"]]) # Rename storage parameters to PowerModels naming convention storage.columns = ["storage_bus", "energy_rating"] # Fill in missing values so that PowerModels doesn't complain # TODO: Get (some of) these from eDisGo/PyPSA as well storage["energy"] = 0.0 storage["charge_rating"] = 1.0 storage["discharge_rating"] = 1.0 storage["charge_efficiency"] = 0.9487 storage["discharge_efficiency"] = 0.9487 storage["ps"] = 0.0 storage["qs"] = 0.0 storage["qmin"] = 0.0 storage["qmax"] = 0.0 storage["r"] = 0.0 storage["x"] = 0.0 storage["p_loss"] = 0.0 storage["q_loss"] = 0.0 storage["standby_loss"] = 0.0 storage["status"] = 1 # Get Bus indices from PyPSA net storage["storage_bus"] = [ psa_net.buses.index.get_loc(bus) + 1 for bus in storage["storage_bus"] ] storage.index = [i + 1 for i in range(len(storage))] # Add dedicated 'index' column because PowerModels likes it storage["index"] = storage.index.to_list() # Add to PowerModels statics Dict pm_dict["storage"] = storage.to_dict(orient="index")
[docs] def pypsa2ppc(psa_net): """Converter from pypsa data structure to pypower data structure adapted from pandapower's pd2ppc converter https://github.com/e2nIEE/pandapower/blob/911f300a96ee0ac062d82f7684083168ff052586/pandapower/pd2ppc.py """ # build static pypower structure ppc = _init_ppc() ppc["name"] = psa_net.name _build_bus(psa_net, ppc) _build_gen(psa_net, ppc) _build_branch(psa_net, ppc) _build_transformers(psa_net, ppc) _build_load(psa_net, ppc) # TODO STORAGE UNITS _build_storage_units(psa_net, ppc) # built dictionaries if timeseries is used time_horizon = len(psa_net.loads_t["p_set"]) try: load_dict = _build_load_dict(psa_net, ppc) print( "Dictionary for load timeseries of timehorizon {} created".format( time_horizon ) ) except IndexError: print("No load timeseries. Create empty dicts for timeseries of load") load_dict = dict() try: gen_dict = _build_generator_dict(psa_net, ppc) print( "Dictionary for generator timeseries of timehorizon {} created".format( time_horizon ) ) except IndexError: print( "no generator timeseries Create empty dicts " "for timeseries of load and generation " ) gen_dict = dict() return ppc, load_dict, gen_dict
[docs] def ppc2pm(ppc, psa_net): # pragma: no cover """ converter from pypower datastructure to powermodels dictionary, adapted from pandapower to powermodels converter: https://github.com/e2nIEE/pandapower/blob/develop/pandapower/converter/pandamodels/to_pm.py :param ppc: :return: """ pm = { "gen": dict(), "branch": dict(), "bus": dict(), "dcline": dict(), "load": dict(), "storage": dict(), "baseMVA": ppc["baseMVA"], "source_version": "2.0.0", "shunt": dict(), "sourcetype": "matpower", "per_unit": True, "name": ppc["name"], } load_idx = 1 shunt_idx = 1 for row in ppc["bus"]: bus = dict() idx = int(row[BUS_I]) + 1 # noqa: F405 bus["index"] = idx bus["bus_i"] = idx bus["zone"] = int(row[ZONE]) # noqa: F405 bus["bus_type"] = int(row[BUS_TYPE]) # noqa: F405 bus["vmax"] = row[VMAX] # noqa: F405 bus["vmin"] = row[VMIN] # noqa: F405 bus["va"] = row[VA] # noqa: F405 bus["vm"] = row[VM] # noqa: F405 bus["base_kv"] = row[BASE_KV] # noqa: F405 pd = row[PD] # noqa: F405 qd = row[QD] # noqa: F405 if pd != 0 or qd != 0: pm["load"][str(load_idx)] = { "pd": pd, "qd": qd, "load_bus": idx, "status": True, "index": load_idx, } load_idx += 1 bs = row[BS] # noqa: F405 gs = row[GS] # noqa: F405 if pd != 0 or qd != 0: pm["shunt"][str(shunt_idx)] = { "gs": gs, "bs": bs, "shunt_bus": idx, "status": True, "index": shunt_idx, } shunt_idx += 1 pm["bus"][str(idx)] = bus n_lines = len(ppc["branch"]) for idx, row in enumerate(ppc["branch"], start=1): branch = dict() branch["index"] = idx branch["transformer"] = idx > n_lines branch["br_r"] = row[BR_R].real # noqa: F405 branch["br_x"] = row[BR_X].real # noqa: F405 branch["g_fr"] = -row[BR_B].imag / 2.0 # noqa: F405 branch["g_to"] = -row[BR_B].imag / 2.0 # noqa: F405 branch["b_fr"] = row[BR_B].real / 2.0 # noqa: F405 branch["b_to"] = row[BR_B].real / 2.0 # noqa: F405 branch["rate_a"] = ( row[RATE_A].real if row[RATE_A] > 0 else row[RATE_B].real # noqa: F405 ) branch["rate_b"] = row[RATE_B].real # noqa: F405 branch["rate_c"] = row[RATE_C].real # noqa: F405 branch["f_bus"] = int(row[F_BUS].real) + 1 # noqa: F405 branch["t_bus"] = int(row[T_BUS].real) + 1 # noqa: F405 branch["br_status"] = int(row[BR_STATUS].real) # noqa: F405 branch["angmin"] = row[ANGMIN].real # noqa: F405 branch["angmax"] = row[ANGMAX].real # noqa: F405 branch["tap"] = row[TAP].real # noqa: F405 branch["shift"] = math.radians(row[SHIFT].real) # noqa: F405 pm["branch"][str(idx)] = branch for idx, row in enumerate(ppc["gen"], start=1): gen = dict() gen["pg"] = row[PG] # noqa: F405 gen["qg"] = row[QG] # noqa: F405 gen["gen_bus"] = int(row[GEN_BUS]) + 1 # noqa: F405 gen["vg"] = row[VG] # noqa: F405 gen["qmax"] = row[QMAX] # noqa: F405 gen["gen_status"] = int(row[GEN_STATUS]) # noqa: F405 gen["qmin"] = row[QMIN] # noqa: F405 gen["pmin"] = row[PMIN] # noqa: F405 gen["pmax"] = row[PMAX] # noqa: F405 gen["index"] = idx pm["gen"][str(idx)] = gen # TODO add attribute "fluctuating" to generators from psa_net, maybe move to ppc # first # is_fluctuating = [ # int("fluctuating" in index.lower()) for index in psa_net.generators.index # ] # for idx, row in enumerate(is_fluctuating, start=1): # pm["gen"][str(idx)]["fluctuating"] = row for idx, row in enumerate(psa_net.generators["control"], start=1): pm["gen"][str(idx)]["gen_slack"] = (row == "Slack") * 1 for idx, row in enumerate(psa_net.generators["fluctuating"], start=1): # convert boolean to 0 and 1, check if row is nan, e.g. slack bus pm["gen"][str(idx)]["fluctuating"] = (not (math.isnan(row)) and row) * 1 if len(ppc["gencost"]) > len(ppc["gen"]): ppc["gencost"] = ppc["gencost"][: ppc["gen"].shape[0], :] for idx, row in enumerate(ppc["gencost"], start=1): gen = pm["gen"][str(idx)] gen["model"] = int(row[MODEL]) # noqa: F405 if gen["model"] == 1: gen["ncost"] = int(row[NCOST]) # noqa: F405 gen["cost"] = row[COST : COST + gen["ncost"] * 2].tolist() # noqa: F405 elif gen["model"] == 2: gen["ncost"] = int(row[NCOST]) # noqa: F405 gen["cost"] = [0] * 3 costs = row[COST:] # noqa: F405 if len(costs) > 3: print(costs) raise ValueError("Maximum quadratic cost function allowed") gen["cost"][-len(costs) :] = costs if len(ppc["branchcost"]) > len(ppc["branch"]): ppc["branchcost"] = ppc["branchcost"][: ppc["branch"].shape[0], :] for idx, row in enumerate(ppc["branchcost"], start=1): ncost = int(row[0]) branch = pm["branch"][str(idx)] branch["ncost"] = ncost branch["cost"] = [0] * ncost costs = row[1:] branch["cost"][-len(costs) :] = costs # TODO STORAGE UNITS! return pm
def _init_ppc(): # init empty ppc ppc = { "name": "name", "baseMVA": 1, "version": 2, "bus": np.array([], dtype=float), "branch": np.array([], dtype=np.complex128), "gen": np.array([], dtype=float), "gencost": np.array([], dtype=float), "branchcost": np.array([], dtype=float), "internal": { "Ybus": np.array([], dtype=np.complex128), "Yf": np.array([], dtype=np.complex128), "Yt": np.array([], dtype=np.complex128), "branch_is": np.array([], dtype=bool), "gen_is": np.array([], dtype=bool), "DLF": np.array([], dtype=np.complex128), "buses_ord_bfs_nets": np.array([], dtype=float), }, } return ppc def _build_bus(psa_net, ppc): n_bus = len(psa_net.buses.index) print("build {} buses".format(n_bus)) col_names = ( "index", "type", "Pd", "Qd", "Gs", "Bs", "area", "v_mag_pu_set", "v_ang_set", "v_nom", "zone", "v_mag_pu_max", "v_mag_pu_min".split(", "), ) bus_cols = len(col_names) ppc["bus"] = np.zeros(shape=(n_bus, bus_cols), dtype=float) ppc["bus"][:, :bus_cols] = np.array([0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1.05, 0.95]) ppc["bus"][:, BUS_I] = np.arange(n_bus) # noqa: F405 bus_types = ["PQ", "PV", "Slack", "None"] bus_types_int = np.array( [bus_types.index(b_type) + 1 for b_type in psa_net.buses["control"].values], dtype=int, ) ppc["bus"][:, BUS_TYPE] = bus_types_int # noqa: F405 ppc["bus"][:, BASE_KV] = psa_net.buses["v_nom"].values # noqa: F405 # for edisgo scenario voltage bounds defined for load and feed-in case with # 0.985<= v <= 1.05 bounds have to be at least in that range, only accept stronger # bounds if given ppc["bus"][:, VMAX] = [ # noqa: F405 min(val, 1.05) for val in psa_net.buses["v_mag_pu_max"].values ] ppc["bus"][:, VMIN] = [ # noqa: F405 max(val, 0.985) for val in psa_net.buses["v_mag_pu_min"].values ] return def _build_gen(psa_net, ppc): n_gen = psa_net.generators.shape[0] gen_cols = 21 # "bus, p_set, q_set, q_max, q_min, v_set_pu, mva_base, status, p_nom, p_min, Pc1, # Pc2, Qc1min, Qc1max, Qc2min, Qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf ppc["gen"] = np.zeros(shape=(n_gen, gen_cols), dtype=float) # get bus indices for generators bus_indices = np.array( [ psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.generators["bus"] ] ) print( "build {} generators, distributed on {} buses".format( n_gen, len(np.unique(bus_indices)) ) ) ppc["gen"][:, GEN_BUS] = bus_indices # noqa: F405 # adjust bus types # bus_types = ["PQ", "PV", "Slack", "None"] # gen_types = np.array( # [ # bus_types.index(gen_type) + 1 # for gen_type in psa_net.generators["control"].values # ], # dtype=int, # ) # ppc["bus"][bus_indices,BUS_TYPE] = gen_types # set setpoint of pg and qg ppc["gen"][:, PG] = psa_net.generators["p_set"].values # noqa: F405 ppc["gen"][:, QG] = psa_net.generators["q_set"].values # noqa: F405 ppc["gen"][:, MBASE] = 1.0 # noqa: F405 ppc["gen"][:, GEN_STATUS] = 1.0 # noqa: F405 ppc["gen"][:, PMAX] = psa_net.generators["p_nom"].values # noqa: F405 ppc["gen"][:, PMIN] = 0 # noqa: F405 # TODO SET QMAX AND QMIN! e.g.: cos(phi) value from config # ppc["gen"][:,QMAX] = 0 # ppc["gen"][:, QMIN] = 0 # build field for generator costs # 2 startup shutdown n c(n-1) ... c0 # for quadratic cost function n=3--> c2,c1,c0 lead to 7 columns cost_cols = 7 ppc["gencost"] = np.zeros(shape=(n_gen, cost_cols), dtype=float) # polynomial cost function ppc["gencost"][:, MODEL] = POLYNOMIAL # noqa: F405 ppc["gencost"][:, STARTUP] = psa_net.generators[ # noqa: F405 "start_up_cost" ].values ppc["gencost"][:, SHUTDOWN] = psa_net.generators[ # noqa: F405 "shut_down_cost" ].values # quadratic cost function has 3 cost coefficients ppc["gencost"][:, NCOST] = 3 # noqa: F405 ppc["gencost"][:, COST] = 0.0 # noqa: F405 ppc["gencost"][:, COST + 1] = psa_net.generators[ # noqa: F405 "marginal_cost" ].values ppc["gencost"][:, COST + 2] = 0.0 # noqa: F405 return def _build_branch(psa_net, ppc): n_branch = len(psa_net.lines.index) print("build {} lines".format(n_branch)) col_names = [ "fbus", "tbus", "r", "x", "b", "rateA", "rateB", "rateC", "ratio", "angle", "status", "angmin", "angmax", ] branch_cols = len(col_names) ppc["branch"] = np.zeros(shape=(n_branch, branch_cols), dtype=float) from_bus = np.array( [psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.lines["bus0"]] ) to_bus = np.array( [psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.lines["bus1"]] ) ppc["branch"][:, F_BUS] = from_bus # noqa: F405 ppc["branch"][:, T_BUS] = to_bus # noqa: F405 ppc["branch"][:, BR_R] = psa_net.lines["r_pu"].values # noqa: F405 ppc["branch"][:, BR_X] = psa_net.lines["x_pu"].values # noqa: F405 ppc["branch"][:, BR_B] = psa_net.lines["b_pu"].values # noqa: F405 ppc["branch"][:, RATE_A] = psa_net.lines["s_nom"].values # noqa: F405 ppc["branch"][:, RATE_B] = 250 # Default values # noqa: F405 ppc["branch"][:, RATE_C] = 250 # Default values # noqa: F405 ppc["branch"][:, TAP] = 0.0 # noqa: F405 ppc["branch"][:, SHIFT] = 0.0 # noqa: F405 ppc["branch"][:, BR_STATUS] = 1.0 # noqa: F405 ppc["branch"][:, ANGMIN] = -360 # noqa: F405 ppc["branch"][:, ANGMAX] = 360 # noqa: F405 # TODO BRANCHCOSTS! # check which branch costs are given in psa_net, ncost = sum( (colName in psa_net.lines.columns) * 1 for colName in ["costs_earthworks", "costs_cable"] ) if ncost == 0: print("no branch costs are given in pypsa network") elif ncost == 1: if "costs_cable" not in psa_net.lines.columns: print( "costs for cables not in pypsa network, not possible to define cost" "function for network expansion" ) else: ppc["branchcost"] = np.zeros(shape=(n_branch, 2), dtype=float) ppc["branchcost"][:, 0] = ncost ppc["branchcost"][:, 1] = psa_net.lines["costs_cable"].values elif ncost == 2: ppc["branchcost"] = np.zeros(shape=(n_branch, 3), dtype=float) ppc["branchcost"][:, 0] = ncost ppc["branchcost"][:, 1] = psa_net.lines["costs_cable"].values ppc["branchcost"][:, 2] = psa_net.lines["costs_earthworks"].values return def _build_transformers(psa_net, ppc): n_transformers = len(psa_net.transformers.index) print("appending {} transformers".format(n_transformers)) col_names = [ "fbus", "tbus", "r", "x", "b", "rateA", "rateB", "rateC", "ratio", "angle", "status", "angmin", "angmax", ] transformers = np.zeros(shape=(n_transformers, len(col_names)), dtype=float) from_bus = np.array( [ psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.transformers["bus0"] ] ) to_bus = np.array( [ psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.transformers["bus1"] ] ) transformers[:, F_BUS] = from_bus # noqa: F405 transformers[:, T_BUS] = to_bus # noqa: F405 transformers[:, BR_R] = psa_net.transformers["r_pu"].values # noqa: F405 transformers[:, BR_X] = psa_net.transformers["x_pu"].values # noqa: F405 transformers[:, BR_B] = psa_net.transformers["b_pu"].values # noqa: F405 transformers[:, RATE_A] = psa_net.transformers["s_nom"].values # noqa: F405 transformers[:, RATE_B] = 250 # Default values # noqa: F405 transformers[:, RATE_C] = 250 # Default values # noqa: F405 transformers[:, TAP] = psa_net.transformers["tap_ratio"].values # noqa: F405 transformers[:, SHIFT] = psa_net.transformers["phase_shift"].values # noqa: F405 transformers[:, BR_STATUS] = 1.0 # noqa: F405 transformers[:, ANGMIN] = -360 # noqa: F405 transformers[:, ANGMAX] = 360 # noqa: F405 ppc["branch"] = np.append(ppc["branch"], transformers, axis=0) # add trafo costs to branch cost with same shape if len(ppc["branchcost"]) > 0: print("append transformer costs") ncost = ppc["branchcost"].shape[1] - 1 trafo_costs = np.zeros(shape=(n_transformers, ncost + 1), dtype=float) if hasattr(psa_net.transformers, "trafo_costs"): trafo_costs[:, 0] = ncost trafo_costs[:, 1] = psa_net.transformers["trafo_costs"].values print(trafo_costs) ppc["branchcost"] = np.append(ppc["branchcost"], trafo_costs, axis=0) return def _build_load(psa_net, ppc): n_load = psa_net.loads.shape[0] load_buses = np.array( [psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.loads["bus"]] ) print( "build {} loads, distributed on {} buses".format( n_load, len(np.unique(load_buses)) ) ) # USE LOAD DATA FROM psa_net.loads as static network data # set bool if loads contains a timeseries # istime = len(psa_net.loads_t["p_set"].values[0]) != 0 # istime = False # print("network has timeseries for load: {}".format(istime)) for load_idx, bus_idx in enumerate(load_buses): # if istime: # # if timeseries take maximal value of load_bus for static information of # # the network # p_d = max(psa_net.loads_t["p_set"].values[:,load_idx]) # q_d = max(psa_net.loads_t["q_set"].values[:,load_idx]) # else: p_d = psa_net.loads["p_set"].values[load_idx] q_d = psa_net.loads["q_set"].values[load_idx] # increase demand at bus_idx by p_d and q_d from load_idx, as multiple loads # can be attached to single bus ppc["bus"][bus_idx, PD] += p_d # noqa: F405 ppc["bus"][bus_idx, QD] += q_d # noqa: F405 return def _build_storage_units(psa_net, ppc): print("storage units are not implemented yet") def _build_load_dict(psa_net, ppc): """ build load dict containing timeseries from psa_net.loads_t :param psa_net: pypsa network :param ppc: :return: load_dict: Dict() """ load_dict = {"load_data": dict()} load_buses = np.array( [psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.loads["bus"]] ) time_horizon = len(psa_net.loads_t["p_set"]) load_dict["time_horizon"] = time_horizon for t in range(time_horizon): load_dict["load_data"][str(t + 1)] = dict() for load_idx, bus_idx in enumerate(load_buses): # p_d = psa_net.loads_t["p_set"].values[t,load_idx] # qd = psa_net.loads_t["q_set"].values[t,load_idx] p_d = psa_net.loads_t["p_set"][psa_net.loads.index[load_idx]][t] qd = psa_net.loads_t["q_set"][psa_net.loads.index[load_idx]][t] load_dict["load_data"][str(t + 1)][str(load_idx + 1)] = { "pd": p_d, "qd": qd, "load_bus": int(bus_idx + 1), "status": True, "index": int(load_idx + 1), } return load_dict def _build_generator_dict(psa_net, ppc): generator_dict = {"gen_data": dict()} time_horizon = len(psa_net.generators_t["p_set"]) generator_dict["time_horizon"] = time_horizon # buses_with_gens = [psa_net.generators.loc[busname]["bus"] for busname in # psa_net.generators_t["p_set"].columns] # gen_buses = np.array([psa_net.buses.index.get_loc(bus_name) for bus_name in # buses_with_gens]) gen_buses = [ psa_net.buses.index.get_loc(bus_name) for bus_name in psa_net.generators["bus"] ] for t in range(time_horizon): generator_dict["gen_data"][str(t + 1)] = dict() for gen_idx, bus_idx in enumerate(gen_buses): # pg = psa_net.generators_t["p_set"].values[t, gen_idx] # qg = psa_net.generators_t["q_set"].values[t, gen_idx] pg = psa_net.generators_t["p_set"][psa_net.generators.index[gen_idx]][t] qg = psa_net.generators_t["q_set"][psa_net.generators.index[gen_idx]][t] # if no value is set, set pg and qg to large value, e.g. representing slack # TODO verify or find another solution not using "large" value if np.isnan(pg): pg = 99999 if np.isnan(qg): qg = 99999 generator_dict["gen_data"][str(t + 1)][str(gen_idx + 1)] = { "pg": pg, "qg": qg, "gen_bus": int(bus_idx) + 1, "status": True, "index": int(gen_idx + 1), } return generator_dict