################################################################################
"""
Visualizations Specifically for Electrical Engineering.
Filled with plotting functions and visualization tools for electrical engineers,
this module is designed to assist engineers visualize their designs.
"""
################################################################################
import cmath as _c
import numpy as _np
import matplotlib as _matplotlib
import matplotlib.pyplot as _plt
from electricpy import powerset, geometry
from electricpy.geometry import Point
from electricpy.geometry.circle import Circle
# Define Power Triangle Function
[docs]
def powertriangle(
P=None, Q=None, S=None, PF=None, color="red", text="Power Triangle", printval=False
):
"""
Power Triangle Plotting Function.
This function is designed to draw a power triangle given
values for the complex power system.
.. image:: /static/PowerTriangle.png
Parameters
----------
P: float
Real Power, unitless, default=None
Q: float
Reactive Power, unitless, default=None
S: float
Apparent Power, unitless, default=None
PF: float
Power Factor, unitless, provided as a decimal value, lagging is
positive, leading is negative; default=None
color: string, optional
The color of the power triangle lines, default="red"
text: string, optional
The title of the power triangle plot, default="Power Triangle"
printval: bool, optional
Control argument to allow the numeric values to be printed on
the plot, default="False"
Returns
-------
matplotlib.pyplot: Plotting object to be used for additional configuration
or plotting.
"""
# Calculate all values if not all are provided
if P is None or Q is None or S is None or PF is None:
P, Q, S, PF = powerset(P, Q, S, PF)
# Generate Lines
p_line_x = [0, P]
p_line_y = [0, 0]
q_line_x = [P, P]
q_line_y = [0, Q]
s_line_x = [0, P]
s_line_y = [0, Q]
# Plot Power Triangle
_plt.figure(1)
_plt.title(text)
_plt.plot(p_line_x, p_line_y, color=color)
_plt.plot(q_line_x, q_line_y, color=color)
_plt.plot(s_line_x, s_line_y, color=color)
_plt.xlabel("Real Power (W)")
_plt.ylabel("Reactive Power (VAR)")
maximum = max(abs(P), abs(Q))
if P > 0:
_plt.xlim(0, maximum * 1.1)
x = maximum
else:
_plt.xlim(-maximum * 1.1, 0)
x = -maximum
if Q > 0:
_plt.ylim(0, maximum * 1.1)
y = maximum
else:
_plt.ylim(-maximum * 1.1, 0)
y = -maximum
if PF > 0:
power_factor_text = "Lagging"
else:
power_factor_text = "Leading"
# Print all values if asked to
if printval:
_plt.text(
x / 20,
y * 4 / 5,
(
f"P: {P} W\n"
f"Q: {Q} VAR\n"
f"S: {S} VA\n"
f"PF: {abs(PF)} {power_factor_text}\n"
f"ΘPF: {_np.degrees(_np.arccos(PF))}° {power_factor_text}"
),
color=color,
)
return _plt
# Define Convolution Bar-Graph Function:
[docs]
def convbar(h, x, outline=True):
"""
Convolution Bar-Graph Plotter Function.
Generates plots of each of two input arrays as bar-graphs, then
generates a convolved bar-graph of the two inputs to demonstrate
and illustrate convolution, typically for an educational purpose.
Examples
--------
>>> import numpy as np
>>> import electricpy.visu as visu
>>> h = np.array([0, 1, 1, 1, 0])
>>> x = np.array([0, 1, 1, 1, 0])
>>> visu.convbar(h, x)
.. image:: /static/convbar-example.png
Parameters
----------
h: numpy.ndarray
Impulse Response - Given as Array (Prefferably Numpy Array)
x: numpy.ndarray
Input Function - Given as Array (Prefferably Numpy Array)
"""
# The impulse response
M = len(h)
t = _np.arange(M)
# Plot
_plt.subplot(121)
if outline:
_plt.plot(t, h, color="red")
_plt.bar(t, h, color="black")
_plt.xticks([0, 5, 9])
_plt.ylabel("h")
_plt.title("Impulse Response")
_plt.grid()
# The input function
N = len(x)
s = _np.arange(N)
# Plot
_plt.subplot(122)
if outline:
_plt.plot(s, x, color="red")
_plt.bar(s, x, color="black")
_plt.xticks([0, 10, 19])
_plt.title("Input Function")
_plt.grid()
_plt.ylabel("x")
# The output
L = M + N - 1
w = _np.arange(L)
_plt.figure(3)
y = _np.convolve(h, x)
if outline:
_plt.plot(w, y, color="red")
_plt.bar(w, y, color="black")
_plt.ylabel("y")
_plt.grid()
_plt.title("Convolved Output")
return _plt
# Define Phasor Plot Generator
[docs]
def phasorplot(
phasors,
title="Phasor Diagram",
legend=False,
bg=None,
colors=None,
radius=None,
linewidth=None,
size=None,
label=False,
labels=False,
tolerance=None,
):
"""
Phasor Plotting Function.
This function is designed to plot a phasor-diagram with angles in degrees
for up to 12 phasor sets (more may be used if additional colors are set).
Phasors must be passed as a complex number set, (e.g.
[ m+ja, m+ja, m+ja, ... , m+ja ] ).
Examples
--------
>>> import numpy as np
>>> from electricpy import phasors
>>> from electricpy import visu
>>> voltages = np.array([
... [67,0],
... [45,-120],
... [52,120]
... ])
>>> phasors = phasors.phasorlist(voltages)
>>> plt = visu.phasorplot(phasors, colors=["red", "green", "blue"])
>>> plt.show()
.. image:: /static/PhasorPlot.png
Parameters
----------
phasors: list of complex
The set of phasors to be plotted.
title: string, optional
The Plot Title, default="Phasor Diagram"
legend: bool, optional
Control argument to enable displaying the legend, must be passed
as an array or list of strings. `label` and `labels` are mimic-
arguments and will perform similar operation, default=False
bg: string, optional
Background-Color control, default="#d5de9c"
radius: float, optional
The diagram radius, unless specified, automatically scales
colors: list of str, optional
List of hexidecimal color strings denoting the line colors to
use.
size: float, optional
Control argument for figure size. default=None
linewidth: float, optional
Control argument to declare the line thickness. default=None
tolerance: float, optional
Minimum magnitude to plot, anything less than tolerance will be
plotted as a single point at the origin, by default, the
tolerance is scaled to be 1/25-th the maximum radius. To disable
the tolerance, simply provide either False or -1.
Returns
-------
matplotlib.pyplot: Plotting object to be used for additional configuration
or plotting.
"""
# Load Complex Values if Necessary
try:
len(phasors)
except TypeError:
phasors = [phasors]
# Manage Colors
if colors is None:
colors = [
"#FF0000",
"#800000",
"#FFFF00",
"#808000",
"#00ff00",
"#008000",
"#00ffff",
"#008080",
"#0000ff",
"#000080",
"#ff00ff",
"#800080",
]
# Scale Radius
if radius is None:
radius = _np.abs(phasors).max()
# Set Tolerance
if tolerance is None:
tolerance = radius / 25
elif tolerance is False:
tolerance = -1
# Set Background Color
if bg is None:
bg = "#FFFFFF"
# Load labels if handled in other argument
if label:
legend = label
if labels:
legend = labels
# Check for more phasors than colors
if len(phasors) > len(colors):
raise ValueError("ERROR: Too many phasors provided. Specify more line colors.")
if size is None:
# Force square figure and square axes
width, height = _matplotlib.rcParams["figure.figsize"]
size = min(width, height)
# Make a square figure
fig = _plt.figure(figsize=(size, size))
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True, facecolor=bg)
_plt.grid(True)
# Plot the diagram
_plt.title(title + "\n")
arrows = []
for i, phasor in enumerate(phasors):
mag, ang_r = _c.polar(phasor)
# Plot with labels
if legend:
if mag > tolerance:
arrows.append(
_plt.arrow(
0,
0,
ang_r,
mag,
color=colors[i],
label=legend[i],
linewidth=linewidth,
)
)
else:
arrows.append(
_plt.plot(
0,
0,
"o",
markersize=linewidth * 3,
label=legend[i],
color=colors[i],
)
)
# Plot without labels
else:
_plt.arrow(0, 0, ang_r, mag, color=colors[i], linewidth=linewidth)
if legend:
_plt.legend(arrows, legend)
# Set Minimum and Maximum Radius Terms
ax.set_rmax(radius)
ax.set_rmin(0)
return _plt
[docs]
class InductionMotorCircle:
"""
Plot Induction Motor Circle Diagram.
This class is designed to plot induction motor circle diagram
and plot circle diagram to obtain various parameters of induction motor.
Examples
--------
>>> from electricpy.visu import InductionMotorCircle
>>> open_circuit_test_data = {'V0': 400, 'I0': 9, 'W0': 1310}
>>> blocked_rotor_test_data = {'Vsc': 200, 'Isc': 50, 'Wsc': 7100}
>>> ratio = 1 # stator copper loss/ rotor copper loss
>>> output_power = 15000
>>> InductionMotorCircle(
... no_load_data=open_circuit_test_data,
... blocked_rotor_data=blocked_rotor_test_data,
... output_power=output_power,
... torque_ration=ratio,
... frequency=50,
... poles=4
... )
.. image:: /static/InductionMotorCircleExample.png
Parameters
----------
no_load_data: dict {'V0', 'I0', 'W0'}
V0: no load test voltage
I0: no load current in rotor
W0: No load power(in Watts)
blocked_rotor_data: dict {'Vsc','Isc','Wsc'}
Vsc: blocked rotor terminal voltage
Isc: blocked rotor current in rotor
Wsc: Power consumed in blocked rotor test
output_power: int
Desired power output from the induction motor
torque_ration: float
Ration between rotor resistance to stator resistance
(i.e., R2/R1)
frequency: int
AC supply frequency
poles: int
Pole count of induction Motor
"""
[docs]
def __init__(
self,
no_load_data,
blocked_rotor_data,
output_power,
torque_ration=1,
frequency=50,
poles=4,
):
"""Primary Entrypoint."""
self.no_load_data = no_load_data
self.blocked_rotor_data = blocked_rotor_data
self.frequency = frequency
self.operating_power = output_power
self.torque_ratio = torque_ration
self.poles = poles
self.sync_speed = 120 * frequency / poles # rpm
v0 = no_load_data["V0"]
i0 = no_load_data["I0"]
w0 = no_load_data["W0"]
self.no_load_pf = w0 / (_np.sqrt(3) * v0 * i0)
theta0 = _np.arccos(self.no_load_pf)
# get short circuit power factor and Current at slip=1
vsc = blocked_rotor_data["Vsc"]
isc = blocked_rotor_data["Isc"]
wsc = blocked_rotor_data["Wsc"]
self.blocked_rotor_pf = wsc / (_np.sqrt(3) * vsc * isc)
theta_sc = _np.arccos(self.blocked_rotor_pf)
# because V is on Y axis
theta0 = _np.pi / 2 - theta0
theta_sc = _np.pi / 2 - theta_sc
# isc is the current at reduced voltage
# calculate current at rated voltage
isc = v0 * isc / vsc
self.no_load_line = [[0, i0 * _np.cos(theta0)], [0, i0 * _np.sin(theta0)]]
self.full_load_line = [
[0, isc * _np.cos(theta_sc)],
[0, isc * _np.sin(theta_sc)],
]
# secondary current line
self.secondary_current_line = [
[i0 * _np.cos(theta0), isc * _np.cos(theta_sc)],
[i0 * _np.sin(theta0), isc * _np.sin(theta_sc)],
]
[[x1, x2], [y1, y2]] = self.secondary_current_line
self.theta = _np.arctan((y2 - y1) / (x2 - x1))
# get the induction motor circle
self.power_scale = w0 / (i0 * _np.sin(theta0))
self.center, self.radius = self.compute_circle_params()
[self.center_x, self.center_y] = self.center
self.p_max = self.radius * _np.cos(self.theta) - (
self.radius - self.radius * _np.sin(self.theta)
) * _np.tan(self.theta)
self.torque_line, self.torque_point = self.get_torque_line()
self.torque_max, self.torque_max_x, self.torque_max_y = self.get_torque_max()
# Take low slip point
_, [self.power_x, self.power_y] = self.get_output_power()
self.data = self.compute_efficiency()
def __call__(self):
# noqa: D102
__doc__ = self.__doc__
return self.data
def plot(self):
"""Plot the Induction Motor Circle Diagram."""
[circle_x, circle_y] = InductionMotorCircle.__get_circle(
self.center, self.radius, semi=True
)
_plt.plot(circle_x, circle_y)
InductionMotorCircle.__plot_line(self.no_load_line)
InductionMotorCircle.__plot_line(self.secondary_current_line)
InductionMotorCircle.__plot_line(self.full_load_line, ls="-.")
InductionMotorCircle.__plot_line(self.torque_line, ls="-.")
# Full load output
_plt.plot(
[self.secondary_current_line[0][1], self.secondary_current_line[0][1]],
[self.secondary_current_line[1][1], self.center_y],
)
# Diameter of the circle
_plt.plot(
[self.center_x - self.radius, self.center_x + self.radius],
[self.center_y, self.center_y],
ls="-.",
)
# Max torque line
_plt.plot(
[self.center_x, self.torque_max_x],
[self.center_y, self.torque_max_y],
ls="-.",
)
# Max Output Power line
_plt.plot(
[self.center_x, self.center_x - self.radius * _np.sin(self.theta)],
[self.center_y, self.center_y + self.radius * _np.cos(self.theta)],
ls="-.",
)
# Operating Point
_plt.plot([0, self.power_x], [0, self.power_y], c="black")
_plt.scatter(
self.power_x, self.power_y, marker="X", c="red", label="_nolegend_"
)
# mark the center of the circle
_plt.scatter(
self.center_x, self.center_y, marker="*", c="blue", label="_nolegend_"
)
_plt.scatter(
self.center_x - self.radius * _np.sin(self.theta),
self.center_y + self.radius * _np.cos(self.theta),
linewidths=3,
c="black",
marker="*",
label="_nolegend_",
)
_plt.scatter(
self.torque_max_x,
self.torque_max_y,
linewidths=3,
c="black",
marker="*",
label="_nolegend_",
)
_plt.title("Induction Motor Circle Diagram")
_plt.grid()
_plt.legend(
[
"I2 locus",
"No Load Current",
"Output Line",
"Blocked Rotor Current",
"Torque line",
"Full Load Losses",
"Diameter",
"Maximum Torque",
"Maximum Output Power",
f"Operating Power {self.operating_power}",
]
)
return _plt
def compute_efficiency(self):
"""Compute the output efficiency of induction motor."""
[[_, no_load_x], [_, no_load_y]] = self.no_load_line
no_load_losses = no_load_y * self.power_scale
compute_slope = InductionMotorCircle.compute_slope
torque_slope = compute_slope(self.torque_line)
stator_cu_loss = (self.power_x - no_load_x) * torque_slope * self.power_scale
rotor_current_slope = compute_slope(self.secondary_current_line)
total_cu_loss = (
(self.power_x - no_load_x) * rotor_current_slope * self.power_scale
)
rotor_cu_loss = total_cu_loss - stator_cu_loss
rotor_output = self.power_y * self.power_scale - (
rotor_cu_loss + stator_cu_loss + no_load_losses
)
slip = rotor_cu_loss / rotor_output
self.rotor_speed = self.sync_speed * (1 - slip)
data = {
"no_load_loss": no_load_losses,
"rotor_copper_loss": rotor_cu_loss,
"stator_copper_loss": stator_cu_loss,
"rotor_output": rotor_output,
"slip": slip,
"stator_rmf_speed (RPM)": self.sync_speed,
"rotor_speed (RMP)": self.rotor_speed,
"power_factor": (
self.power_y / _np.sqrt(self.power_x**2 + self.power_y**2)
),
"efficiency": f"{rotor_output * 100 / (self.power_y * self.power_scale)} %",
}
return data
@staticmethod
def __get_circle(center, radius, semi=False):
"""
Determine parametric equation of circle.
Parameters
----------
center: list[float, float] [x0, y0]
radius: float
Returns
-------
(x, y): tuple
parametric equation of circle
(x = x0 + r*cos(theta) ; y = y0 + r*sin(theta))
"""
[x0, y0] = center
if semi:
theta = _np.arange(0, _np.pi, 1e-4)
else:
theta = _np.arange(0, _np.pi * 2, 1e-4)
x = x0 + radius * _np.cos(theta)
y = y0 + radius * _np.sin(theta)
return x, y
@staticmethod
def __plot_line(line, mark_start=True, mark_end=True, ls="-", marker=None):
"""Supporting function to plot a line."""
[x, y] = line
[x1, x2] = x
[y1, y2] = y
_plt.plot(x, y, ls=ls)
if mark_start:
_plt.scatter(x1, y1, marker=marker, label="_nolegend_")
if mark_end:
_plt.scatter(x2, y2, marker=marker, label="_nolegend_")
def compute_circle_params(self):
"""Compute the parameters of induction motor circle."""
[[x1, x2], [y1, y2]] = self.secondary_current_line
theta = _np.arctan((y2 - y1) / (x2 - x1))
length = _np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
radius = length / (2 * _np.cos(theta))
center = [radius + x1, y1]
return center, radius
def get_torque_line(self):
"""Obtain the torque line of the induction motor."""
[[x1, x2], [y1, y2]] = self.secondary_current_line
y = (self.torque_ratio * y2 + y1) / (self.torque_ratio + 1)
torque_point = [x2, y]
torque_line = [[x1, x2], [y1, y]]
return torque_line, torque_point
def get_torque_max(self):
"""Compute max torque for given Induction Motor parameters."""
[x, y] = self.torque_line
[x1, x2] = x
[y1, y2] = y
alpha = _np.arctan((y2 - y1) / (x2 - x1))
torque_max = self.radius * _np.cos(alpha) - (
self.radius - self.radius * _np.sin(alpha)
) * _np.tan(alpha)
torque_max_x = self.center_x - self.radius * _np.sin(alpha)
torque_max_y = self.center_y + self.radius * _np.cos(alpha)
return torque_max, torque_max_x, torque_max_y
@staticmethod
def compute_slope(line):
"""
Compute slope of the line.
Parameters
----------
line: list[float, float]
Returns
-------
slope: float
"""
[[x1, x2], [y1, y2]] = line
return (y2 - y1) / (x2 - x1)
def get_output_power(self):
"""
Determine induction motor circle desired output power point.
Obtain the point on the induction motor circle diagram which
corresponds to the desired output power
"""
[[x1, x2], [y1, y2]] = self.secondary_current_line
alpha = _np.arctan((y2 - y1) / (x2 - x1))
[center_x, center_y] = self.center
[[_, no_load_x], [_, _]] = self.no_load_line
beta = _np.arcsin(
(
self.operating_power / self.power_scale
+ (center_x - no_load_x) * _np.tan(alpha)
)
* _np.cos(alpha)
/ self.radius
)
beta_0 = alpha + beta
beta_1 = -alpha + beta
# high slip
p_x_1 = center_x + self.radius * _np.cos(beta_0)
p_y_1 = center_y + self.radius * _np.sin(beta_0)
# low slip
p_x_2 = center_x - self.radius * _np.cos(beta_1)
p_y_2 = center_y + self.radius * _np.sin(beta_1)
return [p_x_1, p_y_1], [p_x_2, p_y_2]
[docs]
class PowerCircle:
r"""
Plot Power Circle Diagram of Transmission System.
This class is designed to plot the power circle diagram of a transmission
system both sending and receiving ends.
Examples
--------
>>> import math, cmath
>>> from electricpy import visu
>>> visu.PowerCircle(
... power_circle_type="receiving",
... A=cmath.rect(0.895, math.radians(1.4)),
... B=cmath.rect(182.5, math.radians(78.6)),
... Vr=cmath.rect(215, 0),
... Pr=50,
... power_factor=-0.9
... )
.. image:: /static/ReceivingPowerCircleExample.png
Parameters
----------
power_circle_type: ["sending", "receiving"]
Type of power circle diagram to plot.
Vr: complex
Transmission Line Receiving End Voltage (phasor complex
value)
Vs: complex
Transmission Line Sending End Voltage (phasor complex
value)
power_factor: float
Power Factor of the transmission system, default = None
Pr: float
Receiving End Real Power, default = None
Qr: float
Receiving End Reactive Power, default = None
Sr: complex
Receiving End Total Complex Power, default = None
Ps: float
Sending End Real Power, default = None
Qs: float
Sending End Reactive Power, default = None
Ss: complex
Sending End Total Complex Power, default = None
A: float
Transmission System ABCD Parameters, A, default = None
B: float
Transmission System ABCD Parameters, B, default = None
C: float
Transmission System ABCD Parameters, C, default = None
D: float
Transmission System ABCD Parameters, D, default = None
"""
[docs]
def __init__(
self,
power_circle_type: str,
power_factor: float = None,
Vr: complex = None,
Vs: complex = None,
Pr: float = None,
Qr: float = None,
Sr: complex = None,
Ps: float = None,
Qs: float = None,
Ss: complex = None,
A: complex = None,
B: complex = None,
C: complex = None,
D: complex = None,
) -> None:
r"""Initialize the class."""
if C is not None:
assert (
abs(A * D - B * C - 1) < 1e-6
), "ABCD Matrix is not a valid ABCD Matrix"
if power_circle_type.lower() == "receiving":
if A is not None and B is not None and Vr is not None:
(
self.radius,
self.center,
self.operating_point,
) = PowerCircle._build_circle(
A, B, "receiving_end", Vr, Pr, Qr, Sr, power_factor, Vs
)
else:
raise ValueError("Not enough attributes to build circle")
elif power_circle_type.lower() == "sending":
if B is not None and D is not None and Vs is not None:
(
self.radius,
self.center,
self.operating_point,
) = PowerCircle._build_circle(
D, B, "sending_end", Vs, Ps, Qs, Ss, power_factor, Vr
)
else:
raise ValueError("Not enough attributes to build power circle")
else:
raise ValueError("Invalid power circle type")
self.circle = Circle(self.center, self.radius)
self.parameters = locals()
@staticmethod
def _build_circle(
a1, a2, circle_type, V, P=None, Q=None, S=None, power_factor=None, V_ref=None
):
k = (abs(V) ** 2) * abs(a1) / abs(a2)
alpha = _c.phase(a1)
beta = _c.phase(a2)
if circle_type == "receiving_end":
center = Point(-k * _c.cos(alpha - beta), -k * _c.sin(alpha - beta))
elif circle_type == "sending_end":
center = Point(k * _c.cos(alpha - beta), -k * _c.sin(alpha - beta))
if V_ref is not None and P is not None and Q is not None:
radius = abs(V) * abs(V_ref) / (abs(a2))
operation_point = Point(P, Q)
elif V_ref is not None and S is not None:
radius = abs(V) * abs(V_ref) / (abs(a2))
operation_point = Point(S.real, S.imag)
elif P is not None and Q is not None:
radius = geometry.distance(center, Point(P, Q))
operation_point = Point(P, Q)
elif S is not None:
radius = geometry.distance(center, Point(S.real, S.imag))
operation_point = Point(S.real, S.imag)
elif P is not None and power_factor is not None:
Q = P * _c.sqrt(1 / power_factor**2 - 1).real
if power_factor < 0:
Q = -1 * Q
radius = geometry.distance(center, Point(P, Q))
operation_point = Point(P, Q)
elif Q is not None and power_factor is not None:
P = Q / _c.sqrt(1 / power_factor**2 - 1).real
radius = geometry.distance(center, Point(P, Q))
operation_point = Point(P, Q)
else:
raise AttributeError("Not enough attributes found to perform calculation")
return radius, center, operation_point
def _cal_parameters(self, type1, type2):
if self.parameters["V" + type2] is None:
self.parameters["V" + type2] = (
abs(self.parameters["B"]) * self.radius / self.parameters["V" + type1]
)
if self.parameters["P" + type1] is None:
self.parameters["P" + type1] = self.operating_point.x
if self.parameters["Q" + type1] is None:
self.parameters["Q" + type1] = self.operating_point.y
if self.parameters["S" + type1] == None:
self.parameters["S" + type1] = (
self.operating_point.x + 1j * self.operating_point.y
)
if self.parameters["power_factor"] is None:
self.parameters["power_factor"] = (
self.operating_point.y / self.operating_point.x
)
if type1 == "r" and type2 == "s":
self.parameters["Vs"] = (
self.parameters["B"] * self.parameters["Sr"]
+ self.parameters["A"] * abs(self.parameters["Vr"]) ** 2
)
self.parameters["Vs"] = (
self.parameters["Vs"] / self.parameters["Vr"].conjugate()
)
elif type1 == "s" and type2 == "r":
self.parameters["Vr"] = (
-self.parameters["B"] * self.parameters["Ss"]
+ self.parameters["D"] * abs(self.parameters["Vs"]) ** 2
)
self.parameters["Vr"] = (
self.parameters["Vr"] / self.parameters["Vs"].conjugate()
)
def print_data(self):
r"""Print the data of the circle."""
if self.operating_point is None:
return self.center, self.radius
if self.parameters["power_circle_type"] == "receiving":
self._cal_parameters("r", "s")
if self.parameters["power_circle_type"] == "sending":
self._cal_parameters("s", "r")
for key, value in self.parameters.items():
print(key, " => ", value)
def __call__(self) -> dict:
r"""Return the data of the circle."""
if self.parameters["power_circle_type"] == "receiving":
self._cal_parameters("r", "s")
if self.parameters["power_circle_type"] == "sending":
self._cal_parameters("s", "r")
return self.parameters
def plot(self):
r"""Plot the circle."""
circle_x = []
circle_y = []
for data in self.circle.parametric_equation(theta_resolution=1e-5):
[x, y] = data
circle_x.append(x)
circle_y.append(y)
c_x = self.center.x
c_y = self.center.y
op_x = self.operating_point.x
op_y = self.operating_point.y
# plot Circle and Diameter
_plt.plot(circle_x, circle_y)
_plt.plot([c_x - self.radius, c_x + self.radius], [c_y, c_y], "g--")
_plt.plot([c_x, c_x], [c_y - self.radius, c_y + self.radius], "g--")
_plt.plot([c_x, op_x], [c_y, op_y], "y*-.")
_plt.plot([op_x, op_x], [op_y, c_y], "b*-.")
_plt.scatter(op_x, op_y, marker="*", color="r")
_plt.title(f"{self.parameters['power_circle_type'].capitalize()} Power Circle")
_plt.xlabel("Active Power")
_plt.ylabel("Reactive Power")
_plt.grid()
return _plt
[docs]
def receiving_end_power_circle(
Vr: complex = None,
A: complex = None,
B: complex = None,
Pr: float = None,
Qr: float = None,
Sr: complex = None,
power_factor: float = None,
Vs: complex = None,
) -> PowerCircle:
"""
Construct Receiving End Power Circle.
Examples
--------
>>> import math, cmath
>>> from electricpy import visu
>>> visu.receiving_end_power_circle(
... A=cmath.rect(0.895, math.radians(1.4)),
... B=cmath.rect(182.5, math.radians(78.6)),
... Vr=cmath.rect(215, 0),
... Pr=50,
... power_factor=-0.9
... )
.. image:: /static/ReceivingEndPowerCircleExample.png
Parameters
----------
Vr: complex
Receiving End Voltage, default = None.
A: complex
Transmission System ABCD Parameters, A, default = None.
B: complex,
Transmission System ABCD Parameters, B, default = None.
Pr: float, optional
Receiving End Real Power, default = None
Qr: float, optional
Receiving End Reactive Power, default = None
Sr: complex, optional
Receiving End Apparent Power, default = None
power_factor: float, optional
System End Power Factor, default = None
Vs: complex, optional
Sending End Voltage, default = None
Returns
-------
Receiving End Power Circle: PowerCircle
"""
try:
assert Vr is not None and A is not None and B is not None
except AssertionError:
raise ValueError(
"Not enough attributes to build Receiving end power circle at least"
" provide `Vr`, `A`, `B`"
)
if not (
(
(Pr is not None and Qr is not None)
or (Sr is not None and power_factor is not None)
)
or (
(Pr is not None and power_factor is not None)
or (Qr is not None and power_factor is not None)
)
):
raise ValueError(
"Not enough attributes for marking an operating point on Receiving "
"End Power Circle"
)
return PowerCircle(
"receiving",
**{
"Vr": Vr,
"A": A,
"B": B,
"Pr": Pr,
"Qr": Qr,
"Sr": Sr,
"Vs": Vs,
"power_factor": power_factor,
},
)
[docs]
def sending_end_power_circle(
Vs: complex = None,
B: complex = None,
D: complex = None,
Ps: float = None,
Qs: float = None,
Ss: complex = None,
power_factor: float = None,
Vr: complex = None,
) -> PowerCircle:
"""
Construct Receiving End Power Circle.
Parameters
----------
Vs: complex
Sending End Voltage
B: complex
Transmission System ABCD Parameters, A
D: complex
Transmission System ABCD Parameters, B
Ps: float, optional
Sending End Real Power, default = None
Qs: float, optional
Sending End Reactive Power, default = None
Ss: complex, optional
Sending End Apparent Power, default = None
power_factor: float, optional
System Power Factor, default = None
Vr: complex, optional
Receiving End Voltage, default = None
Returns
-------
Sending End Power Circle: PowerCircle
"""
if not (Vs is not None and B is not None and D is not None):
raise ValueError(
"Not enough attributes to build Sending end power circle at least "
"provide `Vs`, `B`, `D`"
)
if not (
(
(Ps is not None and Qs is not None)
or (Ss is not None and power_factor is not None)
)
or (
(Ps is not None and power_factor is not None)
or (Qs is not None and power_factor is not None)
)
):
raise ValueError(
"Not enough attributes for marking an operating point on Sending "
"End Power Circle"
)
return PowerCircle(
"sending",
**{
"Vr": Vr,
"B": B,
"D": D,
"Ps": Ps,
"Qs": Qs,
"Ss": Ss,
"Vs": Vs,
"power_factor": power_factor,
},
)
[docs]
class SeriesRLC():
r"""
Frequency Response for an RLC (Resistive, Inductive, Capacitive) Load.
Generate unique information about an RLC circuit. Using this class, you may
generate a variety of useful statistics including resonance frequency,
bandwidth, lower and upper cuttoff frequencies, and more. Each of the
specific parameters are evaluated as follows.
**Resonance Frequency:**
.. math:: \text{resonance_frequency} = \frac{1}{\sqrt{L * C} \cdot 2 \pi}
**Bandwidth:**
.. math:: \text{bandwidth} = \frac{R}{L \cdot 2 \pi}
**Quality Factor:**
.. math:: \text{quality_factor} = 2\pi \frac{\text{freq}}{R}
Given the characteristics listed below, and the Python code described in the
associated example, the following plot will be generated.
* Resistance: 5 ohms
* Inductance: 0.4 henreys
* Capacitance: 25.3e-6 farads
* Frequency: 50 Hz
.. image:: /static/series-rlc-r5-l0.4.png
* Resistance: 10 ohms
* Inductance: 0.5 henreys
* Capacitance: 25.3e-6 farads
* Frequency: 50 Hz
.. image:: /static/series-rlc-r10-l0.5.png
Examples
--------
>>> from electricpy.visu import SeriesRLC
>>> rlc_component = SeriesRLC(
... resistance=5, inductance=0.4, capacitance=25.3e-6, frequency=50
... )
>>> rlc_component.resonance_frequency
50.029927713857425
>>> rlc_component.bandwidth
1.9894367886486917
>>> plot_1 = rlc_component.graph(
... lower_frequency_cut=0.1, upper_frequency_cut=100, samples=1000
... )
>>> plot_1.show()
>>> plot_2 = SeriesRLC(
... resistance=10, inductance=0.5, capacitance=25.3e-6, frequency=50
... ).graph(
... lower_frequency_cut=0.1, upper_frequency_cut=100, samples=1000,
... show_legend=True,
... )
>>> plot_2.show()
Parameters
----------
resistance: float
Resistance (in Ohm) of the circuit.
inductance: float
Inductance (in Henry) of the circuit.
capacitance: float
Capacitance (in Hz) of the circuit.
frequency: float
Frequency (in Hz) at which the output gain should be
evaluated.
"""
[docs]
def __init__(
self,
resistance: float,
inductance: float,
capacitance: float,
frequency: float
) -> None:
"""Form the Frequency Response Analysis System."""
self.resistance = resistance
self.inductance = inductance
self.capacitance = capacitance
self.frequency = frequency
@property
def resonance_frequency(self):
"""Resonance Frequency (in Hz) of the Described RLC Circuit."""
return 1 / (_np.sqrt(self.inductance * self.capacitance) * 2 * _np.pi)
@property
def bandwidth(self):
"""Bandwidth of the Described RLC Circuit."""
return self.resistance / (2 * _np.pi * self.inductance)
@property
def quality_factor(self):
"""Quality Factor of the Described RLC Circuit."""
return 2 * _np.pi * self.frequency / self.resistance
@property
def lower_cutoff_frequency(self):
"""Lower Cutoff Frequency (in Hz) of the Described RLC Circuit."""
x = (-self.resistance) / (2 * self.inductance)
resonance_angular_frequency = 2 * _np.pi * self.resonance_frequency
return (x + _np.sqrt(x**2 + resonance_angular_frequency**2)) / (2 * _np.pi)
@property
def upper_cutoff_frequency(self):
"""Upper Cutoff Frequency (in Hz) of the Described RLC Circuit."""
x = (self.resistance) / (2 * self.inductance)
resonance_angular_frequency = 2 * _np.pi * self.resonance_frequency
return (x + _np.sqrt(x**2 + resonance_angular_frequency**2)) / (2 * _np.pi)
def output_gain(self, frequency: float):
"""
Evaluate Output Gain of Described RLC Circuit at a Particular Frequency.
Parameters
----------
frequency: float
Frequency (in Hz) at which the output gain should be
evaluated.
"""
ang_frq = 2 * _np.pi * frequency
current_impedence = (
self.resistance**2
+ (ang_frq * self.inductance - 1 / (ang_frq * self.capacitance)) ** 2
)
return (self.resistance) / (_np.sqrt(current_impedence))
def legend(self):
"""Generate a Legend for the Graph."""
f1, f2 = self.lower_cutoff_frequency, self.upper_cutoff_frequency
f = self.resonance_frequency
return [
"Gain",
f"Resonance frequency ({f}Hz)",
f"Lower cutoff frequency ({f1}Hz)",
f"Upper cutoff frequency ({f2}Hz)",
f"Bandwidth ({f2 - f1}Hz)",
f"Quality factor {self.quality_factor}",
]
def graph(
self,
lower_frequency_cut: float,
upper_frequency_cut: float,
samples: int = 10000,
show_legend: bool = False,
):
"""
Generate a Plot to Represent all Data Respective of the RLC Circuit.
Parameters
----------
lower_frequency_cut: float
Minimum frequency to demonstrate as a boundary
of the X-axis of the plot.
upper_frequency_cut: float
Maximum frequency to demonstrate as a boundary
of the X-axis of the plot.
samples: float, optional
Number of samples over which the plot should be
formed. Defaults to 1000.
show_legend: bool, optional
Control to enable or disable the display of the
legend. Defaults to False.
"""
x = _np.linspace(lower_frequency_cut, upper_frequency_cut, samples)
y = self.output_gain(x)
_plt.title("Frequency response of series RLC circuit")
_plt.grid(visible=True)
_plt.plot(x, y, label="Gain")
_plt.ylabel("Gain")
_plt.xlabel("Frequency (Hz)")
f1, f2 = self.lower_cutoff_frequency, self.upper_cutoff_frequency
f = self.resonance_frequency
_plt.scatter(
[f1, f2],
[_np.sqrt(0.5), _np.sqrt(0.5)],
marker="*",
c="black",
label="_nolegend_",
)
half_power_gain = _np.sqrt(0.5)
_plt.plot([f, f], [0, 1], ls="-.")
_plt.plot([f1, f1], [half_power_gain, 0], ls="-.")
_plt.plot([f2, f2], [half_power_gain, 0], ls="-.")
_plt.plot([f1, f2], [half_power_gain, half_power_gain], ls="-.")
_plt.plot(
[0, f], [half_power_gain, half_power_gain], label="_nolegend_", ls="--"
)
_plt.plot([0, f], [1, 1], label="_nolegend_", ls="--")
_plt.plot([0, 0], [half_power_gain, 1], label="Quality factor", c="black")
_plt.scatter([0], [half_power_gain], label="_nolegend_", c="black", marker="v")
_plt.scatter([0], [1], label="_nolegend_", c="black", marker="^")
if show_legend:
_plt.legend(self.legend(), loc='best')
return _plt
# END