Skip to content

Commit b05492c

Browse files
committed
fixed remaining failing unit tests and cleanup of docfiles
1 parent 1a6d806 commit b05492c

File tree

9 files changed

+147
-101
lines changed

9 files changed

+147
-101
lines changed

control/frdata.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
# External function declarations
4848
from warnings import warn
4949
import numpy as np
50-
from numpy import angle, array, empty, ones, isin, \
50+
from numpy import angle, array, empty, ones, \
5151
real, imag, absolute, eye, linalg, where, dot, sort
5252
from scipy.interpolate import splprep, splev
5353
from .lti import LTI
@@ -354,23 +354,24 @@ def eval(self, omega, squeeze=True):
354354
355355
squeeze: bool, optional (default=True)
356356
If True and sys is single input, single output (SISO), return a
357-
1D array or scalar depending on omega's length.
357+
1D array or scalar the same length as omega.
358358
359359
"""
360360
omega_array = np.array(omega, ndmin=1) # array-like version of omega
361361
if any(omega_array.imag > 0):
362362
raise ValueError("FRD.eval can only accept real-valued omega")
363363

364364
if self.ifunc is None:
365-
elements = isin(self.omega, omega)
365+
elements = np.isin(self.omega, omega) # binary array
366366
if sum(elements) < len(omega_array):
367367
raise ValueError(
368-
"not all frequencies omega are in frequency list of FRD "
369-
"system. Try an interpolating FRD for additional points.")
368+
'''not all frequencies omega are in frequency list of FRD
369+
system. Try an interpolating FRD for additional points.''')
370370
else:
371371
out = self.fresp[:, :, elements]
372372
else:
373-
out = empty((self.outputs, self.inputs, len(omega_array)))
373+
out = empty((self.outputs, self.inputs, len(omega_array)),
374+
dtype=complex)
374375
for i in range(self.outputs):
375376
for j in range(self.inputs):
376377
for k, w in enumerate(omega_array):

control/lti.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,14 @@ def evalfr(sys, x, squeeze=True):
465465
466466
.. todo:: Add example with MIMO system
467467
"""
468+
out = sys.horner(x)
469+
if not hasattr(x, '__len__'):
470+
# received a scalar x, squeeze down the array along last dim
471+
out = np.squeeze(out, axis=2)
468472
if squeeze and issiso(sys):
469-
return sys.horner(x)[0][0]
470-
return sys.horner(x)
473+
return out[0][0]
474+
else:
475+
return out
471476

472477

473478
def freqresp(sys, omega, squeeze=True):

control/margins.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,15 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
213213
# a bit coarse, have the interpolated frd evaluated again
214214
def _mod(w):
215215
"""Calculate |G(jw)| - 1"""
216-
return np.abs(sys(1j * w)[0][0]) - 1
216+
return np.abs(sys(1j * w)) - 1
217217

218218
def _arg(w):
219219
"""Calculate the phase angle at -180 deg"""
220-
return np.angle(-sys(1j * w)[0][0])
220+
return np.angle(-sys(1j * w))
221221

222222
def _dstab(w):
223223
"""Calculate the distance from -1 point"""
224-
return np.abs(sys(1j * w)[0][0] + 1.)
224+
return np.abs(sys(1j * w) + 1.)
225225

226226
# Find all crossings, note that this depends on omega having
227227
# a correct range
@@ -232,7 +232,7 @@ def _dstab(w):
232232

233233
# find the phase crossings ang(H(jw) == -180
234234
widx = np.where(np.diff(np.sign(_arg(sys.omega))))[0]
235-
widx = widx[np.real(sys(1j * sys.omega[widx])[0][0]) <= 0]
235+
widx = widx[np.real(sys(1j * sys.omega[widx])) <= 0]
236236
w_180 = np.array(
237237
[sp.optimize.brentq(_arg, sys.omega[i], sys.omega[i+1])
238238
for i in widx])
@@ -296,11 +296,10 @@ def phase_crossover_frequencies(sys):
296296
(array([ 1.73205081, 0. ]), array([-0.5 , 0.25]))
297297
"""
298298

299+
if not sys.issiso():
300+
raise ValueError("MIMO systems not yet implemented.")
299301
# Convert to a transfer function
300302
tf = xferfcn._convert_to_transfer_function(sys)
301-
302-
# if not siso, fall back to (0,0) element
303-
#! TODO: should add a check and warning here
304303
num = tf.num[0][0]
305304
den = tf.den[0][0]
306305

@@ -313,7 +312,9 @@ def phase_crossover_frequencies(sys):
313312

314313
# using real() to avoid rounding errors and results like 1+0j
315314
# it would be nice to have a vectorized version of self.evalfr here
316-
gain = np.real(np.asarray([tf(1j * f)[0][0] for f in realposfreq]))
315+
# update Sawyer B. Fuller 2020.08.15: your wish is my command.
316+
#gain = np.real(np.asarray([tf(1j * f) for f in realposfreq]))
317+
gain = np.real(tf(1j * realposfreq))
317318

318319
return realposfreq, gain
319320

control/rlocus.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ def _RLFindRoots(nump, denp, kvect):
493493
"""Find the roots for the root locus."""
494494
# Convert numerator and denominator to polynomials if they aren't
495495
roots = []
496-
for k in kvect:
496+
for k in np.array(kvect, ndmin=1):
497497
curpoly = denp + k * nump
498498
curroots = curpoly.r
499499
if len(curroots) < denp.order:
@@ -577,10 +577,10 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False):
577577
# Catch type error when event click is in the figure but not in an axis
578578
try:
579579
s = complex(event.xdata, event.ydata)
580-
K = -1. / sys.horner(s)
581-
K_xlim = -1. / sys.horner(
580+
K = -1. / sys(s)
581+
K_xlim = -1. / sys(
582582
complex(event.xdata + 0.05 * abs(xlim[1] - xlim[0]), event.ydata))
583-
K_ylim = -1. / sys.horner(
583+
K_ylim = -1. / sys(
584584
complex(event.xdata, event.ydata + 0.05 * abs(ylim[1] - ylim[0])))
585585

586586
except TypeError:
@@ -621,7 +621,7 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False):
621621
ax_rlocus.plot(s.real, s.imag, 'k.', marker='s', markersize=8,
622622
zorder=20, label='gain_point')
623623

624-
return K.real[0][0]
624+
return K.real
625625

626626

627627
def _removeLine(label, ax):

control/statesp.py

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -437,60 +437,91 @@ def __rdiv__(self, other):
437437

438438
raise NotImplementedError("StateSpace.__rdiv__ is not implemented yet.")
439439

440-
def __call__(self, s, squeeze=True):
441-
"""Evaluate system's transfer function at complex frequencies s/z.
440+
def __call__(self, x, squeeze=True):
441+
"""Evaluate system's transfer function at complex frequencies.
442+
443+
Evaluates at complex frequency x, where x is s or z dependong on
444+
whether the system is continuous or discrete-time.
442445
443446
If squeeze is True (default) and sys is single input, single output
444-
(SISO), return a 1D array or scalar depending on s.
447+
(SISO), return a 1D array or scalar depending on the size of x.
448+
For a MIMO system, returns an (n_outputs, n_inputs, n_x) array.
445449
446450
"""
447-
# Use TB05AD from Slycot if available
451+
# Use Slycot if available
448452
try:
449-
from slycot import tb05ad
450-
451-
# preallocate
452-
s_arr = np.array(s, ndmin=1) # array-like version of s
453-
out = np.empty((self.outputs, self.inputs, len(s_arr)),
454-
dtype=complex)
455-
n = self.states
456-
m = self.inputs
457-
p = self.outputs
458-
# The first call both evaluates C(sI-A)^-1 B and also returns
459-
# Hessenberg transformed matrices at, bt, ct.
460-
result = tb05ad(n, m, p, s_arr[0], self.A,
461-
self.B, self.C, job='NG')
462-
# When job='NG', result = (at, bt, ct, g_i, hinvb, info)
463-
at = result[0]
464-
bt = result[1]
465-
ct = result[2]
466-
467-
# TB05AD frequency evaluation does not include direct feedthrough.
468-
out[:, :, 0] = result[3] + self.D
469-
470-
# Now, iterate through the remaining frequencies using the
471-
# transformed state matrices, at, bt, ct.
472-
473-
# Start at the second frequency, already have the first.
474-
for kk, s_kk in enumerate(s_arr[1:len(s_arr)]):
475-
result = tb05ad(n, m, p, s_kk, at, bt, ct, job='NH')
476-
# When job='NH', result = (g_i, hinvb, info)
477-
478-
# kk+1 because enumerate starts at kk = 0.
479-
# but zero-th spot is already filled.
480-
out[:, :, kk+1] = result[0] + self.D
481-
482-
if not hasattr(s, '__len__'):
483-
# received a scalar s, squeeze down the array
484-
out = np.squeeze(out, axis=2)
485-
except ImportError: # Slycot unavailable. Fall back to horner.
486-
out = self.horner(s)
453+
out = self.slycot_horner(x)
454+
except ImportError: # Slycot unavailable. use built-in horner.
455+
out = self.horner(x)
456+
if not hasattr(x, '__len__'):
457+
# received a scalar x, squeeze down the array along last dim
458+
out = np.squeeze(out, axis=2)
487459
if squeeze and self.issiso():
488460
return out[0][0]
489461
else:
490462
return out
491463

464+
def slycot_horner(self, s):
465+
"""Evaluate system's transfer function at complex frequencies s
466+
using Horner's method from Slycot.
467+
468+
Expects inputs and outputs to be formatted correctly. Use __call__
469+
for a more user-friendly interface.
470+
471+
Parameters
472+
s : array-like
473+
474+
Returns
475+
output : array of size (outputs, inputs, len(s))
476+
477+
"""
478+
from slycot import tb05ad
479+
480+
# preallocate
481+
s_arr = np.array(s, ndmin=1) # array-like version of s
482+
out = np.empty((self.outputs, self.inputs, len(s_arr)),
483+
dtype=complex)
484+
n = self.states
485+
m = self.inputs
486+
p = self.outputs
487+
# The first call both evaluates C(sI-A)^-1 B and also returns
488+
# Hessenberg transformed matrices at, bt, ct.
489+
result = tb05ad(n, m, p, s_arr[0], self.A,
490+
self.B, self.C, job='NG')
491+
# When job='NG', result = (at, bt, ct, g_i, hinvb, info)
492+
at = result[0]
493+
bt = result[1]
494+
ct = result[2]
495+
496+
# TB05AD frequency evaluation does not include direct feedthrough.
497+
out[:, :, 0] = result[3] + self.D
498+
499+
# Now, iterate through the remaining frequencies using the
500+
# transformed state matrices, at, bt, ct.
501+
502+
# Start at the second frequency, already have the first.
503+
for kk, s_kk in enumerate(s_arr[1:len(s_arr)]):
504+
result = tb05ad(n, m, p, s_kk, at, bt, ct, job='NH')
505+
# When job='NH', result = (g_i, hinvb, info)
506+
507+
# kk+1 because enumerate starts at kk = 0.
508+
# but zero-th spot is already filled.
509+
out[:, :, kk+1] = result[0] + self.D
510+
return out
511+
492512
def horner(self, s):
493-
"""Evaluate systems's transfer function at complex frequencies s.
513+
"""Evaluate system's transfer function at complex frequencies s
514+
using Horner's method.
515+
516+
Expects inputs and outputs to be formatted correctly. Use __call__
517+
for a more user-friendly interface.
518+
519+
Parameters
520+
s : array-like
521+
522+
Returns
523+
output : array of size (outputs, inputs, len(s))
524+
494525
"""
495526
s_arr = np.array(s, ndmin=1) # force to be an array
496527
# Preallocate
@@ -501,9 +532,6 @@ def horner(self, s):
501532
np.dot(self.C,
502533
solve(s_idx * eye(self.states) - self.A, self.B)) \
503534
+ self.D
504-
if not hasattr(s, '__len__'):
505-
# received a scalar s, squeeze down the array along last dim
506-
out = np.squeeze(out, axis=2)
507535
return out
508536

509537
def freqresp(self, omega):
@@ -879,7 +907,7 @@ def dcgain(self):
879907
if self.isctime():
880908
gain = np.asarray(self.D-self.C.dot(np.linalg.solve(self.A, self.B)))
881909
else:
882-
gain = self.horner(1)
910+
gain = np.squeeze(self.horner(1))
883911
except LinAlgError:
884912
# eigenvalue at DC
885913
gain = np.tile(np.nan, (self.outputs, self.inputs))

control/tests/frd_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,9 @@ def test_eval(self):
387387
np.testing.assert_almost_equal(sys_tf(1j), frd_tf(1j))
388388

389389
# Should get an error if we evaluate at an unknown frequency
390-
self.assertRaises(Exception, frd_tf.eval(2))
390+
self.assertRaises(ValueError, frd_tf.eval, 2)
391391
# Should get an error if we use __call__ at real-valued frequency
392-
self.assertRaises(ValueError, frd_tf(2))
392+
self.assertRaises(ValueError, frd_tf, 2)
393393

394394
def test_repr_str(self):
395395
# repr printing

control/tests/margin_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_stability_margins_omega(sys, refout, refoutall):
7676
def test_stability_margins_3input(sys, refout, refoutall):
7777
"""Test stability_margins() function with mag, phase, omega input"""
7878
omega = np.logspace(-2, 2, 2000)
79-
mag, phase, omega_ = sys.freqresp(omega)
79+
mag, phase, omega_ = sys.frequency_response(omega)
8080
out = stability_margins((mag, phase*180/np.pi, omega_))
8181
assert_allclose(out, refout, atol=1.5e-3)
8282

@@ -92,7 +92,7 @@ def test_margin_sys(sys, refout, refoutall):
9292
def test_margin_3input(sys, refout, refoutall):
9393
"""Test margin() function with mag, phase, omega input"""
9494
omega = np.logspace(-2, 2, 2000)
95-
mag, phase, omega_ = sys.freqresp(omega)
95+
mag, phase, omega_ = sys.frequency_response(omega)
9696
out = margin((mag, phase*180/np.pi, omega_))
9797
assert_allclose(out, np.array(refout)[[0, 1, 3, 4]], atol=1.5e-3)
9898

@@ -113,7 +113,7 @@ def test_phase_crossover_frequencies():
113113
[[3], [4]]],
114114
[[[1, 2, 3, 4], [1, 1]],
115115
[[1, 1], [1, 1]]])
116-
omega, gain = phase_crossover_frequencies(tf)
116+
omega, gain = phase_crossover_frequencies(tf[0,0])
117117
assert_allclose(omega, [1.73205, 0.], atol=1.5e-3)
118118
assert_allclose(gain, [-0.5, 0.25], atol=1.5e-3)
119119

@@ -123,7 +123,7 @@ def test_mag_phase_omega():
123123
sys = TransferFunction(15, [1, 6, 11, 6])
124124
out = stability_margins(sys)
125125
omega = np.logspace(-2, 2, 1000)
126-
mag, phase, omega = sys.freqresp(omega)
126+
mag, phase, omega = sys.frequency_response(omega)
127127
out2 = stability_margins((mag, phase*180/np.pi, omega))
128128
ind = [0, 1, 3, 4] # indices of gm, pm, wg, wp -- ignore sm
129129
marg1 = np.array(out)[ind]

control/tests/statesp_array_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ def test_horner(self):
505505
# Make sure result agrees with frequency response
506506
mag, phase, omega = self.sys322.frequency_response([1])
507507
np.testing.assert_array_almost_equal(
508-
self.sys322.horner(1.j),
508+
np.squeeze(self.sys322.horner(1.j)),
509509
mag[:,:,0] * np.exp(1.j * phase[:,:,0]))
510510

511511
def tearDown(self):

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy