Source code for VeraGridEngine.Devices.Branches.transformerNw
# 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
from typing import Sequence, Tuple, Union
import numpy as np
from VeraGridEngine.Devices.Branches.transformer_type import get_impedances
from VeraGridEngine.Devices.Branches.winding import Winding
from VeraGridEngine.Devices.Parents.editable_device import GCProp, get_at
from VeraGridEngine.Devices.Parents.physical_device import PhysicalDevice
from VeraGridEngine.Devices.Substation.bus import Bus
from VeraGridEngine.Devices.Profiles import ProfileBool
from VeraGridEngine.enumerations import BuildStatus, DeviceType, PrpCat
[docs]
class TransformerNW(PhysicalDevice):
"""
Generic N-winding transformer represented as a set of windings connected to a
common internal star bus.
The leakage data is stored per winding. Shared core-loss data is represented by
``Pfe`` and ``I0`` at the transformer level and mapped through ``get_impedances``.
To avoid counting the magnetizing branch multiple times, the resulting shunt
admittance is assigned to the first winding only.
"""
__slots__ = (
"bus0",
"cn0",
"_buses",
"_windings",
"_active",
"_active_prof",
"_Pfe",
"_I0",
"_x",
"_y",
)
LOCAL_PROPERTY_DECLARATIONS: Tuple[GCProp, ...] = (
GCProp(
prop_name="bus0",
units="",
tpe=DeviceType.BusDevice,
definition="Middle point connection bus.",
editable=False,
cat=[PrpCat.TP],
),
GCProp(
prop_name="active",
units="",
tpe=bool,
definition="Is active?",
profile_name="active_prof",
cat=[PrpCat.PF],
),
GCProp(
prop_name="winding_count",
units="",
tpe=int,
definition="Number of windings.",
editable=False,
cat=[PrpCat.TP],
),
GCProp(
prop_name="Pfe",
units="kW",
tpe=float,
definition="Iron loss",
cat=[PrpCat.PF],
),
GCProp(
prop_name="I0",
units="%",
tpe=float,
definition="No-load current",
cat=[PrpCat.PF],
),
GCProp(
prop_name="x",
units="px",
tpe=float,
definition="x position",
cat=[PrpCat.TP],
),
GCProp(
prop_name="y",
units="px",
tpe=float,
definition="y position",
cat=[PrpCat.TP],
),
)
def __init__(self,
idtag: Union[str, None] = None,
code: str = "",
name: str = "TransformerNW",
bus0: Bus | None = None,
winding_count: int | None = None,
buses: Sequence[Bus | None] | None = None,
active: bool = True,
Pfe: float = 0.0,
I0: float = 0.0,
x: float = 0.0,
y: float = 0.0,
build_status: BuildStatus = BuildStatus.Commissioned):
"""
Constructor
:param idtag: Unique identifier
:param code: Secondary identifier
:param name: Name of the transformer
:param bus0: Internal star-point bus. If ``None``, an internal bus is created automatically
:param winding_count: Number of windings to allocate. If ``None``, it is inferred from ``buses`` and
falls back to 3
:param buses: Sequence of terminal buses, one per winding. If provided, those buses are assigned to the
first winding slots
:param active: Is active?
:param Pfe: Shared iron-core losses in kW for the whole transformer
:param I0: Shared no-load current in % for the whole transformer
:param x: Graphical x position (px)
:param y: Graphical y position (px)
:param build_status: Device build status
"""
PhysicalDevice.__init__(self,
name=name,
idtag=idtag,
code=code,
device_type=DeviceType.TransformerNwDevice,
build_status=build_status)
if bus0 is None:
self.bus0 = Bus(name=name + "_bus", Vnom=1.0, xpos=x, ypos=y, is_internal=True)
else:
bus0.internal = True
bus0.Vnom = 1.0
self.bus0 = bus0
self.cn0 = None
self.active = bool(active)
self._active_prof = ProfileBool(default_value=self.active)
self._Pfe = float(Pfe)
self._I0 = float(I0)
self._buses: list[Bus | None] = list()
self._windings: list[Winding] = list()
self.x = float(x)
self.y = float(y)
self.initialize_windings(winding_count=winding_count, buses=buses)
@property
def active_prof(self) -> ProfileBool:
"""
Cost profile
:return: Profile
"""
return self._active_prof
@active_prof.setter
def active_prof(self, val: Union[ProfileBool, np.ndarray]):
if isinstance(val, ProfileBool):
self._active_prof = val
elif isinstance(val, np.ndarray):
self._active_prof.set(arr=val)
else:
raise Exception(str(type(val)) + "not supported to be set into a active_prof")
[docs]
def get_active_at(self, t: int | None) -> float:
"""
:param t:
:return:
"""
return get_at(self.active, self.active_prof, t)
@property
def winding_count(self) -> int:
"""
Number of windings in the transformer.
"""
return len(self._windings)
@winding_count.setter
def winding_count(self, val: int) -> None:
self.set_winding_count(count=int(val))
@property
def buses(self) -> Tuple[Bus | None, ...]:
"""
Terminal buses attached to the windings.
"""
return tuple(self._buses)
@property
def windings(self) -> Tuple[Winding, ...]:
"""
Winding objects connected to the internal bus.
"""
return tuple(self._windings)
[docs]
def all_connected(self) -> bool:
"""
Check that all windings are connected to a terminal bus.
"""
return bool(self._buses) and all(bus is not None for bus in self._buses)
[docs]
def get_bus(self, i: int) -> Bus | None:
"""
Get terminal bus from an integer index.
"""
return self._buses[i]
[docs]
def get_winding(self, i: int) -> Winding:
"""
Get winding from an integer index.
"""
return self._windings[i]
def _prepare_winding(self, winding: Winding, bus: Bus | None, index: int) -> Winding:
winding.bus_from = self.bus0
winding.bus_to = bus
if winding.name == "Winding":
winding.name = f"{self.name}_W{index + 1}"
hv = winding.HV
lv = winding.LV
if hv is None and bus is not None:
hv = bus.Vnom
elif hv is None:
hv = 1.0
if lv is None or lv <= 0.0:
lv = 1.0
if bus is None:
winding.HV = hv
winding.LV = lv
else:
winding.set_hv_and_lv(HV=hv, LV=lv)
if winding.Sn <= 0.0 and winding.rate > 0.0:
winding.Sn = winding.rate
elif winding.rate <= 0.0 and winding.Sn > 0.0:
winding.rate = winding.Sn
return winding
[docs]
def set_bus(self, i: int, bus: Bus | None) -> None:
"""
Set terminal bus and keep the associated winding in sync.
"""
self._buses[i] = bus
winding = self._windings[i]
winding.bus_to = bus
if bus is not None:
winding.set_hv_and_lv(HV=winding.HV, LV=winding.LV)
[docs]
def set_winding(self, i: int, winding: Winding, bus: Bus | None = None) -> None:
"""
Replace a winding at position ``i``.
"""
target_bus = winding.bus_to if bus is None else bus
prepared = self._prepare_winding(winding=winding, bus=target_bus, index=i)
self._windings[i] = prepared
self._buses[i] = target_bus
[docs]
def add_winding(self,
bus: Bus | None = None,
winding: Winding | None = None,
w_idtag: Union[str, None] = None,
nominal_voltage: float | None = None,
nominal_power: float = 0.0) -> Winding:
"""
Add a winding to the transformer.
"""
index = len(self._windings)
if winding is None:
hv = nominal_voltage if nominal_voltage is not None else (bus.Vnom if bus is not None else 1.0)
rate = nominal_power if nominal_power > 0.0 else 1.0
sn = nominal_power if nominal_power > 0.0 else 0.001
winding = Winding(bus_from=self.bus0,
bus_to=bus,
name=f"{self.name}_W{index + 1}",
idtag=w_idtag,
HV=hv,
LV=1.0,
nominal_power=sn,
rate=rate,
active=self.active)
prepared = self._prepare_winding(winding=winding, bus=bus if bus is not None else winding.bus_to, index=index)
self._windings.append(prepared)
self._buses.append(prepared.bus_to)
return prepared
[docs]
def delete_winding(self, i: int) -> Winding:
"""
Delete a winding by index and return it.
"""
self._buses.pop(i)
return self._windings.pop(i)
[docs]
def initialize_windings(self,
winding_count: int | None = None,
buses: Sequence[Bus | None] | None = None) -> None:
"""
Initialize the transformer winding set from a winding count and optional buses.
"""
self._buses = list()
self._windings = list()
buses_list = list(buses) if buses is not None else list()
if winding_count is None:
target_count = len(buses_list) if len(buses_list) > 0 else 3
else:
target_count = max(int(winding_count), len(buses_list))
for i in range(target_count):
bus = buses_list[i] if i < len(buses_list) else None
nominal_voltage = bus.Vnom if bus is not None else 1.0
self.add_winding(bus=bus, nominal_voltage=nominal_voltage, nominal_power=0.0)
[docs]
def set_winding_count(self, count: int) -> None:
"""
Resize the winding list preserving existing winding objects when possible.
"""
target_count = max(int(count), 1)
current_count = len(self._windings)
if target_count == current_count:
return
if target_count < current_count:
del self._buses[target_count:]
del self._windings[target_count:]
return
for _ in range(current_count, target_count):
self.add_winding(bus=None, nominal_voltage=1.0, nominal_power=0.0)
[docs]
def recalculate_windings_from_definition(self, Sbase: float = 100.0) -> None:
"""
Recompute the winding per-unit values from the design data stored in each
winding plus the shared core-loss values stored at the transformer level.
"""
for i, winding in enumerate(self._windings):
nominal_power = winding.Sn if winding.Sn > 0.0 else winding.rate
if nominal_power <= 0.0:
nominal_power = 1.0
winding.Sn = nominal_power
if winding.rate <= 0.0:
winding.rate = nominal_power
hv = winding.HV if winding.HV is not None else (winding.bus_to.Vnom if winding.bus_to is not None else 1.0)
lv = winding.LV if winding.LV is not None and winding.LV > 0.0 else 1.0
z_series, y_shunt = get_impedances(
VH_bus=hv,
VL_bus=lv,
Sn=nominal_power,
HV=hv,
LV=lv,
Pcu=winding.Pcu,
Pfe=self.Pfe if i == 0 else 0.0,
I0=self.I0 if i == 0 else 0.0,
Vsc=winding.Vsc,
Sbase=Sbase,
GR_hv1=0.5,
)
winding.R = np.round(z_series.real, 6)
winding.X = np.round(z_series.imag, 6)
if i == 0:
winding.G = np.round(y_shunt.real, 6)
winding.B = np.round(y_shunt.imag, 6)
else:
winding.G = 0.0
winding.B = 0.0
[docs]
def fill_from_design_values(self, Pfe: float, I0: float, Sbase: float) -> None:
"""
Fill winding per-unit values from each winding's design data and the shared
transformer core-loss data.
"""
self._Pfe = float(Pfe)
self._I0 = float(I0)
self.recalculate_windings_from_definition(Sbase=Sbase)
@property
def Pfe(self) -> float:
"""
Shared iron loss in kW.
"""
return self._Pfe
@Pfe.setter
def Pfe(self, value: float) -> None:
self._Pfe = float(value)
self.recalculate_windings_from_definition(Sbase=100.0)
@property
def I0(self) -> float:
"""
Shared no-load current in %.
"""
return self._I0
@I0.setter
def I0(self, value: float) -> None:
self._I0 = float(value)
self.recalculate_windings_from_definition(Sbase=100.0)
@property
def active(self) -> bool:
"""
Get ``active``.
"""
return self._active
@active.setter
def active(self, val: bool) -> None:
"""
Set ``active``.
"""
self._active = bool(val)
@property
def x(self) -> float:
"""
Get ``x``.
"""
return self._x
@x.setter
def x(self, val: float) -> None:
"""
Set ``x``.
"""
self._x = float(val)
@property
def y(self) -> float:
"""
Get ``y``.
"""
return self._y
@y.setter
def y(self, val: float) -> None:
"""
Set ``y``.
"""
self._y = float(val)