# 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 numpy as np
from typing import Tuple, Union, TYPE_CHECKING
from VeraGridEngine.basic_structures import Logger
from VeraGridEngine.Devices.Substation.substation import Substation
from VeraGridEngine.Devices.Substation.voltage_level import VoltageLevel
from VeraGridEngine.Devices.Substation.bus import Bus
from VeraGridEngine.enumerations import BuildStatus, DeviceType, PrpCat
from VeraGridEngine.Devices.Parents.dynamic_parent import DynamicDevice
from VeraGridEngine.Devices.Aggregation.branch_group import BranchGroup
from VeraGridEngine.Devices.Profiles import ProfileBool, ProfileFloat
from VeraGridEngine.Devices.Parents.editable_device import get_at, GCProp
if TYPE_CHECKING:
from VeraGridEngine.Devices.types import CONNECTION_TYPE
[docs]
class BranchParent(DynamicDevice):
"""
This class serves to represent the basic branch
All other branches inherit from this one
"""
__slots__ = (
'_bus_from',
'_bus_to',
'_active',
'_active_prof',
'_temp_base',
'_temp_oper',
'_temp_oper_prof',
'_alpha',
'_reducible',
'contingency_enabled',
'_monitor_loading',
'_mttf',
'_mttr',
'_Cost',
'_Cost_prof',
'_capex',
'_opex',
'build_status',
'_design_rate',
'_rate',
'_rate_prof',
'_contingency_factor',
'_contingency_factor_prof',
'_protection_rating_factor',
'_protection_rating_factor_prof',
'color',
'_bus_from_pos',
'_bus_to_pos',
'group',
)
LOCAL_PROPERTY_DECLARATIONS: Tuple[GCProp, ...] = (
GCProp(
prop_name='bus_from',
units="",
tpe=DeviceType.BusDevice,
definition='Name of the bus at the "from" side',
editable=False,
cat=[PrpCat.TP],
),
GCProp(
prop_name='bus_to',
units="",
tpe=DeviceType.BusDevice,
definition='Name of the bus at the "to" side',
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='reducible',
units="",
tpe=bool,
definition='Is the branch to be reduced by the topology preprocessor?',
cat=[PrpCat.TP],
),
GCProp(
prop_name='design_rate',
units="MVA",
tpe=float,
definition='Design thermal rating power that is not modified for operational reasons or otherwise',
cat=[PrpCat.TP, PrpCat.PF],
),
GCProp(
prop_name='rate',
units="MVA",
tpe=float,
definition='Operational thermal rating power',
profile_name="rate_prof",
cat=[PrpCat.PF, PrpCat.OPF],
),
GCProp(
prop_name='contingency_factor',
units="p.u.",
tpe=float,
definition='Rating multiplier for contingencies',
profile_name="contingency_factor_prof",
cat=[PrpCat.CON, PrpCat.OPF],
),
GCProp(
prop_name='protection_rating_factor',
units="p.u.",
tpe=float,
definition='Rating multiplier that indicates the maximum flow before the protections tripping',
profile_name="protection_rating_factor_prof",
cat=[PrpCat.CON],
),
GCProp(
prop_name='monitor_loading',
units="",
tpe=bool,
definition="Monitor this device loading for OPF, NTC or contingency studies.",
cat=[PrpCat.CON, PrpCat.OPF],
),
GCProp(
prop_name='mttf',
units="h",
tpe=float,
definition="Mean time to failure",
cat=[PrpCat.REL],
),
GCProp(
prop_name='mttr',
units="h",
tpe=float,
definition="Mean time to repair",
cat=[PrpCat.REL],
),
GCProp(
prop_name='Cost',
units="e/MWh",
tpe=float,
definition="Cost of overloads. Used in OPF",
profile_name="Cost_prof",
old_names=("overload_cost",),
cat=[PrpCat.OPF],
),
GCProp(
prop_name='capex',
units="e/MW",
tpe=float,
definition="Cost of investment. Used in expansion planning.",
cat=[PrpCat.INV],
),
GCProp(
prop_name='opex',
units="e/MWh",
tpe=float,
definition="Cost of operation. Used in expansion planning.",
cat=[PrpCat.INV],
),
GCProp(
prop_name='group',
units="",
tpe=DeviceType.BranchGroupDevice,
definition="Group where this branch belongs",
cat=[PrpCat.CON, PrpCat.OPF],
),
GCProp(
prop_name='color',
units='',
tpe=str,
definition='Color to paint the element in the map diagram',
is_color=True,
),
GCProp(
prop_name='bus_from_pos',
units='',
tpe=int,
definition='Aid to locate devices on a busbar',
display=False,
),
GCProp(
prop_name='bus_to_pos',
units='',
tpe=int,
definition='Aid to locate devices on a busbar',
display=False,
),
GCProp(
prop_name='temp_base',
units='ºC',
tpe=float,
definition='Base temperature at which R was measured.',
cat=[PrpCat.PF],
),
GCProp(
prop_name='temp_oper',
units='ºC',
tpe=float,
definition='Operation temperature to modify R.',
profile_name='temp_oper_prof',
cat=[PrpCat.PF],
),
GCProp(
prop_name='alpha',
units='1/ºC',
tpe=float,
definition='Thermal coefficient to modify R,around a reference temperature using a linear '
'approximation.For example:Copper @ 20ºC: 0.004041,Copper @ 75ºC: 0.00323,'
'Annealed copper @ 20ºC: 0.00393,Aluminum @ 20ºC: 0.004308,Aluminum @ 75ºC: 0.00330',
cat=[PrpCat.PF],
),
)
def __init__(self,
name: str,
idtag: Union[str, None],
code: str,
bus_from: Union[Bus, None],
bus_to: Union[Bus, None],
active: bool,
reducible: bool,
design_rate: float,
rate: float,
contingency_factor: float,
protection_rating_factor: float,
contingency_enabled: bool,
monitor_loading: bool,
mttf: float,
mttr: float,
build_status: BuildStatus,
capex: float,
opex: float,
cost: float,
temp_base: float,
temp_oper: float,
alpha: float,
device_type: DeviceType,
color: str | None = None):
"""
:param name: name of the branch
:param idtag: UUID code
:param code: secondary id
:param bus_from: Name of the bus at the "from" side
:param bus_to: Name of the bus at the "to" side
:param active: Is active?
:param design_rate: Rate that is not manipulated for operational reasons or otherwise (MVA)
:param rate: Branch operational rating (MVA)
:param contingency_factor: Factor to multiply the rating in case of contingency
:param contingency_enabled: Enabled contingency (Legacy, better use contingency objects)
:param monitor_loading: Monitor loading (Legacy)
:param mttf: Mean time to failure
:param mttr: Mean time to repair
:param build_status: Branch build status. Used in expansion planning.
:param capex: Cost of investment. (e/MW)
:param opex: Cost of operation. (e/MWh)
:param cost: Cost of overloads. Used in OPF (e/MWh)
:param device_type: device_type (passed on)
:param color: Color of the branch
"""
DynamicDevice.__init__(self,
name=name,
idtag=idtag,
code=code,
device_type=device_type,
build_status=build_status)
# connectivity
if bus_from is None:
self._bus_from = None
elif isinstance(bus_from, Bus):
self._bus_from = bus_from
else:
raise Exception(f"Bus from is a strange value {bus_from}")
if bus_to is None:
self._bus_to = None
elif isinstance(bus_to, Bus):
self._bus_to = bus_to
else:
raise Exception(f"Bus from is a strange value {bus_from}")
self.active = bool(active)
self._active_prof = ProfileBool(default_value=self.active)
# Conductor base and operating temperatures in ºC
self.temp_base = float(temp_base)
self.temp_oper = float(temp_oper)
self._temp_oper_prof = ProfileFloat(default_value=self.temp_oper)
# Conductor thermal constant (1/ºC)
self.alpha = float(alpha)
self.reducible = bool(reducible)
self.contingency_enabled: bool = contingency_enabled
self.monitor_loading: bool = monitor_loading
self.mttf = mttf
self.mttr = mttr
self.Cost = cost
self._Cost_prof = ProfileFloat(default_value=cost)
self.capex = capex
self.opex = opex
self._design_rate = design_rate
# line rating in MVA
if not isinstance(rate, Union[float, int]):
raise ValueError("Rate must be a float")
self._rate = float(rate)
self._rate_prof = ProfileFloat(default_value=rate)
if not isinstance(contingency_factor, Union[float, int]):
raise ValueError("contingency_factor must be a float")
self._contingency_factor = float(contingency_factor)
self._contingency_factor_prof = ProfileFloat(default_value=contingency_factor)
if not isinstance(protection_rating_factor, Union[float, int]):
raise ValueError("protection_rating_factor must be a float")
self._protection_rating_factor = float(protection_rating_factor)
self._protection_rating_factor_prof = ProfileFloat(default_value=protection_rating_factor)
self.color = color if color is not None else "#909090" # light gray
self.bus_from_pos: int = 0
self.bus_to_pos: int = 0
# group of this branch
self.group: Union[BranchGroup, None] = None
@property
def bus_from(self) -> Bus:
"""
Bus
:return: Bus
"""
return self._bus_from
@bus_from.setter
def bus_from(self, val: Bus):
if val is None:
self._bus_from = val
else:
if isinstance(val, Bus):
self._bus_from = val
else:
raise Exception(str(type(val)) + 'not supported to be set into a _bus_from')
@property
def bus_to(self) -> Bus:
"""
Bus
:return: Bus
"""
return self._bus_to
@bus_to.setter
def bus_to(self, val: Bus):
if val is None:
self._bus_to = val
else:
if isinstance(val, Bus):
self._bus_to = val
else:
raise Exception(str(type(val)) + 'not supported to be set into a _bus_to')
@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 rate_prof(self) -> ProfileFloat:
"""
Cost profile
:return: Profile
"""
return self._rate_prof
@rate_prof.setter
def rate_prof(self, val: Union[ProfileFloat, np.ndarray]):
if isinstance(val, ProfileFloat):
self._rate_prof = val
elif isinstance(val, np.ndarray):
self._rate_prof.set(arr=val)
else:
raise Exception(str(type(val)) + 'not supported to be set into a rate_prof')
[docs]
def get_rate_at(self, t: int | None) -> float:
"""
:param t:
:return:
"""
return get_at(self.rate, self.rate_prof, t)
@property
def contingency_factor_prof(self) -> ProfileFloat:
"""
Cost profile
:return: Profile
"""
return self._contingency_factor_prof
@contingency_factor_prof.setter
def contingency_factor_prof(self, val: Union[ProfileFloat, np.ndarray]):
if isinstance(val, ProfileFloat):
self._contingency_factor_prof = val
elif isinstance(val, np.ndarray):
self._contingency_factor_prof.set(arr=val)
else:
raise Exception(str(type(val)) + 'not supported to be set into a contingency_factor_prof')
[docs]
def get_contingency_factor_at(self, t: int | None) -> float:
"""
:param t:
:return:
"""
return get_at(self.contingency_factor, self.contingency_factor_prof, t)
@property
def protection_rating_factor_prof(self) -> ProfileFloat:
"""
Cost profile
:return: Profile
"""
return self._protection_rating_factor_prof
@protection_rating_factor_prof.setter
def protection_rating_factor_prof(self, val: Union[ProfileFloat, np.ndarray]):
if isinstance(val, ProfileFloat):
self._protection_rating_factor_prof = val
elif isinstance(val, np.ndarray):
self._protection_rating_factor_prof.set(arr=val)
else:
raise Exception(str(type(val)) + 'not supported to be set into a protection_rating_factor_prof')
[docs]
def get_protection_rating_factor_at(self, t: int | None) -> float:
"""
:param t:
:return:
"""
return get_at(self.protection_rating_factor, self.protection_rating_factor_prof, t)
@property
def Cost_prof(self) -> ProfileFloat:
"""
Cost profile
:return: Profile
"""
return self._Cost_prof
@Cost_prof.setter
def Cost_prof(self, val: Union[ProfileFloat, np.ndarray]):
if isinstance(val, ProfileFloat):
self._Cost_prof = val
elif isinstance(val, np.ndarray):
self._Cost_prof.set(arr=val)
else:
raise Exception(str(type(val)) + 'not supported to be set into a Cost_prof')
[docs]
def get_Cost_at(self, t: int | None) -> float:
"""
:param t:
:return:
"""
return get_at(self.Cost, self.Cost_prof, t)
@property
def temp_oper_prof(self) -> ProfileFloat:
"""
Cost profile
:return: Profile
"""
return self._temp_oper_prof
@temp_oper_prof.setter
def temp_oper_prof(self, val: Union[ProfileFloat, np.ndarray]):
if isinstance(val, ProfileFloat):
self._temp_oper_prof = val
elif isinstance(val, np.ndarray):
self._temp_oper_prof.set(arr=val)
else:
raise Exception(str(type(val)) + 'not supported to be set into a temp_oper_prof')
[docs]
def get_temp_oper_at(self, t: int | None) -> float:
"""
:param t:
:return:
"""
return get_at(self.temp_oper, self._temp_oper_prof, t)
@property
def design_rate(self):
"""
Rate (MVA)
:return:
"""
return self._design_rate
@design_rate.setter
def design_rate(self, val: float):
val = float(val)
if isinstance(val, float):
self._design_rate = val
else:
raise ValueError(f'{val} is not a float')
@property
def rate(self):
"""
Rate (MVA)
:return:
"""
return self._rate
@rate.setter
def rate(self, val: float):
val = float(val)
if isinstance(val, float):
self._rate = val
else:
raise ValueError(f'{val} is not a float')
@property
def contingency_factor(self):
"""
Rate (MVA)
:return:
"""
return self._contingency_factor
@contingency_factor.setter
def contingency_factor(self, val: float):
val = float(val)
if isinstance(val, float):
self._contingency_factor = val
else:
raise ValueError(f'{val} is not a float')
@property
def protection_rating_factor(self):
"""
Rate (MVA)
:return:
"""
return self._protection_rating_factor
@protection_rating_factor.setter
def protection_rating_factor(self, val: float):
val = float(val)
if isinstance(val, float):
self._protection_rating_factor = val
else:
raise ValueError(f'{val} is not a float')
[docs]
def get_max_bus_nominal_voltage(self):
"""
GEt the maximum nominal voltage
:return:
"""
return max(self.bus_from.Vnom, self.bus_to.Vnom)
[docs]
def get_min_bus_nominal_voltage(self):
"""
Get the minimum nominal voltage
:return:
"""
return min(self.bus_from.Vnom, self.bus_to.Vnom)
[docs]
def get_sorted_buses_voltages(self):
"""
Get the sorted bus voltages
:return: high voltage, low voltage
"""
bus_f_v = self.bus_from.Vnom
bus_t_v = self.bus_to.Vnom
if bus_f_v > bus_t_v:
return bus_f_v, bus_t_v
else:
return bus_t_v, bus_f_v
[docs]
def get_buses_sorted_by_voltage(self):
"""
Get the sorted buses
:return: HV bus, LV bus
"""
bus_f_v = self.bus_from.Vnom
bus_t_v = self.bus_to.Vnom
if bus_f_v >= bus_t_v:
return self.bus_from, self.bus_to
else:
return self.bus_to, self.bus_from
[docs]
def get_virtual_taps(self) -> Tuple[float, float]:
"""
Get the branch virtual taps
The virtual taps generate when a line nominal voltage ate the two connection buses differ
Returns:
**tap_f** (float, 1.0): Virtual tap at the *from* side
**tap_t** (float, 1.0): Virtual tap at the *to* side
"""
# resolve how the transformer is actually connected and set the virtual taps
bus_f_v = self.bus_from.Vnom
bus_t_v = self.bus_to.Vnom
if bus_f_v == bus_t_v:
return 1.0, 1.0
else:
if bus_f_v > 0.0 and bus_t_v > 0.0:
return 1.0, bus_f_v / bus_t_v
else:
return 1.0, 1.0
[docs]
def get_coordinates(self):
"""
Get the line defining coordinates
"""
return [self.bus_from.get_coordinates(), self.bus_to.get_coordinates()]
[docs]
def convertible_to_vsc(self):
"""
Is this line convertible to VSC?
:return:
"""
if self.bus_to is not None and self.bus_from is not None:
# connectivity:
# for the later primitives to make sense, the "bus from" must be AC and the "bus to" must be DC
if self.bus_from.is_dc and not self.bus_to.is_dc: # this is the correct sense
return True
elif not self.bus_from.is_dc and self.bus_to.is_dc: # opposite sense, revert
return True
else:
return False
else:
return False
@property
def Vf(self) -> float:
"""
Get the voltage "from" (kV)
:return: get the nominal voltage from
"""
return self.bus_from.Vnom
@property
def Vt(self) -> float:
"""
Get the voltage "to" (kV)
:return: get the nominal voltage to
"""
return self.bus_to.Vnom
[docs]
def get_substation_from(self) -> Union[Substation, None]:
"""
Try to get the substation at the From side
:return: Union[Substation, None]
"""
if self.bus_from is not None:
return self.bus_from.substation
else:
return None
[docs]
def get_substation_to(self) -> Union[Substation, None]:
"""
Try to get the substation at the To side
:return: Union[Substation, None]
"""
if self.bus_to is not None:
return self.bus_to.substation
else:
return None
[docs]
def get_voltage_level_from(self) -> Union[VoltageLevel, None]:
"""
Try to get the voltage level at the From side
:return: Union[VoltageLevel, None]
"""
if self.bus_from is not None:
return self.bus_from.voltage_level
else:
return None
[docs]
def get_voltage_level_to(self) -> Union[VoltageLevel, None]:
"""
Try to get the voltage level at the To side
:return: Union[VoltageLevel, None]
"""
if self.bus_to is not None:
return self.bus_to.voltage_level
else:
return None
[docs]
def get_from_and_to_objects(self) -> Tuple[CONNECTION_TYPE, CONNECTION_TYPE, bool]:
"""
Get the from and to connection objects of the branch
:return: Object from, Object to, is it ok?
"""
# Pick the right bus
bus_from = self.bus_from
bus_to = self.bus_to
ok = bus_from is not None and bus_to is not None
return bus_from, bus_to, ok
[docs]
def get_weight(self) -> float:
"""
Get a weight of this line for graph purposes
:return: weight value
"""
return 1.0
[docs]
def get_bus_pos(self, bus: Bus) -> int:
"""
Get the bus specified position
:param bus:
:return:
"""
if bus == self.bus_from:
return self.bus_from_pos
elif bus == self.bus_to:
return self.bus_to_pos
else:
return 0
[docs]
def reassign_bus(self, old_bus: Bus, new_bus: Bus):
"""
Re-assign a bus
:param old_bus: bus where this branch is supposedly connected (either from or to)
:param new_bus: new bus to connect to
"""
if self.bus_from == old_bus:
self.bus_from = new_bus
elif self.bus_to == old_bus:
self.bus_to = new_bus
else:
pass
# Scalar property accessors coerce assignments to the declared schema types.
@property
def active(self) -> bool:
"""
Get ``active``.
:return: bool
"""
return self._active
@active.setter
def active(self, val: bool) -> None:
"""
Set ``active``.
:param val: Value to assign.
:return: None
"""
self._active = bool(val)
@property
def reducible(self) -> bool:
"""
Get ``reducible``.
:return: bool
"""
return self._reducible
@reducible.setter
def reducible(self, val: bool) -> None:
"""
Set ``reducible``.
:param val: Value to assign.
:return: None
"""
self._reducible = bool(val)
@property
def monitor_loading(self) -> bool:
"""
Get ``monitor_loading``.
:return: bool
"""
return self._monitor_loading
@monitor_loading.setter
def monitor_loading(self, val: bool) -> None:
"""
Set ``monitor_loading``.
:param val: Value to assign.
:return: None
"""
self._monitor_loading = bool(val)
@property
def mttf(self) -> float:
"""
Get ``mttf``.
:return: float
"""
return self._mttf
@mttf.setter
def mttf(self, val: float) -> None:
"""
Set ``mttf``.
:param val: Value to assign.
:return: None
"""
self._mttf = float(val)
@property
def mttr(self) -> float:
"""
Get ``mttr``.
:return: float
"""
return self._mttr
@mttr.setter
def mttr(self, val: float) -> None:
"""
Set ``mttr``.
:param val: Value to assign.
:return: None
"""
self._mttr = float(val)
@property
def Cost(self) -> float:
"""
Get ``Cost``.
:return: float
"""
return self._Cost
@Cost.setter
def Cost(self, val: float) -> None:
"""
Set ``Cost``.
:param val: Value to assign.
:return: None
"""
self._Cost = float(val)
@property
def capex(self) -> float:
"""
Get ``capex``.
:return: float
"""
return self._capex
@capex.setter
def capex(self, val: float) -> None:
"""
Set ``capex``.
:param val: Value to assign.
:return: None
"""
self._capex = float(val)
@property
def opex(self) -> float:
"""
Get ``opex``.
:return: float
"""
return self._opex
@opex.setter
def opex(self, val: float) -> None:
"""
Set ``opex``.
:param val: Value to assign.
:return: None
"""
self._opex = float(val)
@property
def bus_from_pos(self) -> int:
"""
Get ``bus_from_pos``.
:return: int
"""
return self._bus_from_pos
@bus_from_pos.setter
def bus_from_pos(self, val: int) -> None:
"""
Set ``bus_from_pos``.
:param val: Value to assign.
:return: None
"""
self._bus_from_pos = int(val)
@property
def bus_to_pos(self) -> int:
"""
Get ``bus_to_pos``.
:return: int
"""
return self._bus_to_pos
@bus_to_pos.setter
def bus_to_pos(self, val: int) -> None:
"""
Set ``bus_to_pos``.
:param val: Value to assign.
:return: None
"""
self._bus_to_pos = int(val)
@property
def temp_base(self) -> float:
"""
Get ``temp_base``.
:return: float
"""
return self._temp_base
@temp_base.setter
def temp_base(self, val: float) -> None:
"""
Set ``temp_base``.
:param val: Value to assign.
:return: None
"""
self._temp_base = float(val)
@property
def temp_oper(self) -> float:
"""
Get ``temp_oper``.
:return: float
"""
return self._temp_oper
@temp_oper.setter
def temp_oper(self, val: float) -> None:
"""
Set ``temp_oper``.
:param val: Value to assign.
:return: None
"""
self._temp_oper = float(val)
@property
def alpha(self) -> float:
"""
Get ``alpha``.
:return: float
"""
return self._alpha
@alpha.setter
def alpha(self, val: float) -> None:
"""
Set ``alpha``.
:param val: Value to assign.
:return: None
"""
self._alpha = float(val)