import matplotlib.lines as mlines
import numpy as np
from relsad.StatDist import StatDist, StatDistType, UniformParameters
from relsad.Time import Time, TimeUnit
from relsad.utils import convert_yearly_fail_rate, random_choice
from .Bus import Bus
from .Component import Component
[docs]class Line(Component):
r"""
A class used to represent an electrical Line
...
Attributes
----------
name : str
Name of the line
is_backup : bool
Decides if the line is a backup line
fbus : Bus
Sending bus
tbus : Bus
Receiving bus
disconnectors : list
List of disconnectors connected to the line
circuitbreaker : CircuitBreaker
Circuit breaker connected to the line
parent_network : PowerNetwork
The parent network of the line
section : Section
The section the line belongs to
s_ref : float
Reference apperet power [MVA]
v_ref : float
Reference voltage [kV]
r_ref : float
Reference resistance [Ohm]
r : float
Resistance [Ohm]
x : float
Reactance [Ohm]
r_pu : float
The pu value of the resistance
x_pu : float
The pu value of the reactance
area : float
The cross-sectional area [m^2]
rho : float
The resistivity of the line [Ohm*m]
length : float
Length of line [km]
capacity : float
The capacity of the line [MW]
ploss : float
The active power loss over the line [MW]
qloss : float
The reactive power loss over the line [MVar]
fail_rate_per_year : float
Failure rate per year [fault/year/km]
repair_time_dist : StatDist
The repair time of the line [hours/fault]
connected : bool
Indicates if the line is connected or disconnected
failed : bool
Failure status of the line
remaining_outage_time : Time
The remaining outage time of the line
sensor : Sensor
The sensor(s) connected to the line
history : dict
Dictonary attribute that stores the historic variables
Methods
-------
set_backup()
Sets the backup lines and opens the disconnectors connected to the backup line
disconnect()
Disconnects a line and removes the line for the list of lines
connect()
Connects a line and append the line to the list of lines
draw_repair_time(dt)
Decides and returns the repair time of the line based on a statistical distribution
fail(dt)
Sets the fail status of the line to True and opens the connected disconnectors and the connected circuit breaker
not_fail()
Sets the fail status of the line to False and closes the connected disconnectors and connected circuit breaker
change_direction()
Changes the direction of the line
update_fail_status(dt)
Updates the fail status of the line
get_line_load()
Returns the flow over the line in PU values
get_line_loading()
Returns the loading of the line in percentage
print_status()
Prints the line status
get_disconnetors()
Returns the connected disconnectors
add_parent_network(network)
Adds the parent network to the line
initialize_history()
Initialize the history variables
update_history(prev_time, curr_time, save_flag)
Updates the history variables
get_history(attribute)
Returns the history variables of an attribute
add_random_instance(random_gen)
Adds global random seed
reset_status()
Resets and sets the status of the class parameters
reset_load_flow_data()
Resets the variables used in the load flow analysis
get_switches()
Returns the switches on the line
"""
lineCount = 0
## Visual attributes
linestyle = "-"
handle = mlines.Line2D([], [], linestyle=linestyle)
## Random instance
ps_random: np.random.Generator = None
def __init__(
self,
name: str,
fbus: Bus,
tbus: Bus,
r: float, # Ohm
x: float, # Ohm
repair_time_dist: StatDist = StatDist(
stat_dist_type=StatDistType.UNIFORM_FLOAT,
parameters=UniformParameters(
min_val=0.0,
max_val=0.0,
),
),
s_ref: float = 1, # MVA
v_ref: float = 12.66, # kV
rho: float = 1.72e-8, # resistivity [Ohm*m]
area: float = 64.52e-6, # cross-sectional area [m**2]
fail_rate_density_per_year: float = 0.0, # fails/(km*year)
capacity: float = 100, # MW
connected=True,
):
# Verify input
if fbus is None:
raise Exception("Line must have a origin bus")
if tbus is None:
raise Exception("Line must have a target bus")
if r < 0:
raise Exception("r must be positive")
if x < 0:
raise Exception("x must be positive")
if repair_time_dist is None:
raise Exception("The line must have a repair time distribution")
if s_ref < 0:
raise Exception("s_ref must be positive")
if v_ref < 0:
raise Exception("v_ref must be positive")
if rho < 0:
raise Exception("The resistivity must be positive")
if area < 0:
raise Exception("The line section area must be positive")
if fail_rate_density_per_year < 0:
raise Exception(
"The failure rate density per year must be positive"
)
if capacity < 0:
raise Exception("The line capacity must be positive")
## Informative attributes
self.name = name
## Backup
self.is_backup = False
## Topological attributes
self.fbus = fbus
self.tbus = tbus
fbus.connected_lines.append(self)
tbus.connected_lines.append(self)
tbus.toline = self
tbus.toline_list.append(self)
fbus.fromline = self
fbus.fromline_list.append(self)
fbus.nextbus.append(self.tbus)
self.disconnectors = list()
self.circuitbreaker = None
self.parent_network = None
self.section = None
Line.lineCount += 1
## Power flow attributes
self.s_ref = s_ref
self.v_ref = v_ref
self.r_ref = v_ref**2 / s_ref
self.r = r
self.x = x
self.r_pu = r / self.r_ref
self.x_pu = x / self.r_ref
self.area = area
self.rho = rho
self.length = r * area / rho * 1e-3 # km
self.capacity = capacity # MW
self.ploss = 0.0
self.qloss = 0.0
## Reliabilility attributes
self.fail_rate_per_year = (
fail_rate_density_per_year * self.length
) # failures per year
self.repair_time_dist = repair_time_dist
## Status attribute
self.connected = connected
self.failed = False
self.remaining_outage_time = Time(0)
## Communication
self.sensor = None
## History
self.history = {}
self.initialize_history()
def __str__(self):
return self.name
def __repr__(self):
return f"Line(name={self.name})"
def __eq__(self, other):
if hasattr(other, "name"):
return self.name == other.name and isinstance(other, Line)
else:
return False
def __hash__(self):
return hash(self.name)
[docs] def set_backup(self):
"""
Sets the backup lines and opens the disconnectors connected to the backup line
Parameters
----------
None
Returns
----------
None
"""
self.is_backup = True
for discon in self.disconnectors:
discon.open()
[docs] def disconnect(self):
"""
Disconnects a line and removes the line from the list of lines
Parameters
----------
None
Returns
----------
None
"""
if self.connected:
self.connected = False
self.linestyle = "--"
self.fbus.fromline_list.remove(self)
if self.fbus.fromline == self:
if len(self.fbus.fromline_list) > 0:
self.fbus.fromline = next(iter(self.fbus.fromline_list))
else:
self.fbus.fromline = None
self.tbus.toline_list.remove(self)
if self.tbus.toline == self:
if len(self.tbus.toline_list) > 0:
self.tbus.toline = next(iter(self.tbus.toline_list))
else:
self.tbus.toline = None
self.fbus.nextbus.remove(self.tbus)
[docs] def connect(self):
"""
Connects a line and append the line to the list of lines
Parameters
----------
None
Returns
----------
None
"""
if not self.connected:
self.connected = True
self.linestyle = "-"
self.tbus.toline = self
self.tbus.toline_list.append(self)
self.fbus.fromline = self
self.fbus.fromline_list.append(self)
self.fbus.nextbus.append(self.tbus)
[docs] def draw_repair_time(self, dt: Time):
"""
Decides and returns the repair time of the line based on a statistical distribution
Parameters
----------
dt : Time
The current time step
Returns
----------
None
"""
return Time(
self.repair_time_dist.draw(
random_instance=self.ps_random,
size=1,
)[0],
dt.unit,
)
[docs] def fail(self, dt: Time):
"""
Sets the fail status of the line to False and opens the connected disconnectors and the connected circuit breaker
Parameters
----------
dt : Time
The current time step
Returns
----------
None
"""
self.failed = True
self.parent_network.failed_line = True
self.remaining_outage_time = self.draw_repair_time(dt)
if self.connected:
# Relay
# Disconnects parent network from its parent network
self.parent_network.connected_line.circuitbreaker.open()
# All child networks are disconnected from the parent network
if hasattr(self.parent_network, "child_network_list"):
if self.parent_network.child_network_list is not None:
for (
child_network
) in self.parent_network.child_network_list:
child_network.connected_line.circuitbreaker.open()
[docs] def not_fail(self):
"""
Sets the fail status of the line to False and closes the connected disconnectors and connected circuit breaker
Parameters
----------
None
Returns
----------
None
"""
if (
sum([line.failed for line in self.parent_network.lines]) == 1
and self.failed
):
self.parent_network.failed_line = False
self.failed = False
[docs] def change_direction(self):
"""
Changes the direction of the line
Parameters
----------
None
Returns
----------
None
"""
self.fbus.fromline_list.remove(self)
self.tbus.fromline_list.append(self)
self.tbus.toline_list.remove(self)
self.fbus.toline_list.append(self)
if self.fbus.fromline == self:
self.fbus.fromline = (
next(iter(self.fbus.fromline_list))
if len(self.fbus.fromline_list) > 0
else None
)
if self.tbus.toline == self:
self.tbus.toline = (
next(iter(self.tbus.toline_list))
if len(self.tbus.toline_list) > 0
else None
)
self.fbus.toline = self
self.tbus.fromline = self
self.fbus.nextbus.remove(self.tbus)
self.tbus.nextbus.append(self.fbus)
bus = self.fbus
self.fbus = self.tbus
self.tbus = bus
[docs] def update_fail_status(self, dt: Time):
"""
Updates the fail status of the line
Parameters
----------
dt : Time
The current time step
Returns
----------
None
"""
if self.is_backup:
for discon in self.disconnectors:
if not discon.is_open:
discon.open()
if self.failed:
self.remaining_outage_time -= dt
if self.remaining_outage_time <= Time(0):
self.not_fail()
self.parent_network.controller.check_components = True
self.remaining_outage_time = Time(0)
else:
p_fail = convert_yearly_fail_rate(self.fail_rate_per_year, dt)
if random_choice(self.ps_random, p_fail):
self.fail(dt)
else:
self.not_fail()
[docs] def get_line_load(self):
"""
Returns the flow over the line in PU values
Parameters
----------
None
Returns
----------
p_from : float
The active power sent from the line [MW]
q_from : float
The reactive power sent from the line [MVar]
p_to : float
The active power sent to the line [MW]
q_to : float
The reactive power sent to the line [MVar]
"""
def uij(gij, bij, tetai, tetaj):
return gij * np.sin(tetai - tetaj) - bij * np.cos(tetai - tetaj)
def tij(gij, bij, tetai, tetaj):
return gij * np.cos(tetai - tetaj) + bij * np.sin(tetai - tetaj)
def bij(R, X):
return (1.0 / complex(R, X)).imag
def gij(R, X):
return (1.0 / complex(R, X)).real
if self.connected:
fbus = self.fbus
tbus = self.tbus
bsh = 0.0 # No shunts included so far
teta1 = fbus.voang
teta2 = tbus.voang
v1 = fbus.vomag
v2 = tbus.vomag
b = bij(self.r_pu, self.x_pu)
g = gij(self.r_pu, self.x_pu)
p_from = g * v1 * v1 - v1 * v2 * tij(g, b, teta1, teta2)
p_to = g * v2 * v2 - v1 * v2 * tij(g, b, teta2, teta1)
q_from = -(b + bsh) * v1 * v1 - v1 * v2 * uij(g, b, teta1, teta2)
q_to = -(b + bsh) * v2 * v2 - v1 * v2 * uij(g, b, teta2, teta1)
return p_from, q_from, p_to, q_to
else:
return 0, 0, 0, 0
[docs] def get_line_loading(self):
"""
Returns the loading of the line in percentage
Parameters
----------
None
Returns
----------
line_loading : float
The line loading
"""
p_from = self.get_line_load()[0]
line_loading = abs(p_from) / (self.capacity / self.s_ref) * 100
return line_loading
[docs] def print_status(self):
"""
Prints the line status
Parameters
----------
None
Returns
----------
None
"""
print(
"name: {:5s}, failed={}, connected={}".format(
self.name, self.failed, self.connected
)
)
def get_disconnectors(self):
"""
Returns the connected disconnectors
Parameters
----------
None
Returns
----------
disconnectors : Disconnector
Disconnector connected to the line
"""
return self.disconnectors
[docs] def add_parent_network(self, network):
"""
Adds the parent network to the line
Parameters
----------
network : PowerNetwork
The parent network of the line
Returns
----------
None
"""
self.parent_network = network
[docs] def initialize_history(self):
"""
Initializes the history variables
Parameters
----------
None
Returns
----------
None
"""
self.history["p_from"] = {}
self.history["q_from"] = {}
self.history["p_to"] = {}
self.history["q_to"] = {}
self.history["remaining_outage_time"] = {}
self.history["failed"] = {}
self.history["line_loading"] = {}
[docs] def update_history(
self, prev_time: Time, curr_time: Time, save_flag: bool
):
"""
Updates the history variables
Parameters
----------
prev_time : Time
The previous time
curr_time : Time
The vurrent time
save_flag : bool
Indicates if saving is on or off
Returns
----------
None
"""
if save_flag:
time = curr_time.get_unit_quantity(curr_time.unit)
p_from, q_from, p_to, q_to = self.get_line_load()
self.history["p_from"][time] = p_from * self.s_ref
self.history["q_from"][time] = q_from * self.s_ref
self.history["p_to"][time] = p_to * self.s_ref
self.history["q_to"][time] = q_to * self.s_ref
self.history["remaining_outage_time"][
time
] = self.remaining_outage_time.get_unit_quantity(curr_time.unit)
self.history["failed"][time] = self.failed
self.history["line_loading"][time] = self.get_line_loading()
[docs] def get_history(self, attribute: str):
"""
Returns the history variables of an attribute
Parameters
----------
attribute : str
System attribute
Returns
----------
history[attribute] : dict
Returns the history variables of an attribute
"""
return self.history[attribute]
[docs] def add_random_instance(self, random_gen):
"""
Adds global random seed
Parameters
----------
random_gen : int
Random number generator
Returns
----------
None
"""
self.ps_random = random_gen
[docs] def reset_status(self, save_flag: bool):
"""
Resets and sets the status of the class parameters
Parameters
----------
None
Returns
----------
None
"""
self.remaining_outage_time = Time(0)
self.not_fail()
if save_flag:
self.initialize_history()
[docs] def reset_load_flow_data(self):
"""
Resets the variables used in the load flow analysis
Parameters
----------
None
Returns
----------
None
"""
self.ploss = 0
self.qloss = 0
[docs] def get_switches(self):
"""
Returns the switches on the line
Parameters
----------
None
Returns
----------
switches
The switches on the line
"""
switches = (
self.disconnectors + [self.circuitbreaker]
if self.circuitbreaker is not None
else self.disconnectors
)
return switches
if __name__ == "__main__":
pass