# 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
import pandas as pd
from typing import Union, Tuple
from matplotlib import pyplot as plt
import numpy as np
from VeraGridEngine.Devices.Substation.bus import Bus
from VeraGridEngine.Devices.Parents.branch_parent import BranchParent
from VeraGridEngine.Devices.Profiles import ProfileFloat
from VeraGridEngine.enumerations import DeviceType, BuildStatus, SubObjectType, PrpCat
from VeraGridEngine.Devices.Branches.line_locations import LineLocations
from VeraGridEngine.Devices.Parents.editable_device import GCProp
[docs]
class DcLine(BranchParent):
__slots__ = (
'measurements',
'_length',
'tolerance',
'_r_fault',
'_fault_pos',
'_R',
'template',
'_locations',
)
LOCAL_PROPERTY_DECLARATIONS: Tuple[GCProp, ...] = (
GCProp(
prop_name='R',
units='p.u.',
tpe=float,
definition='Total positive sequence resistance.',
cat=[PrpCat.PF],
),
GCProp(
prop_name='length',
units='km',
tpe=float,
definition='Length of the line (not used for calculation)',
cat=[PrpCat.TP],
),
GCProp(
prop_name='r_fault',
units='p.u.',
tpe=float,
definition='Resistance of the mid-line fault.Used in short circuit studies.',
cat=[PrpCat.SC],
),
GCProp(
prop_name='fault_pos',
units='p.u.',
tpe=float,
definition='Per-unit positioning of the fault:0 would be at the "from" side,1 would '
'be at the "to" side,therefore 0.5 is at the middle.',
cat=[PrpCat.SC],
),
GCProp(
prop_name='template',
units='',
tpe=DeviceType.AnyLineTemplateDevice,
definition='',
editable=False,
cat=[PrpCat.TP],
),
GCProp(
prop_name='locations',
units='',
tpe=SubObjectType.LineLocations,
definition='',
editable=False,
cat=[PrpCat.TP],
),
)
def __init__(self,
bus_from: Union[Bus, None] = None,
bus_to: Union[Bus, None] = None,
name: str = 'Dc Line',
idtag: Union[str, None] = None,
code: str = '',
r=1e-20,
design_rate: float = 9999,
rate=9999.0,
active=True,
tolerance=0,
cost=0.0,
mttf=0,
mttr=0,
r_fault=0.0,
fault_pos=0.5,
length=1,
temp_base=20,
temp_oper=20,
alpha=0.00330,
template=None,
contingency_factor=1.0,
protection_rating_factor: float = 1.4,
contingency_enabled=True,
monitor_loading=True,
capex=0,
opex=0,
build_status: BuildStatus = BuildStatus.Commissioned):
"""
DC current line
:param bus_from: Bus from
:param bus_to: Bus to
:param name: Name of the branch
:param idtag: UUID code
:param code: secondary ID
:param r: resistance in p.u.
:param design_rate: Design rate (MW)
:param rate: Branch rating (MW)
:param active: is it active?
:param tolerance: Tolerance specified for the branch impedance in %
:param cost: Cost of overload (e/MW)
:param mttf: Mean time too failure
:param mttr: Mean time to repair
:param r_fault: Fault resistance
:param fault_pos: Fault position
:param length: Length (km)
:param temp_base: base temperature (i.e 25ΒΊ Β°C)
:param temp_oper: Operational temperature (Β°C)
:param alpha: Thermal constant of the material (Β°C)
:param template: Basic branch template
:param contingency_factor: Rating factor in case of contingency
:param contingency_enabled: enabled for contingencies (Legacy)
:param monitor_loading: monitor the loading (used in OPF)
:param capex: Cost of investment (e/MW)
:param opex: Cost of operation (e/MWh)
:param build_status: build status (now time)
"""
BranchParent.__init__(self,
name=name,
idtag=idtag,
code=code,
bus_from=bus_from,
bus_to=bus_to,
active=active,
reducible=False,
design_rate=design_rate,
rate=rate,
contingency_factor=contingency_factor,
protection_rating_factor=protection_rating_factor,
contingency_enabled=contingency_enabled,
monitor_loading=monitor_loading,
mttf=mttf,
mttr=mttr,
build_status=build_status,
capex=capex,
opex=opex,
cost=cost,
temp_base=temp_base,
temp_oper=temp_oper,
alpha=alpha,
device_type=DeviceType.DCLineDevice)
# List of measurements
self.measurements = list()
# line length in km
self._length = float(length)
# line impedance tolerance
self.tolerance = float(tolerance)
# short circuit impedance
self.r_fault = float(r_fault)
self.fault_pos = float(fault_pos)
# total impedance and admittance in p.u.
self.R = float(r) if r is not None else 0.0001
# type template
self.template = template
# Line locations
self._locations: LineLocations = LineLocations()
@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')
@property
def locations(self) -> LineLocations:
"""
Cost profile
:return: Profile
"""
return self._locations
@locations.setter
def locations(self, val: Union[LineLocations, np.ndarray]):
if isinstance(val, LineLocations):
self._locations = val
elif isinstance(val, np.ndarray):
self._locations.set(data=val)
else:
raise Exception(str(type(val)) + 'not supported to be set into a locations')
@property
def R_corrected(self):
"""
Returns a temperature corrected resistance based on a formula provided by:
NFPA 70-2005, National Electrical Code, Table 8, footnote #2; and
https://en.wikipedia.org/wiki/Electrical_resistivity_and_conductivity#Linear_approximation
(version of 2019-01-03 at 15:20 EST).
"""
return self.R * (1 + self.alpha * (self.temp_oper - self.temp_base))
@property
def length(self) -> float:
"""
Line length in km
:return: float
"""
return self._length
@length.setter
def length(self, val: float):
val = float(val)
if isinstance(val, float):
if val > 0.0:
if self._length != 0:
factor = np.round(val / self._length, 6) # new length / old length
self.R *= factor
self._length = val
else:
# print('The length cannot be zero, ignoring value')
pass
else:
raise Exception('The length must be a float value')
[docs]
def change_base(self, Sbase_old, Sbase_new):
"""
:param Sbase_old:
:param Sbase_new:
"""
b = Sbase_new / Sbase_old
self.R *= b
[docs]
def get_weight(self):
"""
:return:
"""
return self.R
[docs]
def copy(self, bus_dict=None):
"""
Returns a copy of the dc line
@return: A new with the same content as this
"""
if bus_dict is None:
f = self.bus_from
t = self.bus_to
else:
f = bus_dict[self.bus_from]
t = bus_dict[self.bus_to]
b = DcLine(bus_from=f,
bus_to=t,
name=self.name,
r=self.R,
rate=self.rate,
active=self.active,
mttf=self.mttf,
mttr=self.mttr,
temp_base=self.temp_base,
temp_oper=self.temp_oper,
alpha=self.alpha,
template=self.template)
b.measurements = self.measurements
b.active_prof = self.active_prof
b.rate_prof = self.rate_prof
return b
# def get_save_data(self):
# """
# Return the data that matches the edit_headers
# :return:
# """
# data = list()
# for name, properties in self.registered_properties.items():
# obj = getattr(self, name)
#
# if obj is None:
# data.append("")
# else:
#
# if hasattr(obj, 'idtag'):
# obj = obj.idtag
# else:
# if properties.tpe not in [str, float, int, bool]:
# obj = str(obj)
# else:
# obj = str(obj)
#
# data.append(obj)
# return data
[docs]
def plot_profiles(self, time_series=None, my_index=0, show_fig=True):
"""
Plot the time series results of this object
:param time_series: TimeSeries Instance
:param my_index: index of this object in the simulation
:param show_fig: Show the figure?
"""
if time_series is not None:
fig = plt.figure(figsize=(12, 8))
ax_1 = fig.add_subplot(211)
ax_2 = fig.add_subplot(212, sharex=ax_1)
x = time_series.results.time_array
# loading
y = time_series.results.loading * 100.0
df = pd.DataFrame(data=y[:, my_index], index=x, columns=[self.name])
ax_1.set_title('Loading', fontsize=14)
ax_1.set_ylabel('Loading [%]', fontsize=11)
df.plot(ax=ax_1)
# losses
y = time_series.results.losses
df = pd.DataFrame(data=y[:, my_index], index=x, columns=[self.name])
ax_2.set_title('Losses', fontsize=14)
ax_2.set_ylabel('Losses [MVA]', fontsize=11)
df.plot(ax=ax_2)
plt.legend()
fig.suptitle(self.name, fontsize=20)
if show_fig:
plt.show()
[docs]
def get_coordinates(self):
"""
Get the branch defining coordinates
"""
return [self.bus_from.get_coordinates(), self.bus_to.get_coordinates()]
# Scalar property accessors coerce assignments to the declared schema types.
@property
def R(self) -> float:
"""
Get ``R``.
:return: float
"""
return self._R
@R.setter
def R(self, val: float) -> None:
"""
Set ``R``.
:param val: Value to assign.
:return: None
"""
self._R = float(val)
@property
def r_fault(self) -> float:
"""
Get ``r_fault``.
:return: float
"""
return self._r_fault
@r_fault.setter
def r_fault(self, val: float) -> None:
"""
Set ``r_fault``.
:param val: Value to assign.
:return: None
"""
self._r_fault = float(val)
@property
def fault_pos(self) -> float:
"""
Get ``fault_pos``.
:return: float
"""
return self._fault_pos
@fault_pos.setter
def fault_pos(self, val: float) -> None:
"""
Set ``fault_pos``.
:param val: Value to assign.
:return: None
"""
self._fault_pos = float(val)