# 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 Tuple, Optional, TYPE_CHECKING
from VeraGridEngine.enumerations import DeviceType
from VeraGridEngine.Devices.Dynamic.emt_template import EmtModelTemplate
from VeraGridEngine.Devices.Dynamic.var_factory import VarFactory
from VeraGridEngine.Devices.Substation.bus import Bus
from VeraGridEngine.Utils.Symbolic.block import Block, VarPowerFlowReferenceType
from VeraGridEngine.Utils.Symbolic.symbolic import Var
if TYPE_CHECKING:
from VeraGridEngine.Devices.multi_circuit import MultiCircuit
[docs]
class BusEmtTemplate(EmtModelTemplate):
__slots__ = (
"tpe",
"_block",
"v_DC",
"v_N",
"v_A",
"v_B",
"v_C",
)
def __init__(self,
vf: VarFactory,
mask: list[bool],
is_dc: bool = False,
name: str = "emt_bus_template"):
"""
Created the EMT Template of a Bus
:param vf: VarFactory
:param mask: list[bool] with true if the phase exist and false if it doesn't in the order [N,A,B,C]
:param is_dc: True if it is a DC bus, else False
:param name: Name of the EMT Model
"""
super().__init__(name=name)
self.tpe: DeviceType = DeviceType.BusDevice
self._block.name = name
if is_dc:
# For a DC bus in EMT, we track the instantaneous DC voltage.
self.v_DC = vf.add_var(name=f"v_DC_{name}", reference=VarPowerFlowReferenceType.Vdc)
self._block = Block(
algebraic_vars=[self.v_DC],
out_vars=[self.v_DC]
)
# Map the instantaneous DC voltage. P and Q are None because
# EMT simulations compute power implicitly from v(t) and i(t).
# Include v_N, v_A, v_B, v_C as None so that the loop in emt_problem_dae
# can iterate over all phases without errors.
self._block.external_mapping = {
VarPowerFlowReferenceType.Vdc: self.v_DC,
VarPowerFlowReferenceType.v_N: None,
VarPowerFlowReferenceType.v_A: None,
VarPowerFlowReferenceType.v_B: None,
VarPowerFlowReferenceType.v_C: None,
VarPowerFlowReferenceType.P: None,
VarPowerFlowReferenceType.Q: None
}
else:
self.v_N = vf.add_var(name=f"v_N_{name}", reference=VarPowerFlowReferenceType.v_N) if mask[0] else None
self.v_A = vf.add_var(name=f"v_A_{name}", reference=VarPowerFlowReferenceType.v_A) if mask[1] else None
self.v_B = vf.add_var(name=f"v_B_{name}", reference=VarPowerFlowReferenceType.v_B) if mask[2] else None
self.v_C = vf.add_var(name=f"v_C_{name}", reference=VarPowerFlowReferenceType.v_C) if mask[3] else None
v_list = [v for v in (self.v_N, self.v_A, self.v_B, self.v_C) if v is not None]
if not v_list:
raise ValueError("Bus has no enabled phases")
self._block = Block(
algebraic_vars=v_list,
out_vars=v_list
)
self._block.external_mapping = {
VarPowerFlowReferenceType.Vdc: None,
VarPowerFlowReferenceType.v_N: self.v_N,
VarPowerFlowReferenceType.v_A: self.v_A,
VarPowerFlowReferenceType.v_B: self.v_B,
VarPowerFlowReferenceType.v_C: self.v_C,
VarPowerFlowReferenceType.P: None,
VarPowerFlowReferenceType.Q: None
}
@property
def block(self) -> Block:
"""
block
:return: Block
"""
return self._block
[docs]
def get_bus_emt_template(grid: MultiCircuit,
bus: Bus):
"""
Initialize 3ph bus block
A bus will have the phases of the branches connected to it
:param grid: Multicircuit
:param bus: Bus
:return:
"""
vf = grid.var_factory
mask = [False, False, False, False] # [N, A, B, C]
if not bus.is_dc:
ys_phase_device_types = {
DeviceType.LineDevice,
}
transformer_phase_device_types = {
DeviceType.Transformer2WDevice,
DeviceType.WindingDevice,
}
abc_phase_device_types = {
DeviceType.VscDevice,
DeviceType.SeriesReactanceDevice,
DeviceType.UpfcDevice,
DeviceType.SwitchDevice,
DeviceType.HVDCLineDevice,
DeviceType.DCLineDevice,
}
for branch in grid.get_branches_iter(add_vsc=True, add_switch=True, add_hvdc=True):
if bus == branch.bus_from or bus == branch.bus_to:
if branch.device_type in ys_phase_device_types and branch.ys is not None:
mask[0] |= bool(branch.ys.phN)
mask[1] |= bool(branch.ys.phA)
mask[2] |= bool(branch.ys.phB)
mask[3] |= bool(branch.ys.phC)
elif branch.device_type in transformer_phase_device_types:
mask[1] = True
mask[2] = True
mask[3] = True
elif branch.device_type in abc_phase_device_types:
mask[1] = True
mask[2] = True
mask[3] = True
else:
mask[1] = True
mask[2] = True
mask[3] = True
if not any(mask):
# The dynamic editor can open EMT templates before the network has any EMT-aware branches.
# In that case we expose a default ABC shell so the user can still build and attach EMT models.
mask[0] = False
mask[1] = True
mask[2] = True
mask[3] = True
else:
pass
# choose template depending on the number of phases
bus.emt_model = BusEmtTemplate(vf=vf, mask=mask, is_dc=bus.is_dc, name=f"{bus.name}_emt_template").block
[docs]
def get_bus_emt_algebraic_vars(bus_emt_model: Block) -> Tuple[
Optional[Var], Optional[Var], Optional[Var], Optional[Var]]:
"""
Return the EMT bus algebraic voltage variables.
For AC buses:
returns (v_N, v_A, v_B, v_C)
Missing phases are returned as None.
For DC buses:
returns (Vdc, None, None, None)
:param bus_emt_model: EMT bus block
:return: Tuple with four positions to preserve a stable API
"""
external_mapping = bus_emt_model.external_mapping
# DC bus
vdc = external_mapping.get(VarPowerFlowReferenceType.Vdc, None)
if vdc is not None:
return vdc, None, None, None
# AC bus
v_n = external_mapping.get(VarPowerFlowReferenceType.v_N, None)
v_a = external_mapping.get(VarPowerFlowReferenceType.v_A, None)
v_b = external_mapping.get(VarPowerFlowReferenceType.v_B, None)
v_c = external_mapping.get(VarPowerFlowReferenceType.v_C, None)
if v_n is None and v_a is None and v_b is None and v_c is None:
raise ValueError("Invalid EMT bus model: no voltage algebraic variables found")
return v_n, v_a, v_b, v_c