Source code for andes.interop.gridcal

"""
Basic GridCal (4.6.1) interface, based on the pandapower interface written by Jinning Wang

Author: Josep Fanals (@JosepFanals)
"""

import logging
from functools import wraps

import numpy as np
import matplotlib

from andes.shared import GridCal_Engine as gc

matplotlib.use('agg')
logger = logging.getLogger(__name__)


[docs]def require_gridcal(f): """ Decorator for functions that require GridCal. """ @wraps(f) def wrapper(*args, **kwds): try: getattr(gc, '__name__') except AttributeError as exc: raise ModuleNotFoundError("GridCal needs to be installed manually.") from exc return f(*args, **kwds) return wrapper
def _to_gc_bus(ssp, ssa_bus): """ Define buses in GridCal's grid. """ dic_bus = {} for i in range(ssa_bus.n): bus = gc.Bus(name=ssa_bus.name.v[i], active=ssa_bus.u.v[i], vnom=ssa_bus.Vn.v[i], vmin=ssa_bus.vmin.v[i], vmax=ssa_bus.vmax.v[i], xpos=ssa_bus.xcoord.v[i], ypos=ssa_bus.ycoord.v[i], Vm0=ssa_bus.v0.v[i], Va0=ssa_bus.a0.v[i]) dic_bus[ssa_bus.idx.v[i]] = bus ssp.add_bus(bus) return ssp, dic_bus def _to_gc_branch(ssp, ssa_line, dic_bus): """ Define branches (lines and trafos) in GridCal's grid. """ rate = [xx if xx != 0.0 else 1.0 for xx in ssa_line.rate_a.v] # 1.0 is the default in GridCal for i in range(len(ssa_line)): if ssa_line.tap.v[i] != 1.0 or ssa_line.phi.v[i] != 0.0: # trafo trafo = gc.Transformer2W(bus_from=dic_bus[ssa_line.bus1.v[i]], bus_to=dic_bus[ssa_line.bus2.v[i]], name=ssa_line.name.v[i], active=ssa_line.u.v[i], r=ssa_line.r.v[i], x=ssa_line.x.v[i], b=ssa_line.b.v[i], g=ssa_line.g.v[i], rate=rate[i], tap=ssa_line.tap.v[i], shift_angle=ssa_line.phi.v[i]) ssp.add_transformer2w(trafo) else: # line line = gc.Line(bus_from=dic_bus[ssa_line.bus1.v[i]], bus_to=dic_bus[ssa_line.bus2.v[i]], name=ssa_line.name.v[i], active=ssa_line.u.v[i], r=ssa_line.r.v[i], x=ssa_line.x.v[i], b=ssa_line.b.v[i], rate=rate[i]) ssp.add_line(line) return ssp def _to_gc_load(ssp, ssa_load, dic_bus, sbase=1.0): """ Define loads in GridCal's grid. """ for i in range(len(ssa_load)): load = gc.Load(name=ssa_load.name.v[i], active=ssa_load.u.v[i], P=ssa_load.p0.v[i] * sbase, Q=ssa_load.q0.v[i] * sbase) ssp.add_load(dic_bus[ssa_load.bus.v[i]], load) return ssp def _to_gc_shunt(ssp, ssa_shunt, dic_bus, sbase=1.0): """ Define shunts in GridCal's grid. """ for i in range(len(ssa_shunt)): shunt = gc.Shunt(name=ssa_shunt.name.v[i], active=ssa_shunt.u.v[i], G=ssa_shunt.g.v[i] * sbase, B=ssa_shunt.b.v[i] * sbase) ssp.add_shunt(dic_bus[ssa_shunt.bus.v[i]], shunt) return ssp def _to_gc_generator(ssp, ssa_slack, ssa_pv, dic_bus, sbase=1.0): """ Define generators considering slack and PV buses. """ for i in range(len(ssa_slack)): sl_name = dic_bus[ssa_slack.bus.v[i]] b2_dict = ssp.get_bus_names() bus_id = b2_dict.index(sl_name.name) ssp.buses[bus_id].is_slack = True gen = gc.Generator(name=str(ssa_slack.SynGen.v[i]), active=ssa_slack.u.v[i], active_power=ssa_slack.p0.v[i] * sbase, power_factor=ssa_slack.p0.v[i] / np.sqrt((ssa_slack.p0.v[i]**2 + ssa_slack.q0.v[i]**2)), p_min=ssa_slack.pmin.v[i] * sbase, p_max=ssa_slack.pmax.v[i] * sbase, Qmin=ssa_slack.qmin.v[i] * sbase, Qmax=ssa_slack.qmax.v[i] * sbase, voltage_module=ssa_slack.v0.v[i], Snom=ssa_slack.Sn.v[i]) ssp.add_generator(dic_bus[ssa_slack.bus.v[i]], gen) for i in range(len(ssa_pv)): gen = gc.Generator(name=str(ssa_pv.SynGen.v[i]), active=ssa_pv.u.v[i], active_power=ssa_pv.p0.v[i] * sbase, power_factor=ssa_pv.p0.v[i] / np.sqrt((ssa_pv.p0.v[i]**2 + ssa_pv.q0.v[i]**2)), p_min=ssa_pv.pmin.v[i] * sbase, p_max=ssa_pv.pmax.v[i] * sbase, Qmin=ssa_pv.qmin.v[i] * sbase, Qmax=ssa_pv.qmax.v[i] * sbase, voltage_module=ssa_pv.v0.v[i], Snom=ssa_pv.Sn.v[i]) ssp.add_generator(dic_bus[ssa_pv.bus.v[i]], gen) return ssp
[docs]@require_gridcal def to_gridcal(ssa, verify=True, tol=1e-6): """ Convert an ANDES system to a GridCal grid. Parameters ---------- ssa : andes.system.System The ANDES system to be converted 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 when comparing power flow solutions. Returns ------- GridCal.Engine.Core.multi_circuit.MultiCircuit A GridCal net with the same bus, branch, gen, and load data as the ANDES system Notes ----- Handling of the following parameters: - 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 an empty GC grid sbase = ssa.config.mva ssp = gc.MultiCircuit(Sbase=sbase, fbase=ssa.config.freq, name=ssa.name) # 1. convert buses ssp, dic_bus = _to_gc_bus(ssp, ssa.Bus) # 2. convert branches ssp = _to_gc_branch(ssp, ssa.Line, dic_bus) # 3. convert loads ssp = _to_gc_load(ssp, ssa.PQ, dic_bus, sbase=sbase) # 4. convert shunts ssp = _to_gc_shunt(ssp, ssa.Shunt, dic_bus, sbase=sbase) # 5. convert generators (Slack and PV) ssp = _to_gc_generator(ssp, ssa.Slack, ssa.PV, dic_bus, sbase=sbase) if verify: _verify_pf(ssa, ssp, tol) return ssp
def _verify_pf(ssa, ssp, tol=1e-6): """ Verify power flow results. """ # ANDES ssa.PFlow.run() pf_bus = ssa.Bus.as_df()[["name"]] pf_bus['v_andes'] = ssa.Bus.v.v pf_bus['a_andes'] = ssa.Bus.a.v # GridCal options = gc.PowerFlowOptions(gc.SolverType.NR, verbose=False) pf = gc.PowerFlowDriver(ssp, options) pf.run() # GridCal assumes the slack has an angle of 0 always, correct it pf.results.voltage = pf.results.voltage * np.exp(1j * ssa.Slack.a0.v[0]) # Check vm_dif = pf_bus['v_andes'] - np.abs(pf.results.voltage) va_dif = pf_bus['a_andes'] - np.angle(pf.results.voltage) ret = False if (np.max(np.abs(vm_dif)) < tol) and (np.max(np.abs(va_dif)) < tol): logger.info("Power flow results are consistent. Conversion is successful.") ret = True else: logger.warning("Warning: Power flow results are inconsistent. Please check!") logger.warning(vm_dif) logger.warning(va_dif) return ret