48
48
from warnings import warn
49
49
import numpy as np
50
50
from numpy import angle , array , empty , ones , \
51
- real , imag , absolute , eye , linalg , where , dot
51
+ real , imag , absolute , eye , linalg , where , dot , sort
52
52
from scipy .interpolate import splprep , splev
53
53
from .lti import LTI
54
54
@@ -100,24 +100,18 @@ def __init__(self, *args, **kwargs):
100
100
object, other than an FRD, call FRD(sys, omega)
101
101
102
102
"""
103
+ # TODO: discrete-time FRD systems?
103
104
smooth = kwargs .get ('smooth' , False )
104
105
105
106
if len (args ) == 2 :
106
107
if not isinstance (args [0 ], FRD ) and isinstance (args [0 ], LTI ):
107
108
# not an FRD, but still a system, second argument should be
108
109
# the frequency range
109
110
otherlti = args [0 ]
110
- self .omega = array (args [1 ], dtype = float )
111
- self .omega .sort ()
111
+ self .omega = sort (np .asarray (args [1 ], dtype = float ))
112
112
numfreq = len (self .omega )
113
-
114
113
# calculate frequency response at my points
115
- self .fresp = empty (
116
- (otherlti .outputs , otherlti .inputs , numfreq ),
117
- dtype = complex )
118
- for k , w in enumerate (self .omega ):
119
- self .fresp [:, :, k ] = otherlti ._evalfr (w )
120
-
114
+ self .fresp = otherlti (1j * self .omega , squeeze = False )
121
115
else :
122
116
# The user provided a response and a freq vector
123
117
self .fresp = array (args [0 ], dtype = complex )
@@ -141,7 +135,7 @@ def __init__(self, *args, **kwargs):
141
135
self .omega = args [0 ].omega
142
136
self .fresp = args [0 ].fresp
143
137
else :
144
- raise ValueError ("Needs 1 or 2 arguments; receivd %i." % len (args ))
138
+ raise ValueError ("Needs 1 or 2 arguments; received %i." % len (args ))
145
139
146
140
# create interpolation functions
147
141
if smooth :
@@ -163,17 +157,17 @@ def __str__(self):
163
157
mimo = self .inputs > 1 or self .outputs > 1
164
158
outstr = ['Frequency response data' ]
165
159
166
- mt , pt , wt = self .freqresp (self .omega )
167
160
for i in range (self .inputs ):
168
161
for j in range (self .outputs ):
169
162
if mimo :
170
163
outstr .append ("Input %i to output %i:" % (i + 1 , j + 1 ))
171
164
outstr .append ('Freq [rad/s] Response' )
172
165
outstr .append ('------------ ---------------------' )
173
166
outstr .extend (
174
- ['%12.3f %10.4g%+10.4gj' % (w , m , p )
175
- for m , p , w in zip (real (self .fresp [j , i , :]),
176
- imag (self .fresp [j , i , :]), wt )])
167
+ ['%12.3f %10.4g%+10.4gj' % (w , re , im )
168
+ for w , re , im in zip (self .omega ,
169
+ real (self .fresp [j , i , :]),
170
+ imag (self .fresp [j , i , :]))])
177
171
178
172
return '\n ' .join (outstr )
179
173
@@ -342,110 +336,116 @@ def __pow__(self, other):
342
336
return (FRD (ones (self .fresp .shape ), self .omega ) / self ) * \
343
337
(self ** (other + 1 ))
344
338
345
- def evalfr (self , omega ):
346
- """Evaluate a transfer function at a single angular frequency.
347
-
348
- self._evalfr(omega) returns the value of the frequency response
349
- at frequency omega.
350
-
351
- Note that a "normal" FRD only returns values for which there is an
352
- entry in the omega vector. An interpolating FRD can return
353
- intermediate values.
354
-
355
- """
356
- warn ("FRD.evalfr(omega) will be deprecated in a future release "
357
- "of python-control; use sys.eval(omega) instead" ,
358
- PendingDeprecationWarning ) # pragma: no coverage
359
- return self ._evalfr (omega )
360
-
361
339
# Define the `eval` function to evaluate an FRD at a given (real)
362
340
# frequency. Note that we choose to use `eval` instead of `evalfr` to
363
341
# avoid confusion with :func:`evalfr`, which takes a complex number as its
364
342
# argument. Similarly, we don't use `__call__` to avoid confusion between
365
343
# G(s) for a transfer function and G(omega) for an FRD object.
366
- def eval (self , omega ):
367
- """Evaluate a transfer function at a single angular frequency.
368
-
369
- self.evalfr(omega) returns the value of the frequency response
370
- at frequency omega.
344
+ # update Sawyer B. Fuller 2020.08.14: __call__ added to provide a uniform
345
+ # interface to systems in general and the lti.frequency_response method
346
+ def eval (self , omega , squeeze = True ):
347
+ """Evaluate a transfer function at angular frequency omega.
371
348
372
349
Note that a "normal" FRD only returns values for which there is an
373
350
entry in the omega vector. An interpolating FRD can return
374
351
intermediate values.
375
352
353
+ Parameters
354
+ ----------
355
+ omega : float or array_like
356
+ Frequencies in radians per second
357
+ squeeze : bool, optional (default=True)
358
+ If True and `sys` is single input single output (SISO), returns a
359
+ 1D array rather than a 3D array.
360
+
361
+ Returns
362
+ -------
363
+ fresp : (self.outputs, self.inputs, len(x)) or (len(x), ) complex ndarray
364
+ The frequency response of the system. Array is ``len(x)`` if and only
365
+ if system is SISO and ``squeeze=True``.
376
366
"""
377
- return self ._evalfr (omega )
378
-
379
- # Internal function to evaluate the frequency responses
380
- def _evalfr (self , omega ):
381
- """Evaluate a transfer function at a single angular frequency."""
382
- # Preallocate the output.
383
- if getattr (omega , '__iter__' , False ):
384
- out = empty ((self .outputs , self .inputs , len (omega )), dtype = complex )
385
- else :
386
- out = empty ((self .outputs , self .inputs ), dtype = complex )
367
+ omega_array = np .array (omega , ndmin = 1 ) # array-like version of omega
368
+ if any (omega_array .imag > 0 ):
369
+ raise ValueError ("FRD.eval can only accept real-valued omega" )
387
370
388
371
if self .ifunc is None :
389
- try :
390
- out = self .fresp [:, :, where (self .omega == omega )[0 ][0 ]]
391
- except Exception :
372
+ elements = np .isin (self .omega , omega ) # binary array
373
+ if sum (elements ) < len (omega_array ):
392
374
raise ValueError (
393
- "Frequency %f not in frequency list, try an interpolating"
394
- " FRD if you want additional points" % omega )
395
- else :
396
- if getattr (omega , '__iter__' , False ):
397
- for i in range (self .outputs ):
398
- for j in range (self .inputs ):
399
- for k , w in enumerate (omega ):
400
- frraw = splev (w , self .ifunc [i , j ], der = 0 )
401
- out [i , j , k ] = frraw [0 ] + 1.0j * frraw [1 ]
375
+ "not all frequencies omega are in frequency list of FRD "
376
+ "system. Try an interpolating FRD for additional points." )
402
377
else :
403
- for i in range (self .outputs ):
404
- for j in range (self .inputs ):
405
- frraw = splev (omega , self .ifunc [i , j ], der = 0 )
406
- out [i , j ] = frraw [0 ] + 1.0j * frraw [1 ]
407
-
378
+ out = self .fresp [:, :, elements ]
379
+ else :
380
+ out = empty ((self .outputs , self .inputs , len (omega_array )),
381
+ dtype = complex )
382
+ for i in range (self .outputs ):
383
+ for j in range (self .inputs ):
384
+ for k , w in enumerate (omega_array ):
385
+ frraw = splev (w , self .ifunc [i , j ], der = 0 )
386
+ out [i , j , k ] = frraw [0 ] + 1.0j * frraw [1 ]
387
+ if not hasattr (omega , '__len__' ):
388
+ # omega is a scalar, squeeze down array along last dim
389
+ out = np .squeeze (out , axis = 2 )
390
+ if squeeze and self .issiso ():
391
+ out = out [0 ][0 ]
408
392
return out
409
393
410
- # Method for generating the frequency response of the system
411
- def freqresp (self , omega ):
412
- """Evaluate the frequency response at a list of angular frequencies.
394
+ def __call__ (self , s , squeeze = True ):
395
+ """Evaluate system's transfer function at complex frequencies.
396
+
397
+ Returns the complex frequency response `sys(s)` of system `sys` with
398
+ `m = sys.inputs` number of inputs and `p = sys.outputs` number of
399
+ outputs.
413
400
414
- Reports the value of the magnitude, phase, and angular frequency of
415
- the requency response evaluated at omega, where omega is a list of
416
- angular frequencies, and is a sorted version of the input omega.
401
+ To evaluate at a frequency omega in radians per second, enter
402
+ ``s = omega * 1j`` or use ``sys.eval(omega)``
417
403
418
404
Parameters
419
405
----------
420
- omega : array_like
421
- A list of frequencies in radians/sec at which the system should be
422
- evaluated. The list can be either a python list or a numpy array
423
- and will be sorted before evaluation.
406
+ s : complex scalar or array_like
407
+ Complex frequencies
408
+ squeeze : bool, optional (default=True)
409
+ If True and `sys` is single input single output (SISO), i.e. `m=1`,
410
+ `p=1`, return a 1D array rather than a 3D array.
424
411
425
412
Returns
426
413
-------
427
- mag : (self.outputs, self.inputs, len(omega)) ndarray
428
- The magnitude (absolute value, not dB or log10) of the system
429
- frequency response.
430
- phase : (self.outputs, self.inputs, len(omega)) ndarray
431
- The wrapped phase in radians of the system frequency response.
432
- omega : ndarray or list or tuple
433
- The list of sorted frequencies at which the response was
434
- evaluated.
435
- """
436
- # Preallocate outputs.
437
- numfreq = len (omega )
438
- mag = empty ((self .outputs , self .inputs , numfreq ))
439
- phase = empty ((self .outputs , self .inputs , numfreq ))
414
+ fresp : (p, m, len(s)) complex ndarray or (len(s),) complex ndarray
415
+ The frequency response of the system. Array is ``(len(s), )`` if
416
+ and only if system is SISO and ``squeeze=True``.
440
417
441
- omega .sort ()
442
418
443
- for k , w in enumerate (omega ):
444
- fresp = self ._evalfr (w )
445
- mag [:, :, k ] = abs (fresp )
446
- phase [:, :, k ] = angle (fresp )
419
+ Raises
420
+ ------
421
+ ValueError
422
+ If `s` is not purely imaginary, because
423
+ :class:`FrequencyDomainData` systems are only defined at imaginary
424
+ frequency values.
425
+ """
426
+ if any (abs (np .array (s , ndmin = 1 ).real ) > 0 ):
427
+ raise ValueError ("__call__: FRD systems can only accept "
428
+ "purely imaginary frequencies" )
429
+ # need to preserve array or scalar status
430
+ if hasattr (s , '__len__' ):
431
+ return self .eval (np .asarray (s ).imag , squeeze = squeeze )
432
+ else :
433
+ return self .eval (complex (s ).imag , squeeze = squeeze )
447
434
448
- return mag , phase , omega
435
+ def freqresp (self , omega ):
436
+ """(deprecated) Evaluate transfer function at complex frequencies.
437
+
438
+ .. deprecated::0.9.0
439
+ Method has been given the more pythonic name
440
+ :meth:`FrequencyResponseData.frequency_response`. Or use
441
+ :func:`freqresp` in the MATLAB compatibility module.
442
+ """
443
+ warn ("FrequencyResponseData.freqresp(omega) will be removed in a "
444
+ "future release of python-control; use "
445
+ "FrequencyResponseData.frequency_response(omega), or "
446
+ "freqresp(sys, omega) in the MATLAB compatibility module "
447
+ "instead" , DeprecationWarning )
448
+ return self .frequency_response (omega )
449
449
450
450
def feedback (self , other = 1 , sign = - 1 ):
451
451
"""Feedback interconnection between two FRD objects."""
@@ -515,11 +515,10 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
515
515
"Frequency ranges of FRD do not match, conversion not implemented" )
516
516
517
517
elif isinstance (sys , LTI ):
518
- omega .sort ()
519
- fresp = empty ((sys .outputs , sys .inputs , len (omega )), dtype = complex )
520
- for k , w in enumerate (omega ):
521
- fresp [:, :, k ] = sys ._evalfr (w )
522
-
518
+ omega = np .sort (omega )
519
+ fresp = sys (1j * omega )
520
+ if len (fresp .shape ) == 1 :
521
+ fresp = fresp [np .newaxis , np .newaxis , :]
523
522
return FRD (fresp , omega , smooth = True )
524
523
525
524
elif isinstance (sys , (int , float , complex , np .number )):
0 commit comments