# 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 typing import Dict
import numba as nb
import numpy as np
from VeraGridEngine.basic_structures import CxVec, Vec, IntVec, BoolVec, StrVec
from VeraGridEngine.enumerations import BusMode
@nb.njit(cache=True, inline="always")
def _control_priority(is_p: bool,
is_q: bool,
is_vm: bool,
is_va: bool) -> int:
"""
Rank the control strength of a bus.
"""
# Prefer buses that already define the electrical reference first, then
# voltage-controlled buses, and leave plain injection-controlled buses last.
if is_va and is_vm:
return 6
elif is_va:
return 5
elif is_vm and not is_q:
return 4
elif is_vm:
return 3
elif is_p and not is_q:
return 2
else:
return 1
[docs]
@nb.njit(cache=True)
def propagate_controls(chosen_idx: int,
other_idx: IntVec,
bus_types: IntVec,
Vbus: CxVec,
is_p_controlled: BoolVec,
is_q_controlled: BoolVec,
is_vm_controlled: BoolVec,
is_va_controlled: BoolVec) -> None:
"""
Propagate the strongest control status of a reduced bus cluster to the surviving bus.
The donor bus is chosen from ``chosen_idx`` and ``other_idx`` using the actual control flags,
not only the bus type, so VSC/HVDC-added controls are preserved as well.
:param chosen_idx: island of the island chosen by the topological reduction process to represent the reduction set
:param other_idx: rest of buses in the set that are going to be reduced but may contain important information
:param bus_types:
:param Vbus:
:param is_p_controlled:
:param is_q_controlled:
:param is_vm_controlled:
:param is_va_controlled:
:return:
"""
# Start by assuming the surviving bus already is the best donor.
donor_idx = chosen_idx
donor_priority = _control_priority(
is_p=is_p_controlled[chosen_idx],
is_q=is_q_controlled[chosen_idx],
is_vm=is_vm_controlled[chosen_idx],
is_va=is_va_controlled[chosen_idx],
)
for idx in other_idx:
priority = _control_priority(
is_p=is_p_controlled[idx],
is_q=is_q_controlled[idx],
is_vm=is_vm_controlled[idx],
is_va=is_va_controlled[idx],
)
# Replace the donor when the reduced bus carries a stronger control role.
if priority > donor_priority:
donor_idx = idx
donor_priority = priority
# For equally strong voltage-controlled buses, keep the one with the lowest
# bus index. PSSe retains the lowest-numbered bus of a merged (switch-joined)
# node, so when generators with conflicting setpoints land on the same node
# its setpoint wins. This is also deterministic (independent of island ordering).
elif priority == donor_priority and is_vm_controlled[idx] and idx < donor_idx:
donor_idx = idx
# Once the donor is chosen, copy its control flags and bus type as one block.
if donor_idx != chosen_idx:
bus_types[chosen_idx] = bus_types[donor_idx]
is_p_controlled[chosen_idx] = is_p_controlled[donor_idx]
is_q_controlled[chosen_idx] = is_q_controlled[donor_idx]
is_vm_controlled[chosen_idx] = is_vm_controlled[donor_idx]
is_va_controlled[chosen_idx] = is_va_controlled[donor_idx]
# Copy the donor voltage state directly; do not mix magnitude from one bus
# with angle from another.
Vbus[chosen_idx] = Vbus[donor_idx]
[docs]
class BusData:
"""
BusData
"""
def __init__(self, nbus: int):
"""
Bus data arrays
:param nbus: number of buses
"""
self.nbus: int = nbus
self.idtag: StrVec = np.empty(nbus, dtype=object)
self.names: StrVec = np.empty(nbus, dtype=object)
self.active: IntVec = np.ones(nbus, dtype=int)
self.Vbus: CxVec = np.ones(nbus, dtype=complex)
self.Vmin: Vec = np.ones(nbus, dtype=float)
self.Vmax: Vec = np.ones(nbus, dtype=float)
self.Vnom: Vec = np.ones(nbus, dtype=float)
self.cost_v: Vec = np.ones(nbus, dtype=float)
self.angle_min: Vec = np.full(nbus, fill_value=-3.14, dtype=float)
self.angle_max: Vec = np.full(nbus, fill_value=3.14, dtype=float)
self.bus_types: IntVec = np.empty(nbus, dtype=int)
self.is_p_controlled = np.zeros(nbus, dtype=bool)
self.is_q_controlled = np.zeros(nbus, dtype=bool)
self.is_vm_controlled = np.zeros(nbus, dtype=bool)
self.is_va_controlled = np.zeros(nbus, dtype=bool)
self.installed_power: Vec = np.zeros(nbus, dtype=float)
self.srap_available_power: Vec = np.zeros(nbus, dtype=float)
self.is_dc: BoolVec = np.empty(nbus, dtype=bool)
self.is_grounded: BoolVec = np.empty(nbus, dtype=bool)
self.areas: IntVec = np.empty(nbus, dtype=int)
self.substations: IntVec = np.empty(nbus, dtype=int)
# This is the total value used to compute the q_share in generators, batteries and shunts
self.q_shared_total = np.zeros(nbus, dtype=float)
# This is the fixed amount of Q that is Subtrated from Qbus to compute
# the Q of each generator, battery and shunt controlling
self.q_fixed = np.zeros(nbus, dtype=float)
self.ii_fixed = np.zeros(nbus, dtype=float) # same concept, but imag current from loads
self.b_fixed = np.zeros(nbus, dtype=float) # same concept, but susceptance from shunts
self.original_idx: IntVec = np.zeros(nbus, dtype=int)
[docs]
def slice(self, elm_idx: IntVec) -> "BusData":
"""
Slice this data structure
:param elm_idx: array of bus indices
:return: instance of BusData
"""
data = BusData(nbus=len(elm_idx))
data.names = self.names[elm_idx]
data.idtag = self.idtag[elm_idx]
data.active = self.active[elm_idx]
data.Vbus = self.Vbus[elm_idx]
data.Vmin = self.Vmin[elm_idx]
data.Vmax = self.Vmax[elm_idx]
data.Vnom = self.Vnom[elm_idx]
data.cost_v = self.cost_v[elm_idx]
data.angle_min = self.angle_min[elm_idx]
data.angle_max = self.angle_max[elm_idx]
data.bus_types = self.bus_types[elm_idx]
data.is_p_controlled = self.is_p_controlled[elm_idx]
data.is_q_controlled = self.is_q_controlled[elm_idx]
data.is_vm_controlled = self.is_vm_controlled[elm_idx]
data.is_va_controlled = self.is_va_controlled[elm_idx]
data.installed_power = self.installed_power[elm_idx]
data.srap_available_power = self.srap_available_power[elm_idx]
data.is_dc = self.is_dc[elm_idx]
data.is_grounded = self.is_grounded[elm_idx]
data.areas = self.areas[elm_idx]
data.substations = self.substations[elm_idx]
data.q_shared_total = self.q_shared_total[elm_idx]
data.q_fixed = self.q_fixed[elm_idx]
data.ii_fixed = self.ii_fixed[elm_idx]
data.b_fixed = self.b_fixed[elm_idx]
data.original_idx = elm_idx
return data
[docs]
def size(self) -> int:
"""
Get size of the structure
:return:
"""
return self.nbus
[docs]
def copy(self) -> "BusData":
"""
Deep copy of this structure
:return: instance of BusData
"""
data = BusData(nbus=self.nbus)
data.names = self.names.copy()
data.idtag = self.idtag.copy()
data.active = self.active.copy()
data.Vbus = self.Vbus.copy()
data.Vmin = self.Vmin.copy()
data.Vmax = self.Vmax.copy()
data.Vnom = self.Vnom.copy()
data.cost_v = self.cost_v.copy()
data.angle_min = self.angle_min.copy()
data.angle_max = self.angle_max.copy()
data.bus_types = self.bus_types.copy()
data.is_p_controlled = self.is_p_controlled.copy()
data.is_q_controlled = self.is_q_controlled.copy()
data.is_vm_controlled = self.is_vm_controlled.copy()
data.is_va_controlled = self.is_va_controlled.copy()
data.installed_power = self.installed_power.copy()
data.srap_available_power = self.srap_available_power.copy()
data.is_dc = self.is_dc.copy()
data.is_grounded = self.is_grounded.copy()
data.areas = self.areas.copy()
data.substations = self.substations.copy()
data.q_shared_total = self.q_shared_total.copy()
data.q_fixed = self.q_fixed.copy()
data.ii_fixed = self.ii_fixed.copy()
data.b_fixed = self.b_fixed.copy()
data.original_idx = self.original_idx.copy()
return data
[docs]
def get_original_to_island_bus_dict(self) -> Dict[int, int]:
"""
Dictionary that relates the original bus index to the island bus index
:return: dict[original idx] -> island index
"""
return {
int(val): idx for idx, val in enumerate(self.original_idx)
}
[docs]
def get_idtag_dict(self) -> Dict[str, int]:
"""
Get dictionary of bus idtagd related to the island bus index
:return:
"""
return {idtag_val: i for i, idtag_val in enumerate(self.idtag)}
[docs]
def propagate_controls(self, chosen_idx: int, other_idx: IntVec) -> None:
"""
Propagate the strongest control status of the reduced buses into ``chosen_idx``.
"""
propagate_controls(
chosen_idx=chosen_idx,
other_idx=other_idx,
bus_types=self.bus_types,
Vbus=self.Vbus,
is_p_controlled=self.is_p_controlled,
is_q_controlled=self.is_q_controlled,
is_vm_controlled=self.is_vm_controlled,
is_va_controlled=self.is_va_controlled,
)
[docs]
def set_bus_mode(self, idx: int, val: BusMode):
"""
Set bus mode
:param idx: int
:param val: BusMode
"""
self.bus_types[idx] = val.value
is_dc = self.is_dc[idx]
is_grounded = self.is_grounded[idx]
# For each bus the logic and priority is:
# 1) grounded (AC or DC): Vm/Va fixed (to 0), P/Q free
# 2) DC non-grounded
# 3) AC non-grounded
if val == BusMode.PQ_tpe:
if is_grounded:
# grounded bus (AC or DC): acts as ground reference
self.is_p_controlled[idx] = False
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
elif is_dc:
# DC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = False
self.is_va_controlled[idx] = True
else:
# AC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = True
self.is_vm_controlled[idx] = False
self.is_va_controlled[idx] = False
elif val == BusMode.PV_tpe:
if is_grounded:
# grounded bus (AC or DC): acts as ground reference
self.is_p_controlled[idx] = False
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
elif is_dc:
# DC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
else:
# AC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = False
elif val == BusMode.Slack_tpe:
if is_grounded:
# grounded bus (AC or DC): acts as ground reference
self.is_p_controlled[idx] = False
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
elif is_dc:
# DC, not grounded
self.is_p_controlled[idx] = False
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
else:
# AC, not grounded
self.is_p_controlled[idx] = False
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
elif val == BusMode.PQV_tpe:
if is_grounded:
# grounded bus (AC or DC): acts as ground reference
self.is_p_controlled[idx] = False
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
elif is_dc:
# DC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
else:
# AC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = True
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = False
elif val == BusMode.P_tpe:
if is_grounded:
# grounded bus (AC or DC): acts as ground reference
self.is_p_controlled[idx] = False
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = True
self.is_va_controlled[idx] = True
elif is_dc:
# DC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = False
self.is_va_controlled[idx] = True
else:
# AC, not grounded
self.is_p_controlled[idx] = True
self.is_q_controlled[idx] = False
self.is_vm_controlled[idx] = False
self.is_va_controlled[idx] = False
else:
raise ValueError("Unexpected bus mode")
def __len__(self) -> int:
return self.nbus
[docs]
def get_3ph_names(self):
"""
Get the 3-phase names
:return:
"""
# names3: StrVec = np.empty(self.nbus * 3, dtype=object)
# for i in range(self.nbus):
# names3[3 * i + 0] = self.names[i] + "_A"
# names3[3 * i + 1] = self.names[i] + "_B"
# names3[3 * i + 2] = self.names[i] + "_C"
names3: StrVec = np.empty(self.nbus, dtype=object)
for i in range(self.nbus):
names3[i] = self.names[i]
return names3