"""
Simple pandapower (2.7.0) interface
"""
import logging
import numpy as np
from functools import wraps
from andes.shared import pd, rad2deg, deg2rad
from andes.shared import pandapower as pp
logger = logging.getLogger(__name__)
[docs]def require_pandapower(f):
"""
Decorator for functions that require pandapower.
"""
@wraps(f)
def wrapper(*args, **kwds):
try:
getattr(pp, '__version__')
except AttributeError:
raise ModuleNotFoundError("pandapower needs to be manually installed.")
return f(*args, **kwds)
return wrapper
[docs]def build_group_table(ssa, group, columns, mdl_name=[]):
"""
Build the table for devices in a group in an ADNES System.
Parameters
----------
ssa : andes.system.System
The ADNES system to build the table
group : string
The ADNES group
columns : list of string
The common columns of a group that to be included in the table.
mdl_name : list of string
The list of models that to be included in the table. Default as all models.
Returns
-------
DataFrame
The output Dataframe contains the columns from the device
"""
group_df = pd.DataFrame(columns=columns)
group = getattr(ssa, group)
if not mdl_name:
mdl_dict = getattr(group, 'models')
for key in mdl_dict:
mdl = getattr(ssa, key)
group_df = pd.concat([group_df, mdl.as_df()[columns]], axis=0)
else:
for key in mdl_name:
mdl = getattr(ssa, key)
group_df = pd.concat([group_df, mdl.as_df()[columns]], axis=0)
return group_df.reset_index(drop=True)
[docs]def make_link_table(ssa):
"""
Build the link table for generators and generator controllers in an ADNES
System, including ``SynGen`` and ``DG`` for now.
Parameters
----------
ssa : andes.system.System
The ADNES system to link
Returns
-------
DataFrame
Each column in the output Dataframe contains the ``idx`` of linked
``StaticGen``, ``Bus``, ``DG``, ``RenGen``, ``RenExciter``, ``SynGen``,
``Exciter``, and ``TurbineGov``, ``gammap``, ``gammaq``.
"""
# build StaticGen df
ssa_stg = build_group_table(ssa, 'StaticGen', ['u', 'name', 'idx', 'bus'])
# build TurbineGov df
ssa_gov = build_group_table(ssa, 'TurbineGov', ['idx', 'syn'])
# build Exciter df
ssa_exc = build_group_table(ssa, 'Exciter', ['idx', 'syn'])
# build SynGen df
ssa_syg = build_group_table(ssa, 'SynGen', ['idx', 'bus', 'gen', 'gammap', 'gammaq'], ['GENCLS', 'GENROU'])
# build DG df
ssa_dg = build_group_table(ssa, 'DG', ['idx', 'bus', 'gen', 'gammap', 'gammaq'])
# build RenGen df
ssa_rg = build_group_table(ssa, 'RenGen', ['idx', 'bus', 'gen', 'gammap', 'gammaq'])
# build RenExciter df
ssa_rexc = build_group_table(ssa, 'RenExciter', ['idx', 'reg'])
# output
ssa_bus = ssa.Bus.as_df()[['name', 'idx']]
ssa_key = pd.merge(left=ssa_stg.rename(columns={'name': 'stg_name', 'idx': 'stg_idx',
'bus': 'bus_idx', 'u': 'stg_u'}),
right=ssa_bus.rename(columns={'name': 'bus_name', 'idx': 'bus_idx'}),
how='left', on='bus_idx')
ssa_syg = pd.merge(left=ssa_key, how='right', on='stg_idx',
right=ssa_syg.rename(columns={'idx': 'syg_idx', 'gen': 'stg_idx'}))
ssa_dg = pd.merge(left=ssa_key, how='right', on='stg_idx',
right=ssa_dg.rename(columns={'idx': 'dg_idx', 'gen': 'stg_idx'}))
ssa_rg = pd.merge(left=ssa_key, how='right', on='stg_idx',
right=ssa_rg.rename(columns={'idx': 'rg_idx', 'gen': 'stg_idx'}))
ssa_key0 = pd.merge(left=ssa_key, how='left', on='stg_idx',
right=ssa_syg[['stg_idx', 'syg_idx']])
ssa_key0 = pd.merge(left=ssa_key0, how='left', on='stg_idx',
right=ssa_dg[['stg_idx', 'dg_idx']])
ssa_key0 = pd.merge(left=ssa_key0, how='left', on='stg_idx',
right=ssa_rg[['stg_idx', 'rg_idx']])
ssa_key0.fillna(False, inplace=True)
ssa_key0['dyr'] = ssa_key0['syg_idx'].astype(
bool) + ssa_key0['dg_idx'].astype(bool) + ssa_key0['rg_idx'].astype(bool)
ssa_key0['dyr'] = 1 - ssa_key0['dyr'].astype(int)
ssa_key0['dyr'] = ssa_key0['dyr'].astype(bool)
ssa_dyr0 = ssa_key0[ssa_key0.dyr].drop(['dyr'], axis=1)
ssa_dyr0['gammap'] = 1
ssa_dyr0['gammaq'] = 1
ssa_key = pd.concat([ssa_syg, ssa_dg, ssa_rg, ssa_dyr0], axis=0)
ssa_key = pd.merge(left=ssa_key,
right=ssa_exc.rename(columns={'idx': 'exc_idx', 'syn': 'syg_idx'}),
how='left', on='syg_idx')
ssa_key = pd.merge(left=ssa_key,
right=ssa_gov.rename(columns={'idx': 'gov_idx', 'syn': 'syg_idx'}),
how='left', on='syg_idx')
ssa_key = pd.merge(left=ssa_key, how='left', on='rg_idx',
right=ssa_rexc.rename(columns={'idx': 'rexc_idx', 'reg': 'rg_idx'}))
cols = ['stg_name', 'stg_u', 'stg_idx', 'bus_idx', 'dg_idx', 'rg_idx', 'rexc_idx',
'syg_idx', 'exc_idx', 'gov_idx', 'bus_name', 'gammap', 'gammaq']
return ssa_key[cols].reset_index(drop=True)
[docs]@require_pandapower
def runopp_map(ssp, link_table, **kwargs):
"""
Run OPF in pandapower using ``pp.runopp()`` and map results back to ANDES
based on the link table.
Parameters
----------
ssp : pandapower network
The pandapower network
link_table : DataFrame
The link table of ADNES system
Returns
-------
DataFrame
The DataFrame contains the OPF results with columns ``p_mw``,
``q_mvar``, ``vm_pu`` in p.u., and the corresponding ``idx`` of
``StaticGen``, ``Exciter``, ``TurbineGov`` in ANDES.
Notes
-----
- The pandapower net and the ANDES system must have same base MVA.
- Multiple ``DG`` connected to the same ``StaticGen`` will be converted to one generator.
The power is dispatched to each ``DG`` by the power ratio ``gammap``
"""
try:
pp.runopp(ssp, **kwargs)
except Exception:
pp.rundcopp(ssp, **kwargs)
logger.warning("ACOPF failed, DCOPF is used instead.")
# take dispatch results from pp
ssp_gen = ssp.gen.rename(columns={'name': 'stg_idx'})
ssp_res = pd.concat([ssp_gen[['stg_idx']],
ssp.res_gen[['p_mw', 'q_mvar', 'vm_pu']]], axis=1)
ssp_res = pd.merge(left=ssp_res,
right=ssp_gen[['stg_idx', 'controllable']],
how='left', on='stg_idx')
ssp_res = pd.merge(left=ssp_res, right=link_table,
how='left', on='stg_idx')
ssp_res['p'] = ssp_res['p_mw'] * ssp_res['gammap'] / ssp.sn_mva
ssp_res['q'] = ssp_res['q_mvar'] * ssp_res['gammaq'] / ssp.sn_mva
col = ['stg_idx', 'p', 'q', 'vm_pu', 'bus_idx', 'controllable',
'dg_idx', 'rg_idx', 'syg_idx', 'gov_idx', 'exc_idx']
return ssp_res[col]
[docs]@require_pandapower
def add_gencost(ssp, gen_cost):
"""
Add cost function to converted pandapower net `ssp`.
The cost data follows the same format of pypower and matpower.
Now only poly_cost is supported.
Parameters
----------
ssp :
The pandapower net
gen_cost : array
generator cost data
"""
# check dimension
if gen_cost.shape[0] != ssp.gen.shape[0]:
print('Input cost function size does not match gen size.')
for num, uid in enumerate(ssp.gen.index):
if gen_cost[num, 0] == 2: # poly_cost
pp.create_poly_cost(net=ssp,
element=uid,
et='gen',
cp2_eur_per_mw2=gen_cost[num, 4],
cp1_eur_per_mw=gen_cost[num, 5],
cp0_eur=gen_cost[num, 6])
else:
# TODO: piecewise linear
continue
return True
def _to_pp_bus(ssp, ssa_bus):
"""Create bus in pandapower net"""
bus_cols = ['Vn', 'name', 'u', 'vmax', 'vmin', 'zone']
bus_df = ssa_bus[bus_cols]
bus_df.columns = ['vn_kv', 'name', 'in_service', 'max_vm_pu', 'min_vm_pu', 'zone']
bus_df['type'] = 'b'
bus_df['in_service'] = bus_df['in_service'].astype('bool')
bus_df.reset_index(inplace=True, drop=True)
setattr(ssp, 'bus', bus_df)
return ssp
def _to_pp_line(ssa, ssp, ssa_bus):
"""Create line in pandapower net"""
# TODO: 1) from- and to- sides `Y`; 2)`g`
omega = 2 * np.pi * ssp.f_hz
ssa_bus_slice = ssa.Bus.as_df()[['idx', 'Vn']].rename(columns={"idx": "bus1", "Vn": "Vb"})
ssa_line = ssa.Line.as_df().merge(ssa_bus_slice, on='bus1', how='left')
ssa_line['Zb'] = ssa_line["Vb"]**2 / ssa_line["Sn"]
ssa_line['Yb'] = ssa_line["Sn"] / ssa_line["Vb"]**2
ssa_line['R'] = ssa_line["r"] * ssa_line['Zb'] # ohm
ssa_line['X'] = ssa_line["x"] * ssa_line['Zb'] # ohm
ssa_line['C'] = ssa_line["b"] / ssa_line['Zb'] / omega * 1e9 # nF
ssa_line['G'] = ssa_line["g"] * ssa_line['Yb'] * 1e6 # mS
# default rate_a is 2000 MVA
ssa_line['rate_a'] = ssa_line['rate_a'].replace(0, 2000)
ssa_line['max_i_ka'] = ssa_line["rate_a"] / ssa_line['Vb'] / np.sqrt(3) # kA
ssa_bus1 = ssa_bus[['idx']]
ssa_bus1['from_bus'] = ssa_bus.index
ssa_bus1.rename(columns={'uid': 'from_bus', 'idx': 'bus1'}, inplace=True)
ssa_bus1 = ssa_bus1.reset_index(drop=True)
ssa_bus2 = ssa_bus1.copy()
ssa_bus2.rename(columns={'from_bus': 'to_bus', 'bus1': 'bus2'}, inplace=True)
ssa_line = ssa_line.merge(ssa_bus1, on='bus1', how='left')
ssa_line = ssa_line.merge(ssa_bus2, on='bus2', how='left')
ssa_line['name'] = _rename(ssa_line['name'])
line_cols = ['name', 'from_bus', 'to_bus', 'u', 'R', 'X', 'C', 'G', 'max_i_ka']
line_df = ssa_line[line_cols][ssa_line['trans'] == 0].reset_index(drop=True)
line_df['length_km'] = 1
line_df['type'] = 'ol'
line_df['parallel'] = 1
line_df['max_loading_percent'] = 100
line_df['df'] = 1
line_df['std_type'] = None
line_df.rename(columns={'R': 'r_ohm_per_km', 'X': 'x_ohm_per_km',
'C': 'c_nf_per_km', 'G': 'g_us_per_km',
'u': 'in_service'}, inplace=True)
line_df['in_service'] = line_df['in_service'].astype('bool')
setattr(ssp, 'line', line_df)
tf_df = ssa_line[ssa_line['trans'] == 1].reset_index(drop=True)
if tf_df.shape[0] > 1:
tf_df.rename(columns={'R': 'r_ohm_per_km', 'X': 'x_ohm_per_km',
'C': 'c_nf_per_km', 'G': 'g_us_per_km',
'u': 'in_service'}, inplace=True)
tf_df['in_service'] = tf_df['in_service'].astype('bool')
tf_df['hv_bus'] = tf_df[['from_bus', 'to_bus', 'Vn1', 'Vn2']].apply(
lambda x: x[0] if x[2] >= x[3] else x[1], axis=1)
tf_df['lv_bus'] = tf_df[['from_bus', 'to_bus', 'Vn1', 'Vn2']].apply(
lambda x: x[0] if x[2] < x[3] else x[1], axis=1)
tf_df[['hv_bus', 'lv_bus']] = tf_df[['hv_bus', 'lv_bus']].astype('int')
tf_df['vn_hv_kv'] = tf_df[['from_bus', 'to_bus', 'Vn1', 'Vn2']].apply(
lambda x: x[2] if x[2] >= x[3] else x[3], axis=1)
tf_df['vn_lv_kv'] = tf_df[['from_bus', 'to_bus', 'Vn1', 'Vn2']].apply(
lambda x: x[2] if x[2] < x[3] else x[3], axis=1)
tf_df['tap_side'] = tf_df[['Vn1', 'Vn2']].apply(lambda x: 'hv' if x[0] >= x[1] else 'lv', axis=1)
tf_df['zk'] = (tf_df['r'] ** 2 + tf_df['x'] ** 2) ** 0.5
tf_df['sn_mva'] = 99999
tf_df['vk_percent'] = np.sign(tf_df['x']) * tf_df['zk'] * tf_df['sn_mva'] * 100 / ssp.sn_mva
tf_df['vkr_percent'] = tf_df['r'] * tf_df['sn_mva'] * 100 / ssp.sn_mva
tf_df['max_loading_percent'] = 100
tf_df['pfe_kw'] = 0
tf_df['i0_percent'] = -tf_df['b'] * 100 * ssp.sn_mva / tf_df['sn_mva']
tf_df['shift_degree'] = tf_df['phi'] * rad2deg
tf_df['tap_step_percent'] = abs((tf_df['tap'] - 1) * 100)
tf_df['tap_pos'] = np.sign((tf_df['tap'] - 1) * 100)
tf_df['tap_neutral'] = 0
tf_df['parallel'] = 1
tf_df['oltc'] = False
tf_df['tap_phase_shifter'] = False
tf_df['tap_step_degree'] = np.NaN
tf_df['df'] = 1
tf_df['std_type'] = None
trafo_cols = ['name', 'hv_bus', 'lv_bus', 'sn_mva', 'vn_hv_kv',
'vn_lv_kv', 'vk_percent', 'vkr_percent', 'pfe_kw', 'i0_percent',
'shift_degree', 'tap_side', 'tap_neutral',
'tap_step_percent', 'tap_pos', 'in_service',
'max_loading_percent', 'parallel', 'tap_phase_shifter',
'tap_step_degree', 'df', 'std_type']
tf_df[trafo_cols].reset_index(drop=True)
setattr(ssp, 'trafo', tf_df[trafo_cols].reset_index(drop=True))
return ssp
def _to_pp_load(ssa, ssp, ssa_bus):
"""Create load in pandapower net"""
ssa_pq = ssa.PQ.as_df()
ssa_pq['p_mw'] = ssa_pq["p0"] * ssp.sn_mva
ssa_pq['q_mvar'] = ssa_pq["q0"] * ssp.sn_mva
ssa_pq['name'] = _rename(ssa_pq['name'])
pq_cols = ['name', 'u', 'p_mw', 'q_mvar', 'bus']
load_df = ssa_pq[pq_cols].reset_index(drop=True)
load_df.rename(columns={'u': 'in_service', 'bus': 'idx'}, inplace=True)
load_df['in_service'] = load_df['in_service'].astype('bool')
load_df = load_df.merge(ssa_bus[['idx', 'bus']], on='idx', how='left')
load_df['sn_mva'] = ssp.sn_mva
load_df['controllable'] = False
load_df['type'] = None
load_df['const_i_percent'] = 0
load_df['const_z_percent'] = 0
load_df['scaling'] = 1
load_df.drop(['idx'], axis=1, inplace=True)
setattr(ssp, 'load', load_df)
return ssp
def _to_pp_shunt(ssa, ssp, ssa_bus):
"""Create shunt in pandapower net"""
ssa_shunt = ssa.Shunt.as_df()
ssa_shunt['p_mw'] = ssa_shunt["g"] * ssp.sn_mva
ssa_shunt['q_mvar'] = ssa_shunt["b"] * (-1) * ssp.sn_mva
ssa_shunt['name'] = _rename(ssa_shunt['name'])
shunt_cols = ['name', 'u', 'p_mw', 'q_mvar', 'bus', 'Vn']
shunt_df = ssa_shunt[shunt_cols].reset_index(drop=True)
shunt_df.rename(inplace=True,
columns={'u': 'in_service', 'bus': 'idx',
'Vn': 'vn_kv'})
shunt_df['in_service'] = shunt_df['in_service'].astype('bool')
shunt_df = shunt_df.merge(ssa_bus[['idx', 'bus']], on='idx', how='left')
shunt_df['step'] = 1
shunt_df['max_step'] = 1
shunt_df.drop(['idx'], axis=1, inplace=True)
setattr(ssp, 'shunt', shunt_df)
return ssp
def _to_pp_gen_pre(ssa):
"""Create generator data in pandapower net"""
# build StaticGen df
stg_cols = ['idx', 'u', 'bus', 'v0', 'vmax', 'vmin']
stg_calc_cols = ['p0', 'q0', 'pmax', 'pmin', 'qmax', 'qmin']
ssa_stg = build_group_table(ssa, 'StaticGen', stg_cols + stg_calc_cols)
ssa_stg['name'] = ssa_stg.idx
ssa_stg.rename(inplace=True,
columns={"bus": "bus_idx",
"u": "stg_u",
"idx": "stg_idx"})
# retrieve Bus idx in pp net
ssa_busn = ssa.Bus.as_df().copy().reset_index()[["uid", "idx"]].rename(
columns={"uid": "pp_bus", "idx": "bus_idx"})
ssa_stg = pd.merge(ssa_stg, ssa_busn,
how="left", on="bus_idx")
return ssa_stg
def _to_pp_gen(ssa, ssp, ctrl=[]):
"""Create shunt in pandapower net"""
# TODO: Add RenGen
ssa_sg = _to_pp_gen_pre(ssa)
# assign slack bus
ssa_sg["slack"] = False
ssa_sg["slack"][ssa_sg["bus_idx"] == ssa.Slack.bus.v[0]] = True
# compute the actual value
stg_calc_cols = ['p0', 'q0', 'pmax', 'pmin', 'qmax', 'qmin']
ssa_sg[stg_calc_cols] = ssa_sg[stg_calc_cols].apply(lambda x: x * ssp.sn_mva)
if 'gammap' not in _to_pp_gen_pre(ssa).columns:
ssa_sg['gammap'] = 1
ssa_sg['gammaq'] = 1
else:
ssa_sg['p0'] = ssa_sg['p0'] * ssa_sg['gammap']
ssa_sg['q0'] = ssa_sg['q0'] * ssa_sg['gammaq']
# default controllable is determined by governor
if len(ctrl) > 0:
if len(ctrl) != len(ssa_sg):
raise ValueError("ctrl length does not match StaticGen length")
else:
ctrl = [1] * len(ssa_sg)
ssa_sg["ctrl"] = [bool(x) for x in ctrl]
ssa_sg['name'] = _rename(ssa_sg['name'])
gen_cols = ['name', 'slack', 'pp_bus', 'p0', 'v0', 'ctrl',
'stg_u', 'pmax', 'pmin', 'qmax', 'qmin']
gen_df = ssa_sg[gen_cols].copy()
gen_df.rename(inplace=True,
columns={'pp_bus': 'bus', 'p0': 'p_mw',
'v0': 'vm_pu', 'ctrl': 'controllable',
'stg_u': 'in_service', 'pmax': 'max_p_mw',
'pmin': 'min_p_mw', 'qmax': 'max_q_mvar',
'qmin': 'min_q_mvar'})
gen_df['in_service'] = gen_df['in_service'].astype('bool')
gen_df['controllable'] = gen_df['controllable'].astype('bool')
gen_df['slack_weight'] = gen_df['slack'].astype('float')
gen_df['scaling'] = 1
gen_df['sn_mva'] = ssp.sn_mva
setattr(ssp, 'gen', gen_df)
return ssp
[docs]@require_pandapower
def to_pandapower(ssa, ctrl=[], verify=True, tol=1e-6):
"""
Convert an ADNES system to a pandapower network for power flow studies.
Parameters
----------
ssa : andes.system.System
The ADNES system to be converted
ctrl : list
The controlability of generators. The length should be the same with the
number of ``StaticGen``.
If not given, controllability of generators will be assigned by default.
Example input: [1, 0, 1, ...]; ``PV`` first, then ``Slack``.
verify : bool
If True, the converted network will be verified with the source ANDES system
using AC power flow.
tol : float
The tolerance of error.
Returns
-------
pandapower.auxiliary.pandapowerNet
A pandapower net with the same bus, branch, gen, and load data as the
ANDES system
Notes
-----
Handling of the following parameters:
- Generator cost is not included in the conversion. Use ``add_gencost()``
to add cost data.
- By default, all generators in ``ssp`` are controllable unless user-defined controllability
is given
- The online status of generators are determined by the online status of ``StaticGen``
that connected to the ``SynGen`` or ``DG``
- ``ssp.gen.name`` is from ``ssa.StaticGen.idx``, which should be unique
"""
# create a PP network
ssp = pp.create_empty_network(f_hz=ssa.config.freq,
sn_mva=ssa.config.mva,
)
# build bus table
ssa_bus = ssa.Bus.as_df()
ssa_bus['name'] = _rename(ssa_bus['name'])
ssa_bus['bus'] = ssa_bus.index
# --- 1. convert buses ---
ssp = _to_pp_bus(ssp, ssa_bus)
# --- 2. convert Line ---
ssp = _to_pp_line(ssa, ssp, ssa_bus)
# --- 3. load ---
ssp = _to_pp_load(ssa, ssp, ssa_bus)
# --- 4. shunt ---
ssp = _to_pp_shunt(ssa, ssp, ssa_bus)
# --- 5. generator ---
ssp = _to_pp_gen(ssa, ssp, ctrl)
if verify:
_verify_pf(ssa, ssp, tol)
return ssp
def _verify_pf(ssa, ssp, tol=1e-6):
"""
Verify power flow results.
"""
ssa.PFlow.run()
pp.runpp(ssp)
pf_bus = ssa.Bus.as_df()[["name"]]
# ssa
pf_bus['v_andes'] = ssa.Bus.v.v
pf_bus['a_andes'] = ssa.Bus.a.v
# ssp
pf_bus['v_pp'] = ssp.res_bus['vm_pu']
pf_bus['a_pp'] = ssp.res_bus['va_degree'] * deg2rad
# align ssa angle with slcka bus angle
row_slack = np.argmin(np.abs(pf_bus['a_pp']))
pf_bus['a_andes'] = pf_bus['a_andes'] - pf_bus['a_andes'].iloc[row_slack]
pf_bus['v_diff'] = pf_bus['v_andes'] - pf_bus['v_pp']
pf_bus['a_diff'] = pf_bus['a_andes'] - pf_bus['a_pp']
if (np.max(np.abs(pf_bus['v_diff'])) < tol) and \
(np.max(np.abs(pf_bus['a_diff'])) < tol):
logger.info("Power flow results are consistent. Conversion is successful.")
return True
else:
logger.warning("Warning: Power flow results are inconsistent. Pleaes check!")
return False
def _rename(pds_in):
"""
Rename the duplicated elelemts in a pandas.Series.
"""
if pds_in.duplicated().any():
pds = pds_in.copy()
dp_index = pds.duplicated()[pds.duplicated()].index
pds.iloc[dp_index] = pds.iloc[dp_index] + ' ' + pds.iloc[dp_index].index.astype(str)
return pds
else:
return pds_in
# TODO: add test for make GSF
[docs]@require_pandapower
def make_GSF(ppn, verify=True, using_sparse_solver=False):
"""
Build the Generation Shift Factor matrix of a pandapower net.
Parameters
----------
ppn : pandapower.network.Network
Pandapower network
verify : bool
True to verify the GSF with that from DC power flow
using_sparse_solver : bool
True to use a sparse solver for pandapower maktPTDF
Returns
-------
np.ndarray
The GSF array
"""
from pandapower.pypower.makePTDF import makePTDF
from pandapower.pd2ppc import _pd2ppc
# --- run DCPF ---
pp.rundcpp(ppn)
# --- compute PTDF ---
_, ppci = _pd2ppc(ppn)
ptdf = makePTDF(ppci["baseMVA"], ppci["bus"], ppci["branch"],
using_sparse_solver=using_sparse_solver)
# --- get the gsf ---
line_size = ppn.line.shape[0]
gsf = ptdf[0:line_size, :]
if verify:
_verifyGSF(ppn, gsf)
return gsf
def _verifyGSF(ppn, gsf, tol=1e-4):
"""Verify the GSF with DCPF"""
# --- DCPF results ---
rl = pd.concat([ppn.res_line['p_from_mw'], ppn.line[['from_bus', 'to_bus']]], axis=1)
rp = _sumPF_ppn(ppn)
rl_c = np.array(np.matrix(gsf) * np.matrix(rp.ngen).T)
res_gap = rl.p_from_mw.values - rl_c.flatten()
if np.abs(res_gap).max() <= tol:
logger.info("GSF is consistent.")
else:
logger.warning("Warning: GSF is inconsistent. Pleaes check!")
def _sumPF_ppn(ppn):
"""Summarize PF results of a pandapower net"""
rg = pd.DataFrame()
rg['gen'] = ppn.res_gen['p_mw']
rg['bus'] = ppn.gen['bus']
rg = rg.groupby('bus').sum()
rg.reset_index(inplace=True)
rd = pd.DataFrame()
rd['demand'] = ppn.res_load['p_mw']
rd['bus'] = ppn.load['bus']
rd = rd.groupby('bus').sum()
rd.reset_index(inplace=True)
rp = pd.DataFrame()
rp['bus'] = ppn.bus.index
rp = pd.merge(rp, rg, how='left', on='bus')
rp = pd.merge(rp, rd, how='left', on='bus')
rp.fillna(0, inplace=True)
rp['ngen'] = rp['gen'] - rp['demand']
return rp