Skip to content

Commit 4a91b4a

Browse files
committed
Fix phasorplot legend handling, linewidth bugs, and bode improvements
1 parent 68a6972 commit 4a91b4a

3 files changed

Lines changed: 142 additions & 69 deletions

File tree

electricpy/bode.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ def _sys_condition(system, feedback):
2222
num = system[0]
2323
den = system[1]
2424
# Convolve numerator or denominator as needed
25-
if str(type(num)) == tuple:
25+
if isinstance(num, tuple):
2626
num = convolve(num) # Convolve terms in numerator
27-
if str(type(den)) == tuple:
27+
if isinstance(den, tuple):
2828
den = convolve(den) # Convolve terms in denominator
2929
if feedback: # If asked to add the numerator to the denominator
3030
ld = len(den) # Length of denominator
@@ -99,11 +99,21 @@ def bode(system, mn=0.001, mx=1000, npts=100, title="", xlim=False, ylim=False,
9999

100100
# Condition min and max freq terms
101101
degrees = False
102-
if freqaxis.lower().find("deg") != -1: # degrees requested
102+
# Backwards-compatible: historically "deg" here behaved like "Hz"
103+
if freqaxis.lower().find("hz") != -1 or freqaxis.lower().find("deg") != -1:
103104
degrees = True
104-
# Scale Degrees to Radians for calculation
105+
# Scale Hz to rad/sec for calculation
105106
mn = 2 * _np.pi * mn
106107
mx = 2 * _np.pi * mx
108+
109+
# Prevent invalid log10 values
110+
if mn <= 0:
111+
mn = 1e-12
112+
if mx <= 0:
113+
mx = 1e-12
114+
if mx <= mn:
115+
raise ValueError("mx must be greater than mn")
116+
107117
mn = _np.log10(mn) # find the _exponent value
108118
mx = _np.log10(mx) # find the _exponent value
109119

@@ -113,14 +123,14 @@ def bode(system, mn=0.001, mx=1000, npts=100, title="", xlim=False, ylim=False,
113123
# Calculate the bode system
114124
w, mag, ang = _sig.bode(system, wover)
115125

116-
def _plot(plot_title, y_label):
126+
def _plot(plot_title, y_label, ydata):
117127
_plt.title(plot_title)
118128
_plt.ylabel(y_label)
119-
if degrees: # Plot in degrees
120-
_plt.plot(w / (2 * _np.pi), ang)
129+
if degrees: # Plot in Hz
130+
_plt.plot(w / (2 * _np.pi), ydata)
121131
_plt.xlabel("Frequency (Hz)")
122132
else: # Plot in radians
123-
_plt.plot(w, ang)
133+
_plt.plot(w, ydata)
124134
_plt.xlabel("Frequency (rad/sec)")
125135
_plt.xscale("log")
126136
_plt.grid(which="both")
@@ -129,12 +139,12 @@ def _plot(plot_title, y_label):
129139
if ylim:
130140
_plt.ylim(ylim)
131141
if sv:
132-
_plt.savefig(title + ".png")
142+
_plt.savefig(plot_title + ".png")
133143

134144
# Plot Magnitude
135145
if magnitude:
136146
magTitle = "Magnitude " + title
137-
_plot(magTitle, "Magnitude (DB)")
147+
_plot(magTitle, "Magnitude (DB)", mag)
138148
if disp3db:
139149
_plt.axhline(-3)
140150
if lowcut is not None:
@@ -144,7 +154,7 @@ def _plot(plot_title, y_label):
144154
# Plot Angle
145155
if angle:
146156
angTitle = "Angle " + title
147-
_plot(angTitle, "Angle (degrees)")
157+
_plot(angTitle, "Angle (degrees)", ang)
148158
_plt.show()
149159

150160

@@ -200,8 +210,14 @@ def sbode(f, NN=1000, title="", xlim=False, ylim=False, mn=0, mx=1000,
200210
angle: bool, optional
201211
Control argument to enable plotting of angle, default=True
202212
"""
203-
W = _np.linspace(mn, mx, NN)
204-
H = _np.zeros(NN, dtype=_np.complex)
213+
# Avoid log(0) on semilog plots
214+
if mn <= 0:
215+
mn_plot = 1e-12
216+
else:
217+
mn_plot = mn
218+
219+
W = _np.linspace(mn_plot, mx, NN)
220+
H = _np.zeros(NN, dtype=complex)
205221

206222
for n in range(0, NN):
207223
s = 1j * W[n]
@@ -278,17 +294,23 @@ def zbode(f, dt=0.01, NN=1000, title="", mn=0, mx=2 * _pi, xlim=False, ylim=Fals
278294
angle: bool, optional
279295
Control argument to enable plotting of angle, default=True
280296
"""
281-
phi = _np.linspace(mn, mx, NN)
297+
# Avoid log(0) on semilog plots
298+
if mn <= 0:
299+
mn_plot = 1e-12
300+
else:
301+
mn_plot = mn
302+
303+
phi = _np.linspace(mn_plot, mx, NN)
282304

283-
H = _np.zeros(NN, dtype=_np.complex)
305+
H = _np.zeros(NN, dtype=complex)
284306
for n in range(0, NN):
285307
z = _exp(1j * phi[n])
286308
if approx is not False and callable(approx):
287309
# Approximated Z-Domain
288310
s = approx(z, dt) # Pass current z-value and dt
289311
H[n] = f(s)
290312
else: # Z-Domain Transfer Function Provided
291-
H[n] = dt * f(z)
313+
H[n] = f(z)
292314

293315
if magnitude:
294316
_plt.semilogx((180 / _pi) * phi, 20 * _np.log10(abs(H)), 'k')

electricpy/phasors.py

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,18 @@ def phasorz(C=None, L=None, freq=60, complex=True):
127127
Z: complex
128128
The ohmic impedance of either C or L (respectively).
129129
"""
130+
if (C is None) and (L is None):
131+
raise ValueError("Either C or L must be provided.")
132+
if (C is not None) and (L is not None):
133+
raise ValueError("Provide only one of C or L, not both.")
130134
w = 2 * _np.pi * freq
135+
if w == 0:
136+
raise ValueError("freq must be non-zero.")
137+
131138
# C Given in ohms, return as Z
132139
if C is not None:
140+
if C == 0:
141+
raise ValueError("C must be non-zero.")
133142
Z = -1 / (w * C)
134143
# L Given in ohms, return as Z
135144
if L is not None:
@@ -181,8 +190,6 @@ def phasorlist(arr):
181190
electricpy.phasors.vectarray: Magnitude/Angle Array Pairing Function
182191
electricpy.phasors.phasorz: Impedance Phasor Generator
183192
"""
184-
# Use List Comprehension to Process
185-
186193
# Return Array
187194
return _np.array([phasor(i) for i in arr])
188195

@@ -223,14 +230,10 @@ def vectarray(arr, degrees=True, flatarray=False):
223230
electricpy.phasors.phasor: Phasor Generating Function
224231
electricpy.phasors.phasorlist: Phasor Generator for List or Array
225232
"""
226-
# Iteratively Append Arrays to the Base
227-
228233
def vector_cast(num):
229234
mag, ang = _c.polar(num)
230-
231235
if degrees:
232236
ang = _np.degrees(ang)
233-
234237
return [mag, ang]
235238

236239
polararr = _np.array([vector_cast(num) for num in arr])
@@ -277,10 +280,13 @@ def phasordata(mn, mx=None, npts=1000, mag=1, ang=0, freq=60,
277280
The resultant data array.
278281
"""
279282
# Test Inputs for Min/Max
280-
if mx == None:
283+
if mx is None:
281284
# No Minimum provided, use Value as Maximum
282285
mx = mn
283286
mn = 0
287+
npts = int(npts)
288+
if npts <= 0:
289+
raise ValueError("npts must be a positive integer.")
284290
# Generate Omega
285291
w = 2 * _np.pi * freq
286292
# Generate Time Array
@@ -313,7 +319,7 @@ def compose(*arr):
313319
314320
- [ real, imag]
315321
- [ [ real1, ..., realn ], [ imag1, ..., imagn ] ]
316-
- [ [ real1, imag1 ], ..., [ realn, imagn ] ]
322+
- [ [ real1, imag1 ], ..., [ realn, imag2 ] ]
317323
318324
Will always return values in form:
319325
@@ -347,7 +353,7 @@ def compose(*arr):
347353
raise ValueError("Invalid Array Shape, must be 2xN or Nx2.")
348354
# Successfully Generated Array, Return
349355
return (retarr)
350-
except: # 1-Dimension Array
356+
except Exception: # 1-Dimension Array
351357
length = arr.size
352358
# Test for invalid Array Size
353359
if length != 2:
@@ -380,31 +386,33 @@ def parallelz(*args):
380386
Zp: complex
381387
The calculated parallel impedance of the input tuple.
382388
"""
383-
# Gather length (number of elements in tuple)
384-
L = len(args)
385-
if L == 1:
386-
Z = args[0] # Only One Tuple Provided
387-
try:
388-
L = len(Z)
389-
if L == 1:
390-
Zp = Z[0] # Only one impedance, burried in tuple
391-
else:
392-
# Inversely add the first two elements in tuple
393-
Zp = (1 / Z[0] + 1 / Z[1]) ** (-1)
394-
# If there are more than two elements, add them all inversely
395-
if L > 2:
396-
for i in range(2, L):
397-
Zp = (1 / Zp + 1 / Z[i]) ** (-1)
398-
except ValueError or IndexError:
399-
Zp = Z # Only one impedance
389+
# Normalize input: allow parallelz([Z1,Z2,...]) or parallelz(Z1,Z2,...)
390+
if len(args) == 0:
391+
raise ValueError("At least one impedance must be provided.")
392+
if len(args) == 1 and isinstance(args[0], (tuple, list, _np.ndarray)):
393+
Z = args[0]
400394
else:
401-
Z = args # Set of Args acts as Tuple
402-
# Inversely add the first two elements in tuple
403-
Zp = (1 / Z[0] + 1 / Z[1]) ** (-1)
404-
# If there are more than two elements, add them all inversely
405-
if L > 2:
406-
for i in range(2, L):
407-
Zp = (1 / Zp + 1 / Z[i]) ** (-1)
395+
Z = args
396+
397+
try:
398+
L = len(Z)
399+
except Exception:
400+
return Z
401+
402+
if L == 0:
403+
raise ValueError("At least one impedance must be provided.")
404+
if L == 1:
405+
return Z[0]
406+
407+
# Inverse-sum method with explicit zero checks
408+
invsum = 0
409+
for Zi in Z:
410+
if Zi == 0:
411+
raise ValueError("Impedance values must be non-zero for parallel combination.")
412+
invsum += 1 / Zi
413+
if invsum == 0:
414+
raise ValueError("Invalid impedances: reciprocal sum evaluates to zero.")
415+
Zp = 1 / invsum
408416
return Zp
409417

410418
# END

0 commit comments

Comments
 (0)