Source code for VeraGridEngine.IO.others.pandapower_parser

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.  
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations

import math
from typing import Dict
import sqlite3
import json
import numpy as np
import pandas as pd

import VeraGridEngine.Devices as dev
# from VeraGridEngine.Devices.Branches.tap_changer import TapChanger
from VeraGridEngine.enumerations import (ExternalGridMode, TapChangerTypes, GeneratorControlMode)
from VeraGridEngine.Devices.types import ALL_DEV_TYPES
from VeraGridEngine.basic_structures import Logger

try:
    from pandapower import from_pickle, from_sqlite, from_json, from_excel
    from pandapower.auxiliary import pandapowerNet

    PANDAPOWER_AVAILABLE = True

except ImportError:
    pandapower = None
    PANDAPOWER_AVAILABLE = False


[docs] def is_pandapower_pickle(file_path): """ Check if a file is pandapower Pickle :param file_path: :return: """ if PANDAPOWER_AVAILABLE: try: net = from_pickle(file_path) return isinstance(net, dict) and all(key in net for key in ["bus", "line", "load", "ext_grid"]) except Exception: return False else: return False
[docs] def is_pandapower_json(file_path): """ Check if a file is pandapower JSON :param file_path: :return: """ if PANDAPOWER_AVAILABLE: try: with open(file_path, "r") as f: data = json.load(f) return isinstance(data, dict) and all(key in data for key in ["bus", "line", "load", "ext_grid"]) except Exception: return False else: return False
[docs] def is_pandapower_sqlite(file_path): """ Check if a file is pandapower SQLite :param file_path: :return: """ if PANDAPOWER_AVAILABLE: try: with sqlite3.connect(file_path) as conn: cursor = conn.cursor() cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") tables = {row[0] for row in cursor.fetchall()} return {"bus", "line", "load", "ext_grid"}.issubset(tables) except Exception: return False else: return False
[docs] def is_pandapower_file(file_path: str): """ Check if this is a pandapower file :param file_path: :return: """ if file_path.endswith(".p"): return is_pandapower_pickle(file_path) elif file_path.endswith(".json"): return is_pandapower_json(file_path) elif file_path.endswith(".sqlite"): return is_pandapower_sqlite(file_path) else: return False
[docs] class Panda2VeraGrid: def __init__(self, file_or_net: str | "pandapowerNet", logger: Logger | None = None): """ Initialize :param file_or_net: PandaPower file name or pandapowerNet """ self.logger = logger if logger is not None else Logger() self.panda_dict: Dict[str, Dict[int, ALL_DEV_TYPES]] = dict() if PANDAPOWER_AVAILABLE: if isinstance(file_or_net, str): if file_or_net.endswith(".p"): self.panda_net: pandapowerNet = from_pickle(file_or_net) elif file_or_net.endswith(".sqlite"): self.panda_net: pandapowerNet = from_sqlite(file_or_net) elif file_or_net.endswith(".json"): self.panda_net: pandapowerNet = from_json(file_or_net) elif file_or_net.endswith(".xlsx"): self.panda_net: pandapowerNet = from_excel(file_or_net) else: raise Exception("Don't know what to do with this PandaPower file :/") elif isinstance(file_or_net, pandapowerNet): self.panda_net: pandapowerNet = file_or_net else: raise Exception(f"The argument is not recognized as a Pandapower net or file :/ {file_or_net}") self.logger.add_info("This seems to be a pandapower file") self.fBase = self.panda_net.f_hz Sbase = self.panda_net.sn_mva if self.panda_net.sn_mva > 0.0 else 100.0 self.load_scale = 1 / Sbase # To handle the terrible practice of pandapower to use Sbase to represent kW else: self.panda_net = None self.fBase = 50.0 self.load_scale = 1 self.logger.add_info("Pandapower not available :/, try pip install pandapower")
[docs] def register(self, panda_type: str, panda_code: int, api_obj: ALL_DEV_TYPES): """ Register a panda object and it's associated VeraGrid object :param panda_type: table name :param panda_code: index key :param api_obj: VeraGrid object """ d = self.panda_dict.get(panda_type, None) if d is None: self.panda_dict[panda_type] = {panda_code: api_obj} else: p = d.get(panda_code, None) if p is None: d[panda_code] = api_obj else: self.logger.add_error("Panda index repeated", device_class=panda_type, value=panda_code)
[docs] def get_api_object_by_registry(self, panda_type: str, panda_code: int) -> ALL_DEV_TYPES | None: """ Get a previously registered veragrid object from a pandapower table-key :param panda_type: table name :param panda_code: index key :return: VeraGrid object """ d = self.panda_dict.get(panda_type, None) if d is None: return None else: return d.get(panda_code, None)
[docs] def parse_buses(self, grid: dev.MultiCircuit) -> Dict[str | int, dev.Bus]: """ Add buses to the VeraGrid grid based on Pandapower data :param grid: MultiCircuit grid :return: PP row name or index to VeraGrid Bus object """ bus_dictionary: Dict[str | int, dev.Bus] = dict() for idx, row in self.panda_net.bus.iterrows(): elm = dev.Bus( name=row['name'], Vnom=row['vn_kv'], code=idx, vmin=row['min_vm_pu'] if 'min_vm_pu' in row else 0.9, vmax=row['max_vm_pu'] if 'max_vm_pu' in row else 1.1, active=bool(row['in_service']), # idtag=row.get('uuid', None) # uuids can be repeated and mess things up ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_bus(elm) # Add the row to the VeraGrid grid # add the entry bu name in the buses dict if row['name'] in bus_dictionary: self.logger.add_error("Repeated bus name", value=row['name']) else: bus_dictionary[row['name']] = elm # add also the entry by idx bus_dictionary[idx] = elm self.register(panda_type="bus", panda_code=idx, api_obj=elm) return bus_dictionary
[docs] def parse_external_grids(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add external grid (slack bus) generators to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: :return: """ for idx, row in self.panda_net.ext_grid.iterrows(): if row["in_service"]: bus = bus_dictionary[row['bus']] elm = dev.ExternalGrid( name=row['name'], code=idx, Vm=row['vm_pu'], mode=ExternalGridMode.VD, idtag=row.get('uuid', None) ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_external_grid(bus, elm) self.register(panda_type="ext_grid", panda_code=idx, api_obj=elm)
[docs] def parse_loads(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add loads to the VeraGrid grid based on Pandapower data :param grid: MultiCircuit grid :param bus_dictionary: """ for idx, row in self.panda_net.load.iterrows(): if row["in_service"]: bus = bus_dictionary[row['bus']] elm = dev.Load( name=row['name'], code=idx, P=row['p_mw'] * self.load_scale, Q=row['q_mvar'] * self.load_scale, idtag=row.get('uuid', None) ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_load(bus=bus, api_obj=elm) self.register(panda_type="load", panda_code=idx, api_obj=elm)
[docs] def parse_shunts(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add shunts to the VeraGrid grid based on Pandapower data :param grid: MultiCircuit grid :param bus_dictionary: """ for idx, row in self.panda_net.shunt.iterrows(): bus = bus_dictionary[row['bus']] elm = dev.Shunt( name=row['name'], code=idx, G=row["p_mw"] * self.load_scale, B=row["q_mvar"] * self.load_scale, idtag=row.get('uuid', None) ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_shunt(bus=bus, api_obj=elm) self.register(panda_type="shunt", panda_code=idx, api_obj=elm)
[docs] def parse_lines(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add lines (conductors) to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: """ for idx, row in self.panda_net.line.iterrows(): bus1 = bus_dictionary[row['from_bus']] bus2 = bus_dictionary[row['to_bus']] elm = dev.Line( bus_from=bus1, bus_to=bus2, name=row['name'], code=idx, active=bool(row['in_service']), idtag=row.get('uuid', None) ) elm.rdfid = row.get('uuid', elm.idtag) elm.fill_design_properties( r_ohm=row['r_ohm_per_km'], x_ohm=row['x_ohm_per_km'], c_nf=row['c_nf_per_km'], length=row['length_km'], Imax=row.get('max_i_ka', 10.0), # max_i_ka might not be there... freq=grid.fBase, Sbase=grid.Sbase, apply_to_profile=False ) # Uncomment the following lines if line activation status is needed # if (self.lines[self.lines['id'] == idx]['Enabled'].values[0] == False): # line.active = False grid.add_line(elm) self.register(panda_type="line", panda_code=idx, api_obj=elm)
[docs] def parse_impedances(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add impedances to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: :return: """ mult = 1.6 # Multiplicative factor for impedance # Add impedance elements to the VeraGrid grid for idx, row in self.panda_net.impedance.iterrows(): bus1 = bus_dictionary[row['from_bus']] bus2 = bus_dictionary[row['to_bus']] # Calculate base impedance zbase = math.pow((self.panda_net.bus.loc[row['from_bus'], 'vn_kv']), 2) / grid.Sbase ru = row.rft_pu * row.sn_mva / zbase * mult xu = row.xft_pu * row.sn_mva / zbase * mult elm = dev.SeriesReactance( bus_from=bus1, bus_to=bus2, name=row['name'], code=idx, r=ru, x=xu, idtag=row.get('uuid', None) ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_series_reactance(elm) self.register(panda_type="impedance", panda_code=idx, api_obj=elm)
[docs] def parse_storage(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add storages to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: :return: """ for idx, row in self.panda_net.storage.iterrows(): bus = bus_dictionary[row['bus']] elm = dev.Battery( code=idx, Pmin=row['min_p_mw'], Pmax=row['max_p_mw'], Qmin=row['min_q_mvar'], Qmax=row['max_q_mvar'], Sbase=row['sn_mva'], Enom=row['max_e_mwh'], active=row['in_service'], soc=row['soc_percent'], idtag=row.get('uuid', None) ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_battery(bus=bus, api_obj=elm) # Add battery to the grid self.register(panda_type="storage", panda_code=idx, api_obj=elm)
[docs] def parse_generators(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add synchronous generators (row) to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: :return: """ for idx, row in self.panda_net.gen.iterrows(): if row["in_service"]: bus = bus_dictionary[row['bus']] elm = dev.Generator( name=row['name'], code=idx, P=row['p_mw'] * self.load_scale, control_mode=GeneratorControlMode.V, idtag=row.get('uuid', None), vset=row["vm_pu"] ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_generator(bus=bus, api_obj=elm) # Add generator to the grid self.register(panda_type="gen", panda_code=idx, api_obj=elm)
[docs] def parse_static_generators(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add synchronous generators (row) to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: :return: """ for idx, row in self.panda_net.sgen.iterrows(): if row["in_service"]: bus = bus_dictionary[row['bus']] elm = dev.StaticGenerator( name=row['name'], code=idx, P=row['p_mw'] * self.load_scale, Q=row["q_mvar"], active=row['in_service'], idtag=row.get('uuid', None) ) elm.rdfid = row.get('uuid', elm.idtag) grid.add_static_generator(bus=bus, api_obj=elm) # Add generator to the grid self.register(panda_type="sgen", panda_code=idx, api_obj=elm)
[docs] def parse_transformers(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add transformers to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: """ for idx, row in self.panda_net.trafo.iterrows(): bus1 = bus_dictionary[row['hv_bus']] bus2 = bus_dictionary[row['lv_bus']] elm = dev.Transformer2W( bus_from=bus1, bus_to=bus2, name=row["name"], code=idx, HV=row['vn_hv_kv'], LV=row['vn_lv_kv'], nominal_power=row['sn_mva'], rate=row['sn_mva'], idtag=row.get('uuid', None), ) elm.rdfid = row.get('uuid', elm.idtag) # --- Derived values --- mf, mt = elm.get_virtual_taps() if not (0.9 <= mf <= 1.1): self.logger.add_warning("Virtual tap 'from' out of bounds, possible transformer data error", value=mf) if not (0.9 <= mt <= 1.1): self.logger.add_warning("Virtual tap 'to' out of bounds, possible transformer data error", value=mt) # see: https://pandapower.readthedocs.io/en/latest/elements/trafo.html#trafo Pcu = row.get("vkr_percent", 0.0) / 100 * row["sn_mva"] * 1000 # copper losses in kW Pfe = row.get("pfe_kw", 0.0) # iron losses in kW I0 = row.get('i0_percent', 0.0) # no-load current in % Vsc = row.get("vk_percent", 0.0) # short-circuit voltage (%) elm.fill_design_properties( Pcu=Pcu, Pfe=Pfe, I0=I0, Vsc=Vsc, Sbase=grid.Sbase ) tc = self.extract_tap_changers(row) if tc is not None: elm.tap_changer = tc grid.add_transformer2w(elm) self.register(panda_type="trafo", panda_code=idx, api_obj=elm)
[docs] def extract_tap_changers(self, row) -> dev.TapChanger | None: """ # Tap changer mapping (pandapower β†’ GridCal) # # Ratio + tap_step_percent only: # dV = tap_step_percent / 100 # asymmetry_angle = 0Β° # # Ratio + tap_step_percent + tap_step_degree (cross regulator): # Ξ΄u = tap_step_percent / 100 # Ξ± = tap_step_degree (phase shift per tap, deg) # # Conversion (UCTE): # tan(Ξ±) = (Ξ΄u Β· sinΘ) / (1 + Ξ΄u Β· cosΘ) # β‡’ Θ = Ξ± + arcsin(sin(Ξ±) / Ξ΄u) # # If Ξ΄u = 0 or |sin(Ξ±)| > Ξ΄u: # Fallback β†’ treat as Ideal phase shifter: # dV = 0 # asymmetry_angle = Ξ± # # Symmetrical: # dV = tap_step_percent / 100 # asymmetry_angle = 90Β° # # Ideal: # dV = 0 # asymmetry_angle = tap_step_degree For "Symmetrical" and "Ideal", mapping stays as before. """ tap_changer_type = row.get("tap_changer_type", None) dV = 0.0 asymmetry_angle = 0.0 tc_type = TapChangerTypes.NoRegulation if tap_changer_type is not None and pd.notna(row["tap_neutral"]): if tap_changer_type == "Ratio": # Longitudinal regulator dV = row['tap_step_percent'] / 100.0 asymmetry_angle = 0.0 tc_type = TapChangerTypes.VoltageRegulation # Check if cross regulator (with angle) if "tap_step_degree" in row and row["tap_step_degree"] != 0.0: alpha = np.deg2rad(row["tap_step_degree"]) # pandapower phase shift Ξ± [rad] if dV > 0 and abs(np.sin(alpha)) <= dV: # Convert Ξ± -> Θ (GridCal asymmetry_angle) theta = alpha + np.arcsin(np.sin(alpha) / dV) asymmetry_angle = np.rad2deg(theta) tc_type = TapChangerTypes.Asymmetrical else: # fallback: cannot map with given dV, treat as ideal angle shifter asymmetry_angle = row["tap_step_degree"] dV = 0.0 tc_type = TapChangerTypes.Asymmetrical elif tap_changer_type == "Symmetrical": dV = row['tap_step_percent'] / 100.0 asymmetry_angle = 90.0 tc_type = TapChangerTypes.Symmetrical elif tap_changer_type == "Ideal": dV = 0.0 tc_type = TapChangerTypes.Asymmetrical asymmetry_angle = row.get("tap_step_degree", 90.0) # default to 90Β° if missing else: tc_type = TapChangerTypes.NoRegulation dV = 0.0 asymmetry_angle = 90.0 # Build GridCal TapChanger return dev.TapChanger( total_positions=row['tap_max'] - row['tap_min'] + 1, neutral_position=row['tap_neutral'], normal_position=row['tap_pos'], dV=dV, asymmetry_angle=asymmetry_angle, tc_type=tc_type ) else: return None
[docs] def parse_transformers3W(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str, dev.Bus]): """ Add 3W transformers to the VeraGrid grid :param grid: MultiCircuit grid :param bus_dictionary: """ for idx, row in self.panda_net.trafo3w.iterrows(): bus_hv = bus_dictionary[row['hv_bus']] bus_mv = bus_dictionary[row['mv_bus']] bus_lv = bus_dictionary[row['lv_bus']] # Nominal voltages V1, V2, V3 = row.vn_hv_kv, row.vn_mv_kv, row.vn_lv_kv # Ratings (if available, else default to 100 MVA) Sn1 = getattr(row, "sn_hv_mva", grid.Sbase) Sn2 = getattr(row, "sn_mv_mva", grid.Sbase) Sn3 = getattr(row, "sn_lv_mva", grid.Sbase) # Build transformer elm = dev.Transformer3W( idtag=str(row.uuid), code=str(idx), name=str(row.name), bus1=bus_hv, bus2=bus_mv, bus3=bus_lv, V1=V1, V2=V2, V3=V3, r12=0.0, r23=0.0, r31=0.0, # will be recomputed x12=0.0, x23=0.0, x31=0.0, rate12=Sn1, rate23=Sn2, rate31=Sn3, ) elm.rdfid = row.get('uuid', elm.idtag) # --- Derived values --- # Copper losses [kW] and short-circuit voltages [%] Pfe = row.get("pfe_kw", 0.0) # iron losses in kW I0 = row.get('i0_percent', 0.0) # short-circuit voltage (%) Vsc12 = row.get("vk_hv_percent", 0.0) Vsc23 = row.get("vk_mv_percent", 0.0) Vsc31 = row.get("vk_lv_percent", 0.0) # see: https://pandapower.readthedocs.io/en/latest/elements/trafo.html#trafo Pcu12 = row.get("vkr_hv_percent", 0.0) / 100.0 * Sn1 * 1000.0 # copper losses in kW Pcu23 = row.get("vkr_mv_percent", 0.0) / 100.0 * Sn2 * 1000.0 # copper losses in kW Pcu31 = row.get("vkr_lv_percent", 0.0) / 100.0 * Sn3 * 1000.0 # copper losses in kW # Fill design values (VeraGrid computes r,x from % values) elm.fill_from_design_values( V1=V1, V2=V2, V3=V3, Sn1=Sn1, Sn2=Sn2, Sn3=Sn3, Pcu12=Pcu12, Pcu23=Pcu23, Pcu31=Pcu31, Vsc12=Vsc12, Vsc23=Vsc23, Vsc31=Vsc31, Pfe=Pfe, I0=I0, Sbase=grid.Sbase, ) tc = self.extract_tap_changers(row) if tc is not None: elm.winding1.tap_changer = tc grid.add_transformer3w(elm) self.register(panda_type="trafo3w", panda_code=idx, api_obj=elm)
[docs] def parse_switches(self, grid: dev.MultiCircuit, bus_dictionary: Dict[str | int, dev.Bus]): """ See: https://pandapower.readthedocs.io/en/latest/elements/switch.html :param grid: MultiCircuit grid :param bus_dictionary: :return: """ # Add switches to the VeraGrid grid for idx, switch_row in self.panda_net.switch.iterrows(): # Identify the first bus in the switch bus_from = bus_dictionary[switch_row['bus']] k = int(switch_row['element']) if switch_row['et'] == 'b': # Bus-to-bus switch if k < grid.get_bus_number(): # Get the second bus directly bus_to = bus_dictionary[k] # Create the switch as a normal branch in VeraGrid switch_branch = dev.Switch( bus_from=bus_from, bus_to=bus_to, name=f"Switch_{switch_row['et']}_{switch_row['element']}", code=idx, active=switch_row['closed'], ) switch_branch.rdfid = switch_row.get("uuid", "") grid.add_switch(switch_branch) self.register(panda_type="switch", panda_code=idx, api_obj=switch_branch) else: self.logger.add_error("Switch referencing to bus out of bounds", value=k, device=f"switch {idx}") elif switch_row['et'] == 'l': # switch between bus and line if k < grid.get_lines_number(): # Create the auxiliary bus if it doesn't exist bus_to = dev.Bus(name=f"switch {idx} bus", Vnom=bus_from.Vnom) grid.add_bus(bus_to) branch = grid.lines[k] if branch.bus_from == bus_from: branch.bus_from = bus_to elif branch.bus_to == bus_from: branch.bus_to = bus_to else: self.logger.add_error("Disconnected switch", device=f"switch of transformer {branch.name}") # Create the switch as a normal branch in VeraGrid switch_branch = dev.Switch( bus_from=bus_from, bus_to=bus_to, name=f"switch of line {branch.name}", code=idx, active=switch_row['closed'], ) switch_branch.rdfid = switch_row.get("uuid", "") grid.add_switch(switch_branch) self.register(panda_type="switch", panda_code=idx, api_obj=switch_branch) else: self.logger.add_error("Switch referencing to line out of bounds", value=k, device=f"switch {idx}") elif switch_row['et'] == 't': # switch between bus and transformer if k < grid.get_transformers2w_number(): branch = grid.transformers2w[k] # Create the auxiliary bus if it doesn't exist bus_to = dev.Bus(name=f"switch {idx} bus", Vnom=bus_from.Vnom) grid.add_bus(bus_to) if branch.bus_from == bus_from: branch.bus_from = bus_to elif branch.bus_to == bus_from: branch.bus_to = bus_to else: self.logger.add_error("Disconnected switch", device=f"switch of transformer {branch.name}") # Create the switch as a normal branch in VeraGrid switch_branch = dev.Switch( bus_from=bus_from, bus_to=bus_to, name=f"switch of transformer {branch.name}", code=idx, active=switch_row['closed'], ) switch_branch.rdfid = switch_row.get("uuid", "") grid.add_switch(switch_branch) self.register(panda_type="switch", panda_code=idx, api_obj=switch_branch) else: self.logger.add_error("Switch referencing to transformer out of bounds", value=k, device=f"switch {idx}") else: self.logger.add_warning("TR3 switch not implemented", device=f"switch {idx}")
[docs] def parse_measurements(self, grid: dev.MultiCircuit): """ :param grid: :return: """ df: pd.DataFrame | None = self.panda_net.get("measurement", None) if df is not None: for i, row in df.iterrows(): name = row['name'] m_tpe = row['measurement_type'] # v, va, p, q, i # bus, line, transformer, transformer3w, load, sgen, static_generator, ward, xward, external_grid elm_tpe = row['element_type'] idx = row['element'] # index val = row['value'] std = row['std_dev'] side = row['side'] api_object = self.get_api_object_by_registry(panda_type=elm_tpe, panda_code=idx) if api_object is not None: if elm_tpe == 'bus': if m_tpe == 'v': grid.add_vm_measurement(dev.VmMeasurement( value=val, uncertainty=std, api_obj=api_object, name=name) ) elif m_tpe == "va": grid.add_va_measurement(dev.VaMeasurement(value=val, uncertainty=std, api_obj=api_object, name=name)) elif m_tpe == 'p': grid.add_pi_measurement(dev.PiMeasurement( value=val, uncertainty=std, api_obj=api_object, name=name) ) elif m_tpe == 'q': grid.add_qi_measurement(dev.QiMeasurement( value=val, uncertainty=std, api_obj=api_object, name=name) ) elif m_tpe == 'i': vnom = api_object.Vnom if hasattr(api_object, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_if_measurement( dev.IfMeasurement( value=value, uncertainty=std, api_obj=api_object, name=name ) ) else: self.logger.add_warning(f"PandaPower {m_tpe} measurement not implemented") elif elm_tpe in ['load', 'gen', 'sgen', 'shunt']: if m_tpe == 'v': grid.add_vm_measurement(dev.VmMeasurement( value=val, uncertainty=std, api_obj=api_object.bus, name=name) ) elif m_tpe == "va": grid.add_va_measurement(dev.VaMeasurement(value=val, uncertainty=std, api_obj=api_object, name=name)) elif m_tpe == 'p': grid.add_pi_measurement(dev.PiMeasurement( value=val, uncertainty=std, api_obj=api_object.bus, name=name) ) elif m_tpe == 'q': grid.add_qi_measurement(dev.QiMeasurement( value=val, uncertainty=std, api_obj=api_object.bus, name=name) ) elif m_tpe == 'i': vnom = api_object.bus.Vnom if hasattr(api_object.bus, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_if_measurement( dev.IfMeasurement( value=value, uncertainty=std, api_obj=api_object, name=name )) else: self.logger.add_warning(f"PandaPower {m_tpe} measurement not implemented") elif elm_tpe in ['line', 'impedance', 'trafo', 'trafo3w']: if m_tpe == 'p': if side == 1 or side == 'from' or side == "hv": if elm_tpe == "trafo3w": grid.add_pf_measurement(dev.PfMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object.winding1, name=name )) else: grid.add_pf_measurement(dev.PfMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object, name=name )) elif side == 2 or side == 'to' or side == "lv": if elm_tpe == "trafo3w": grid.add_pt_measurement(dev.PtMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object.winding3, # winding3 corresponds to bus3 and LV side name=name )) else: grid.add_pt_measurement(dev.PtMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object, name=name )) elif side == "mv": # for trafo3w MV side grid.add_pt_measurement(dev.PtMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object.winding2, # bus2 is MV and winding2 name=name )) elif m_tpe == 'q': if side == 1 or side == 'from' or side == "hv": if elm_tpe == "trafo3w": grid.add_qf_measurement(dev.QfMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object.winding1, name=name )) else: grid.add_qf_measurement(dev.QfMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object, name=name )) elif side == 2 or side == 'to' or side == "lv": if elm_tpe == "trafo3w": grid.add_qt_measurement(dev.QtMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object.winding3, name=name )) else: grid.add_qt_measurement(dev.QtMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object, name=name )) elif side == "mv": grid.add_qt_measurement(dev.QtMeasurement( value=val * self.load_scale, uncertainty=std, api_obj=api_object.winding2, name=name )) elif m_tpe == "i": if elm_tpe == 'trafo': if side == 1 or side == "hv" or side == 'from': vnom = api_object.bus_from.Vnom if hasattr(api_object.bus_from, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_if_measurement( dev.IfMeasurement( value=value, uncertainty=std, api_obj=api_object, name=name )) if side == 2 or side == "lv" or side == 'to': vnom = api_object.bus_to.Vnom if hasattr(api_object.bus_to, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_it_measurement( dev.ItMeasurement( value=value, uncertainty=std, api_obj=api_object, name=name )) elif elm_tpe == 'trafo3w': if side == 1 or side == "hv": vnom = api_object.bus1.Vnom if hasattr(api_object.bus1, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_if_measurement( dev.IfMeasurement( value=value, uncertainty=std, api_obj=api_object.winding1, name=name )) if side == 2 or side == "mv": vnom = api_object.bus2.Vnom if hasattr(api_object.bus2, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_it_measurement( dev.ItMeasurement( value=value, uncertainty=std, api_obj=api_object.winding2, name=name )) if side == 3 or side == "lv": vnom = api_object.bus3.Vnom if hasattr(api_object.bus3, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_it_measurement( dev.ItMeasurement( value=value, uncertainty=std, api_obj=api_object.winding3, name=name )) else: if side == 1 or side == 'from': vnom = api_object.bus_from.Vnom if hasattr(api_object.bus_from, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_if_measurement( dev.IfMeasurement( value=value, uncertainty=std, api_obj=api_object, name=name )) if side == 2 or side == "to": vnom = api_object.bus_from.Vnom if hasattr(api_object.bus_to, 'Vnom') else 1.0 ibase = grid.Sbase / (vnom * math.sqrt(3)) value = val / ibase # Convert kA to pu grid.add_it_measurement( dev.ItMeasurement( value=value, uncertainty=std, api_obj=api_object, name=name )) else: self.logger.add_warning(f"PandaPower {m_tpe} measurement type not implemented for double " f"pole elements") else: self.logger.add_warning(f"PandaPower {elm_tpe} measurement type not implemented")
[docs] def get_multicircuit(self, convert_switches: bool = True) -> dev.MultiCircuit: """ Get a VeraGrid Multi-circuit from a PandaPower grid :return: MultiCircuit """ grid = dev.MultiCircuit() if self.panda_net is not None: # grid.Sbase = self.panda_net.sn_mva if self.panda_net.sn_mva > 0.0 else 100.0 # always, the pandapower # For pandaPower Sbase is crazily affecting only load scaling and not the impedances grid.Sbase = 100.0 # always, the pandapower scaling is handled in the conversions grid.fBase = self.panda_net.f_hz bus_dict = self.parse_buses(grid=grid) self.parse_lines(grid=grid, bus_dictionary=bus_dict) self.parse_impedances(grid=grid, bus_dictionary=bus_dict) self.parse_loads(grid=grid, bus_dictionary=bus_dict) self.parse_shunts(grid=grid, bus_dictionary=bus_dict) self.parse_external_grids(grid=grid, bus_dictionary=bus_dict) self.parse_storage(grid=grid, bus_dictionary=bus_dict) self.parse_generators(grid=grid, bus_dictionary=bus_dict) self.parse_static_generators(grid=grid, bus_dictionary=bus_dict) self.parse_transformers(grid=grid, bus_dictionary=bus_dict) self.parse_transformers3W(grid=grid, bus_dictionary=bus_dict) if convert_switches: self.parse_switches(grid=grid, bus_dictionary=bus_dict) self.parse_measurements(grid=grid) return grid