Source code for hynet.data.import_

"""
Import of model data into the *hynet* data format.
"""

import logging
import os.path

import numpy as np
import scipy.io
import h5py

from hynet.types_ import (hynet_float_,
                          DBInfoKey,
                          BusType,
                          BranchType,
                          InjectorType,
                          EntityType)
from hynet.data.connection import connect, DBTransaction
from hynet.data.structure import (DBInfo,
                                  DBScenario,
                                  DBBus,
                                  DBBranch,
                                  DBConverter,
                                  DBInjector,
                                  DBCapabilityRegion,
                                  DBSamplePoint,
                                  DBShunt,
                                  DBScenarioLoad,
                                  DBScenarioInjector,
                                  DBScenarioInactivity)

_log = logging.getLogger(__name__)

MAT_FILE_EXTENSION = '.mat'


[docs]class BusConstants: """Constants for the MATPOWER bus data, see the MATPOWER manual.""" # Bus matrix column indices BUS_I = 0 # Bus number BUS_TYPE = 1 # Bus type PD = 2 # Real power demand (MW) QD = 3 # Reactive power demand (MVAr) GS = 4 # Shunt conductance as loss in MW at V = 1.0 p.u. BS = 5 # Shunt susceptance as injection in MVAr at V = 1.0 p.u. BUS_AREA = 6 # Area number VM = 7 # Voltage magnitude (p.u.) VA = 8 # Voltage angle (degrees) BASE_KV = 9 # Base voltage (kV) ZONE = 10 # (Loss) Zone VMAX = 11 # Maximum voltage magnitude (p.u.) VMIN = 12 # Minimum voltage magnitude (p.u.) # Bus types PQ = 1 # PQ-bus (fixed variables in power flow calculations) PV = 2 # PV-bus (fixed variables in power flow calculations) REF = 3 # Reference bus NONE = 4 # Isolated/inactive bus # OPF result column indices in the bus matrix LAM_P = 13 # Lagrange multiplier on real power balance LAM_Q = 14 # Lagrange multiplier on reactive power balance MU_VMAX = 15 # KKT multiplier on upper voltage limit MU_VMIN = 16 # KKT multiplier on lower voltage limit
[docs]class BranchConstants: """Constants for the MATPOWER branch data, see the MATPOWER manual.""" # Branch matrix column indices F_BUS = 0 # "From" bus number T_BUS = 1 # "To" bus number BR_R = 2 # Series resistance (p.u.) BR_X = 3 # Series reactance (p.u.) BR_B = 4 # Total line charging susceptance (p.u.) RATE_A = 5 # MVA rating A (long term rating) RATE_B = 6 # MVA rating B (short term rating) RATE_C = 7 # MVA rating C (emergency rating) TAP = 8 # Transformer ratio SHIFT = 9 # Transformer phase shift (degrees) BR_STATUS = 10 # Status (1 - in service, 0 - out of service) ANGMIN = 11 # Minimum angle difference (degrees) ANGMAX = 12 # Maximum angle difference (degrees) # OPF result column indices in the branch matrix PF = 13 # Real power injected at "from" bus end (MW) QF = 14 # Reactive power injected at "from" bus end (MVAr) PT = 15 # Real power injected at "to" bus end (MW) QT = 16 # Reactive power injected at "to" bus end (MVAr) MU_SF = 17 # KKT multiplier on MVA limit at "from" bus MU_ST = 18 # KKT multiplier on MVA limit at "to" bus MU_ANGMIN = 19 # KKT multiplier lower angle difference limit MU_ANGMAX = 20 # KKT multiplier upper angle difference limit
[docs]class GeneratorConstants: """Constants for the MATPOWER generator data, see the MATPOWER manual.""" # Generator matrix column indices GEN_BUS = 0 # Bus number PG = 1 # Real power output (MW) QG = 2 # Reactive power output (MVAr) QMAX = 3 # Maximum reactive power output (MVAr) QMIN = 4 # Minimum reactive power output (MVAr) VG = 5 # Voltage magnitude set point (p.u.) MBASE = 6 # Total MVA base of this machine GEN_STATUS = 7 # Status (1 - in service, 0 - out of service) PMAX = 8 # Maximum real power output (MW) PMIN = 9 # Minimum real power output (MW) PC1 = 10 # Lower real power output of PQ capability curve (MW) PC2 = 11 # Upper real power output of PQ capability curve (MW) QC1MIN = 12 # Minimum reactive power output at PC1 (MVAr) QC1MAX = 13 # Maximum reactive power output at PC1 (MVAr) QC2MIN = 14 # Minimum reactive power output at PC2 (MVAr) QC2MAX = 15 # Maximum reactive power output at PC2 (MVAr) RAMP_AGC = 16 # Ramp rate for load following/AGC (MW/min) RAMP_10 = 17 # Ramp rate for 10 minute reserves (MW) RAMP_30 = 18 # Ramp rate for 30 minute reserves (MW) RAMP_Q = 19 # Ramp rate for reactive power (2 sec timescale) (MVAr/min) APF = 20 # Area participation factor # OPF result column indices in the generator matrix MU_PMAX = 21 # KKT multiplier on upper active power upper limit MU_PMIN = 22 # KKT multiplier on lower active power upper limit MU_QMAX = 23 # KKT multiplier on upper reactive power upper limit MU_QMIN = 24 # KKT multiplier on lower reactive power upper limit # Generator cost matrix column indices MODEL = 0 # Cost model STARTUP = 1 # Startup cost in $ SHUTDOWN = 2 # Shutdown cost in $ NCOST = 3 # Number of cost coefficients (poly) or sample points (PWL) COST = 4 # Start column of coefficient or sample point data # Cost model types PW_LINEAR = 1 # Piecewise linear cost function POLYNOMIAL = 2 # Polynomial cost function
[docs]class DCLineConstants: """Constants for the MATPOWER DC line data, see the MATPOWER manual.""" # DC line matrix column indices F_BUS = 0 # "From" bus number T_BUS = 1 # "To" bus number BR_STATUS = 2 # Status (1 - in service, 0 - out of service) PMIN = 9 # Lower limit on MW flow at "from" end PMAX = 10 # Upper limit on MW flow at "from" end QMINF = 11 # Lower limit on MVAr injection at "from" bus QMAXF = 12 # Upper limit on MVAr injection at "from" bus QMINT = 13 # Lower limit on MVAr injection at "to" bus QMAXT = 14 # Upper limit on MVAr injection at "to" bus LOSS0 = 15 # Offset of linear loss function (MW) LOSS1 = 16 # Slope of linear loss function # OPF result column indices in the DC line matrix PF = 3 # Flow at "from" bus in MW PT = 4 # Flow at "to" bus in MW QF = 5 # Injection at "from" bus in MVAr QT = 6 # Injection at "to" bus in MVAr VF = 7 # Voltage set point at "from" bus (p.u.) VT = 8 # Voltage set point at "to" bus (p.u.) MU_PMIN = 17 # KKT multiplier on lower flow lim at "from" bus MU_PMAX = 18 # KKT multiplier on upper flow lim at "from" bus MU_QMINF = 19 # KKT multiplier on lower VAr lim at "from" bus MU_QMAXF = 20 # KKT multiplier on upper VAr lim at "from" bus MU_QMINT = 21 # KKT multiplier on lower VAr lim at "to" bus MU_QMAXT = 22 # KKT multiplier on upper VAr lim at "to" bus
def _load_mpc_from_mat_file(input_file): """ Load the MATPOWER case struct from the MAT file. SciPy's MAT loading (``scipy.io.loadmat``) only supports the formats v4 (Level 1.0), v6, and v7 to 7.2. For later versions, ``h5py`` is employed. The data is preprocessed for equal structure among all versions. """ mpc = {} try: mat = scipy.io.loadmat(input_file)['mpc'] mpc['baseMVA'] = mat['baseMVA'][0][0][0][0] prepare_data = lambda x: x[0][0] prepare_dict = prepare_data get_keys = lambda x: x.dtype.names except NotImplementedError: mat = h5py.File(input_file, 'r')['mpc'] mpc['baseMVA'] = mat['baseMVA'][0][0] def fix_empty_matrix(x): if x.shape == (2,): return np.ndarray(shape=x, dtype=hynet_float_).transpose() return x prepare_data = lambda x: fix_empty_matrix(x[()]).transpose() prepare_dict = lambda x: x get_keys = lambda x: x.keys() mpc['bus'] = prepare_data(mat['bus']) mpc['branch'] = prepare_data(mat['branch']) mpc['gen'] = prepare_data(mat['gen']) try: mpc['gencost'] = prepare_data(mat['gencost']) except (ValueError, KeyError): # If the generator cost is missing, set all costs to zero cgen = GeneratorConstants mpc['gencost'] = np.zeros((mpc['gen'].shape[0], cgen.COST + 2), dtype=hynet_float_) mpc['gencost'][:, cgen.MODEL] = cgen.POLYNOMIAL mpc['gencost'][:, cgen.NCOST] = 2 # Linear 'zero cost' function try: mpc['dcline'] = prepare_data(mat['dcline']) except (ValueError, KeyError): mpc['dcline'] = np.ndarray((0, DCLineConstants.LOSS1 + 1), dtype=hynet_float_) # --- CUSTOM EXTENSION: Scenario & line length information --- try: mpc['line_length'] = prepare_data(mat['line_length']) except (ValueError, KeyError): pass if 'line_length' in mpc: if mpc['line_length'].shape[0] != mpc['branch'].shape[0]: raise ValueError("Line length information is inconsistent.") try: scenarios = prepare_dict(mat['scenarios']) except (ValueError, KeyError): pass else: mpc['scenarios'] = {} for scenario in get_keys(scenarios): mpc['scenarios'][scenario] = {} scenario_data = prepare_dict(scenarios[scenario]) for field in get_keys(scenario_data): mpc['scenarios'][scenario][field] = \ prepare_data(scenario_data[field]) # --- CUSTOM EXTENSION: END ---------------------------------- return mpc def _get_gencost_sample_points(cost, p_min, p_max, num_sample_points): """Return the PWL function sample points for a MATPOWER ``gencost`` row.""" cgen = GeneratorConstants sample_points = [] N = int(cost[cgen.NCOST]) if N < 1: return sample_points if cost[cgen.MODEL] == cgen.PW_LINEAR: for x in range(N): sample_points.append((cost[cgen.COST + 2*x], cost[cgen.COST + 2*x + 1])) return sample_points if cost[cgen.MODEL] == cgen.POLYNOMIAL: coeff = cost[cgen.COST:cgen.COST + N] if np.all(coeff == 0): return sample_points if N == 1: # Constant function sample_points.append((0, coeff[0])) sample_points.append((1, coeff[0])) return sample_points if np.all(coeff[:-2] == 0): # Linear function sample_points.append((0, coeff[-1])) sample_points.append((1, coeff[-1] + coeff[-2])) return sample_points # Polynomial of second or higher order if p_min > p_max: raise ValueError("The test case contains a generator with " "infeasible active power limits: " "PMIN = {:f} > {:f} = PMAX.".format(p_min, p_max)) if p_min == p_max: constant_cost = np.polyval(coeff, p_min) sample_points.append((0, constant_cost)) sample_points.append((1, constant_cost)) _log.warning("For generators with fixed active power injection, " "the conversion of polynomial cost functions is not " "supported. The cost function is set to the constant " "{:f} that reflects the cost at its operating point." .format(constant_cost)) return sample_points for x in np.linspace(p_min, p_max, num_sample_points): sample_points.append((x, np.polyval(coeff, x))) return sample_points raise ValueError("Invalid cost function type: {:d}".format(cost[cgen.MODEL]))
[docs]def import_matpower_test_case(input_file, output_file=None, grid_name=None, description='', num_sample_points=10, res_detection=False): """ Import a MATPOWER test case file into *hynet*'s database format. This function imports a MATPOWER test case, which is stored as a MATPOWER test case struct ``mpc`` in a MATLAB MAT-file, to a *hynet*'s database. To prepare the import, start MATLAB and perform the following two steps: 1) Load the MATPOWER test case into the variable ``mpc``:: mpc = loadcase('your_matpower_test_case_file.m'); 2) Save the MATPOWER test case struct ``mpc`` to a MATLAB MAT file:: save('your_matpower_test_case_file.mat', 'mpc'); Call this function with the MAT-file as the input. Parameters ---------- input_file : str MATLAB MAT-file (.mat) with the MATPOWER test case struct ``mpc``. output_file : str, optional Destination *hynet* grid database file. By default (None), the file name is set to the input file name with a ``.db`` extension. grid_name : str, optional Name of the grid. By default (None), the grid name is set to the input file name excluding the extension. description : str, optional Description of the grid model and, if applicable, copyright and licensing information. num_sample_points : int, optional Number of sample points that shall be used for the conversion of polynomial to piecewise linear cost functions (on the interval from minimum to maximum output). This setting is a trade-off between an accurate representation of the original cost function and the number of additional constraints in the OPF problem. res_detection : bool, optional Detection of the injector type for renewable energy sources (RES), which is inactive by default. This scheme is motivated by the German grid data, which contains PV- and wind-based injectors with zero marginal cost that are arranged in a specific pattern: The injectors that connect to the same bus are stored consecutively in the generator matrix. If two or more injectors connect to the same bus, then the *last* one is wind-based and the *second to the last* is PV-based generation if their marginal cost is zero. If this parameter is set to True, this RES detection scheme is enabled. Returns ------- output_file : str Destination *hynet* grid database file name. Raises ------ ValueError NotImplementedError """ cbus = BusConstants cbra = BranchConstants cgen = GeneratorConstants cdcl = DCLineConstants if num_sample_points < 2: raise ValueError("Invalid number of sample points (use >= 2).") # Check file names if not input_file.lower().endswith(MAT_FILE_EXTENSION): raise ValueError("The input file is not a MATLAB MAT-file.") if output_file is None: output_file = "{:}.db".format(input_file[:-len(MAT_FILE_EXTENSION)]) # Load MATPOWER test case data try: mpc = _load_mpc_from_mat_file(input_file) except Exception as exception: raise IOError("Failed to load the MAT file: " + str(exception)) # Connect to the destination database database = connect(output_file) if not database.empty: raise ValueError("The destination database is not empty.") if grid_name is None: grid_name = os.path.basename(os.path.splitext(input_file)[0]) # Detect 'Inf' values and, if appropriate, replace them for field in ['bus', 'branch', 'gen', 'gencost', 'dcline']: idx_inf = np.where(np.isinf(mpc[field])) if not idx_inf[0].size: continue if (field == 'gen' and np.all(np.isin(idx_inf[1], [cgen.PMIN, cgen.PMAX, cgen.QMIN, cgen.QMAX]))) or \ (field == 'dcline' and np.all(np.isin(idx_inf[1], [cdcl.PMIN, cdcl.PMAX, cdcl.QMINF, cdcl.QMINT, cdcl.QMAXF, cdcl.QMAXT]))): INF_REPLACEMENT = 1e4 _log.warning(("The field '{:s}' contains (+/-) 'Inf' " "active/reactive power limits in the rows {:s}. " "These are replaced by (+/-) {:g} GW/Gvar.") .format(field, str(list(np.unique(idx_inf[0]) + 1)), INF_REPLACEMENT/1e3)) mpc[field][idx_inf] = np.sign(mpc[field][idx_inf]) * INF_REPLACEMENT else: raise ValueError("The field '{:s}' contains 'Inf' values." .format(field)) scenario_id = 0 # Scenario ID for the base case base_case_inactivity = [] # Track base case inactivity for scenarios base_kv = {} # Track bus base volt. for transformer detection with DBTransaction(database) as transaction: # General database information transaction.add(DBInfo(key=DBInfoKey.GRID_NAME, value=grid_name)) transaction.add(DBInfo(key=DBInfoKey.BASE_MVA, value=hynet_float_(mpc['baseMVA']))) transaction.add(DBInfo(key=DBInfoKey.DESCRIPTION, value=description)) # Add a default scenario transaction.add(DBScenario(id=scenario_id, time=0., name='Base Case', loss_price=0., annotation='')) # Import bus data if mpc['bus'].shape[0] < 1: raise ValueError("The test case does not contain any buses.") # Use the area number as zone if there is no zone information available ZONE_COLUMN = cbus.ZONE if np.all(mpc['bus'][:, cbus.ZONE] == mpc['bus'][0, cbus.ZONE]): if np.any(mpc['bus'][:, cbus.BUS_AREA] != mpc['bus'][0, cbus.BUS_AREA]): ZONE_COLUMN = cbus.BUS_AREA shunt_id = 1 for bus in mpc['bus']: bus_id = int(bus[cbus.BUS_I]) transaction.add(DBBus(id=bus_id, type=BusType.AC, ref=(bus[cbus.BUS_TYPE] == cbus.REF), base_kv=bus[cbus.BASE_KV], v_min=bus[cbus.VMIN], v_max=bus[cbus.VMAX], zone=int(bus[ZONE_COLUMN]), annotation='')) base_kv[bus_id] = bus[cbus.BASE_KV] # Add shunt if bus[cbus.GS] != 0 or bus[cbus.BS] != 0: transaction.add(DBShunt(id=shunt_id, bus_id=bus_id, p=bus[cbus.GS], q=bus[cbus.BS], annotation='')) shunt_id += 1 # Add load to the scenario if bus[cbus.PD] != 0 or bus[cbus.QD] != 0: transaction.add(DBScenarioLoad(scenario_id=scenario_id, bus_id=bus_id, p=bus[cbus.PD], q=bus[cbus.QD])) # Is this bus isolated/inactive? if bus[cbus.BUS_TYPE] == cbus.NONE: inactivity = DBScenarioInactivity(scenario_id=scenario_id, entity_type=EntityType.BUS, entity_id=bus_id) transaction.add(inactivity) base_case_inactivity.append(inactivity) # Import branch data for i, branch in enumerate(mpc['branch']): branch_id = i + 1 src_id = int(branch[cbra.F_BUS]) dst_id = int(branch[cbra.T_BUS]) # TRANSFORMER DETECTION: The branch type is set to transformer if # the branch connects different voltage levels or if the ratio or # phase setting is not neutral. # # REMARK: MATPOWER includes only a phase shifting transformer at # the source bus, which employs a *reciprocal* tap ratio compared # to *hynet*. ratio_src = 1. if branch[cbra.TAP] == 0 else 1 / branch[cbra.TAP] phase_src = -branch[cbra.SHIFT] if base_kv[src_id] != base_kv[dst_id] or \ ratio_src != 1 or phase_src != 0: branch_type = BranchType.TRANSFORMER else: branch_type = BranchType.LINE rating = branch[cbra.RATE_A] rating = rating if rating != 0 else None angle_min = angle_max = 0 if len(branch) > cbra.ANGMIN: angle_min = branch[cbra.ANGMIN] if len(branch) > cbra.ANGMAX: angle_max = branch[cbra.ANGMAX] if ((angle_min == 0 and angle_max == 0) or (angle_min <= -360 and angle_max >= 360)): angle_min = angle_max = None # --- CUSTOM EXTENSION: Line length information -------------- if 'line_length' in mpc: line_length = mpc['line_length'][i, 0] else: line_length = None # --- CUSTOM EXTENSION: END ---------------------------------- transaction.add(DBBranch(id=branch_id, type=branch_type, src_id=src_id, dst_id=dst_id, r=branch[cbra.BR_R], x=branch[cbra.BR_X], b_src=branch[cbra.BR_B] / 2, b_dst=branch[cbra.BR_B] / 2, ratio_src=ratio_src, phase_src=phase_src, ratio_dst=1., phase_dst=0., length=line_length, rating=rating, angle_min=angle_min, angle_max=angle_max, drop_min=None, drop_max=None, annotation='')) # Is this branch inactive? if branch[cbra.BR_STATUS] == 0: inactivity = DBScenarioInactivity(scenario_id=scenario_id, entity_type=EntityType.BRANCH, entity_id=branch_id) transaction.add(inactivity) base_case_inactivity.append(inactivity) # Import DC line data # (REMARK: The MATPOWER DC line model is compatible with *hynet*'s # converter model, i.e., DC lines are imported as AC/AC converters.) i = 0 converter_id = 0 cap_region_id = 1 # Note: This ID counter is also used for inj. cap. reg. while i < mpc['dcline'].shape[0]: converter_id += 1 dcline = mpc['dcline'][i] p_min = dcline[cdcl.PMIN] p_max = dcline[cdcl.PMAX] q_min_src = dcline[cdcl.QMINF] q_max_src = dcline[cdcl.QMAXF] q_min_dst = dcline[cdcl.QMINT] q_max_dst = dcline[cdcl.QMAXT] loss_fwd = loss_bwd = dcline[cdcl.LOSS1] * 100 # BIDIRECTIONAL LINES: The MATPOWER data format only supports # unidirectional lines and, as a consequence, test cases with # HVDC lines typically contain two entries for one line. The # following code combines these two entries to a bidirectional # HVDC line model. if i + 1 < len(mpc['dcline']): next_dcline = mpc['dcline'][i + 1] if (next_dcline[cdcl.F_BUS] == dcline[cdcl.T_BUS] and next_dcline[cdcl.T_BUS] == dcline[cdcl.F_BUS] and next_dcline[cdcl.BR_STATUS] == dcline[cdcl.BR_STATUS] and next_dcline[cdcl.PMIN] == dcline[cdcl.PMIN] == 0 and next_dcline[cdcl.PMAX] >= 0 and p_max >= 0): p_min = -next_dcline[cdcl.PMAX] q_min_src += next_dcline[cdcl.QMINT] q_max_src += next_dcline[cdcl.QMAXT] q_min_dst += next_dcline[cdcl.QMINF] q_max_dst += next_dcline[cdcl.QMAXF] loss_bwd = next_dcline[cdcl.LOSS1] * 100 i += 1 # Skip the second part of this bidirectional line # Capability region at the source and destination terminal cap_src_id = cap_region_id cap_dst_id = cap_region_id + 1 cap_region_id += 2 transaction.add(DBCapabilityRegion(id=cap_src_id, p_min=p_min, p_max=p_max, q_min=q_min_src, q_max=q_max_src, lt_ofs=None, lt_slp=None, rt_ofs=None, rt_slp=None, lb_ofs=None, lb_slp=None, rb_ofs=None, rb_slp=None, annotation='')) transaction.add(DBCapabilityRegion(id=cap_dst_id, p_min=-p_max, # Flow *into* the p_max=-p_min, # converter! q_min=q_min_dst, q_max=q_max_dst, lt_ofs=None, lt_slp=None, rt_ofs=None, rt_slp=None, lb_ofs=None, lb_slp=None, rb_ofs=None, rb_slp=None, annotation='')) # Add converter transaction.add(DBConverter(id=converter_id, src_id=int(dcline[cdcl.F_BUS]), dst_id=int(dcline[cdcl.T_BUS]), cap_src_id=cap_src_id, cap_dst_id=cap_dst_id, loss_fwd=loss_fwd, loss_bwd=loss_bwd, loss_fix=dcline[cdcl.LOSS0], annotation='')) # Is this DC line inactive? if dcline[cdcl.BR_STATUS] == 0: inactivity = DBScenarioInactivity(scenario_id=scenario_id, entity_type=EntityType.CONVERTER, entity_id=converter_id) transaction.add(inactivity) base_case_inactivity.append(inactivity) i += 1 # Import generator data if mpc['gen'].shape[0] != mpc['gencost'].shape[0]: raise NotImplementedError("The import of reactive power cost " "functions is not supported.") pwl_function_id = 1 for i, injector in enumerate(mpc['gen']): injector_id = i + 1 gencost = mpc['gencost'][i] # Capability region at the source and destination terminal cap_id = cap_region_id cap_region_id += 1 idx_cap_ref = [cgen.PC1, cgen.PC2, cgen.QC1MIN, cgen.QC1MAX, cgen.QC2MIN, cgen.QC2MAX] if len(injector) > max(idx_cap_ref) and \ any(injector[idx_cap_ref] != 0): _log.warning("The import of the sloped refinements of the " "generator capability are not supported. This " "data is ignored, i.e., only PMIN, PMAX, QMIN, " "QMAX is considered.") transaction.add(DBCapabilityRegion(id=cap_id, p_min=injector[cgen.PMIN], p_max=injector[cgen.PMAX], q_min=injector[cgen.QMIN], q_max=injector[cgen.QMAX], lt_ofs=None, lt_slp=None, rt_ofs=None, rt_slp=None, lb_ofs=None, lb_slp=None, rb_ofs=None, rb_slp=None, annotation='')) # PWL cost function for active power sample_points = _get_gencost_sample_points(gencost, injector[cgen.PMIN], injector[cgen.PMAX], num_sample_points) if sample_points: cost_id = pwl_function_id pwl_function_id += 1 for x, y in sample_points: transaction.add(DBSamplePoint(id=cost_id, x=x, y=y)) else: cost_id = None # INJECTOR TYPE: By default, it is set to conventional generation. # If and only if the generator offers exclusively reactive power at # zero cost, then it is considered as shunt compensation. terminal = int(injector[cgen.GEN_BUS]) injector_type = InjectorType.CONVENTIONAL if cost_id is None and \ all(injector[[cgen.PMIN, cgen.PMAX]] == 0) and \ any(injector[[cgen.QMIN, cgen.QMAX]] != 0): injector_type = InjectorType.COMPENSATION # --- CUSTOM EXTENSION: RES injector type detection ---------- if res_detection and cost_id is None: num_gen = mpc['gen'].shape[0] if i + 1 < num_gen: next_terminal = int(mpc['gen'][i + 1, cgen.GEN_BUS]) if next_terminal == terminal: if (i + 2 == num_gen or int(mpc['gen'][i + 2, cgen.GEN_BUS] != terminal)): next_cost = _get_gencost_sample_points( mpc['gencost'][i + 1], mpc['gen'][i + 1, cgen.PMIN], mpc['gen'][i + 1, cgen.PMAX], num_sample_points) if not next_cost: injector_type = InjectorType.PV if i >= 1: prev_terminal = int(mpc['gen'][i - 1][cgen.GEN_BUS]) if prev_terminal == terminal: if (i + 1 == num_gen or int(mpc['gen'][i + 1][cgen.GEN_BUS] != terminal)): prev_cost = _get_gencost_sample_points( mpc['gencost'][i - 1], mpc['gen'][i - 1, cgen.PMIN], mpc['gen'][i - 1, cgen.PMAX], num_sample_points) if not prev_cost: injector_type = InjectorType.WIND # --- CUSTOM EXTENSION: END ---------------------------------- # Add converter if injector[cgen.MBASE] not in [0., mpc['baseMVA']]: # REMARK: The MVA base of the machine does not seem to be # utilized at any point in the MATPOWER code and, in the vast # majority of test cases, it is set to the test case MVA base. _log.debug("The generator MVA base does not match the MVA " "base of the test case.") if len(injector) > cgen.RAMP_AGC: ramp = injector[cgen.RAMP_AGC] # Check if omitted and, if not, convert from MW/min to MW/h ramp = ramp * 60 if ramp != 0 else None else: ramp = None transaction.add(DBInjector(id=injector_id, type=injector_type, bus_id=terminal, cap_id=cap_id, cost_p_id=cost_id, cost_q_id=None, cost_start=gencost[cgen.STARTUP], cost_stop=gencost[cgen.SHUTDOWN], ramp_up=ramp, ramp_down=ramp, min_up=None, min_down=None, energy_min=None, energy_max=None, annotation='')) # Is this generator inactive? if injector[cgen.GEN_STATUS] <= 0: inactivity = DBScenarioInactivity(scenario_id=scenario_id, entity_type=EntityType.INJECTOR, entity_id=injector_id) transaction.add(inactivity) base_case_inactivity.append(inactivity) # If there is no scenario data, we're done... if 'scenarios' not in mpc: return output_file # --- CUSTOM EXTENSION: Scenario information ----------------- for scenario_name in mpc['scenarios']: scenario_data = mpc['scenarios'][scenario_name] scenario_title = scenario_name.replace('_', ' ').title() load_p = scenario_data['load_p'] load_q = scenario_data['load_q'] gen_pmin = scenario_data['gen_pmin'] gen_pmax = scenario_data['gen_pmax'] gen_qmin = scenario_data['gen_qmin'] gen_qmax = scenario_data['gen_qmax'] hours = load_p.shape[1] if not all([x.shape[1] == hours for x in [load_p, load_q, gen_pmin, gen_pmax, gen_qmin, gen_qmax]]): raise ValueError("The scenario data is inconsistent.") for hour in range(hours): scenario_id += 1 with DBTransaction(database) as transaction: transaction.add(DBScenario(id=scenario_id, time=hour, name=scenario_title, loss_price=0., annotation='')) for i, bus_id in enumerate(mpc['bus'][:, cbus.BUS_I]): if load_p[i, hour] == 0 and load_q[i, hour] == 0: continue transaction.add(DBScenarioLoad(scenario_id=scenario_id, bus_id=int(bus_id), p=load_p[i, hour], q=load_q[i, hour])) for i, injector in enumerate(mpc['gen']): injector_id = i + 1 if (gen_pmin[i, hour] == injector[cgen.PMIN] and gen_pmax[i, hour] == injector[cgen.PMAX] and gen_qmin[i, hour] == injector[cgen.QMIN] and gen_qmax[i, hour] == injector[cgen.QMAX]): continue transaction.add(DBScenarioInjector(scenario_id=scenario_id, injector_id=injector_id, p_min=gen_pmin[i, hour], p_max=gen_pmax[i, hour], q_min=gen_qmin[i, hour], q_max=gen_qmax[i, hour], cost_scaling=1.)) for inactivity in base_case_inactivity: transaction.add( DBScenarioInactivity(scenario_id=scenario_id, entity_type=inactivity.entity_type, entity_id=inactivity.entity_id)) # --- CUSTOM EXTENSION: END ---------------------------------- return output_file