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,115 @@ 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 or scalar depending on the length of `omega`
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
365
+ only if system is SISO and ``squeeze=True``.
366
+
376
367
"""
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 )
368
+ omega_array = np .array (omega , ndmin = 1 ) # array-like version of omega
369
+ if any (omega_array .imag > 0 ):
370
+ raise ValueError ("FRD.eval can only accept real-valued omega" )
387
371
388
372
if self .ifunc is None :
389
- try :
390
- out = self .fresp [:, :, where (self .omega == omega )[0 ][0 ]]
391
- except Exception :
373
+ elements = np .isin (self .omega , omega ) # binary array
374
+ if sum (elements ) < len (omega_array ):
392
375
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 ]
376
+ "not all frequencies omega are in frequency list of FRD "
377
+ "system. Try an interpolating FRD for additional points." )
402
378
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
-
379
+ out = self .fresp [:, :, elements ]
380
+ else :
381
+ out = empty ((self .outputs , self .inputs , len (omega_array )),
382
+ dtype = complex )
383
+ for i in range (self .outputs ):
384
+ for j in range (self .inputs ):
385
+ for k , w in enumerate (omega_array ):
386
+ frraw = splev (w , self .ifunc [i , j ], der = 0 )
387
+ out [i , j , k ] = frraw [0 ] + 1.0j * frraw [1 ]
388
+ if not hasattr (omega , '__len__' ):
389
+ # omega is a scalar, squeeze down array along last dim
390
+ out = np .squeeze (out , axis = 2 )
391
+ if squeeze and self .issiso ():
392
+ out = out [0 ][0 ]
408
393
return out
409
394
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.
395
+ def __call__ (self , s , squeeze = True ):
396
+ """Evaluate system's transfer function at complex frequencies.
413
397
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.
398
+ Returns the complex frequency response `sys(s)`.
399
+
400
+ To evaluate at a frequency omega in radians per second, enter
401
+ ``x = omega * 1j`` or use ``sys.eval(omega)``
417
402
418
403
Parameters
419
404
----------
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.
405
+ s: complex scalar or array_like
406
+ Complex frequencies
407
+ squeeze: bool, optional (default=True)
408
+ If True and `sys` is single input single output (SISO), returns a
409
+ 1D array or scalar depending on the length of x`.
424
410
425
411
Returns
426
412
-------
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 ))
413
+ fresp : (self.outputs, self.inputs, len(s)) or len(s) complex ndarray
414
+ The frequency response of the system. Array is ``len(s)`` if and
415
+ only if system is SISO and ``squeeze=True``.
440
416
441
- omega .sort ()
417
+ Raises
418
+ ------
419
+ ValueError
420
+ If `s` is not purely imaginary, because
421
+ :class:`FrequencyDomainData` systems are only defined at imaginary
422
+ frequency values.
423
+
424
+ """
425
+ if any (abs (np .array (s , ndmin = 1 ).real ) > 0 ):
426
+ raise ValueError ("__call__: FRD systems can only accept"
427
+ "purely imaginary frequencies" )
428
+ # need to preserve array or scalar status
429
+ if hasattr (s , '__len__' ):
430
+ return self .eval (np .asarray (s ).imag , squeeze = squeeze )
431
+ else :
432
+ return self .eval (complex (s ).imag , squeeze = squeeze )
442
433
443
- for k , w in enumerate (omega ):
444
- fresp = self ._evalfr (w )
445
- mag [:, :, k ] = abs (fresp )
446
- phase [:, :, k ] = angle (fresp )
434
+ def freqresp (self , omega ):
435
+ """(deprecated) Evaluate transfer function at complex frequencies.
447
436
448
- return mag , phase , omega
437
+ .. deprecated::0.9.0
438
+ Method has been given the more pythonic name
439
+ :meth:`FrequencyResponseData.frequency_response`. Or use
440
+ :func:`freqresp` in the MATLAB compatibility module.
441
+ """
442
+ warn ("FrequencyResponseData.freqresp(omega) will be removed in a "
443
+ "future release of python-control; use "
444
+ "FrequencyResponseData.frequency_response(omega), or "
445
+ "freqresp(sys, omega) in the MATLAB compatibility module "
446
+ "instead" , DeprecationWarning )
447
+ return self .frequency_response (omega )
449
448
450
449
def feedback (self , other = 1 , sign = - 1 ):
451
450
"""Feedback interconnection between two FRD objects."""
@@ -515,11 +514,10 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
515
514
"Frequency ranges of FRD do not match, conversion not implemented" )
516
515
517
516
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
-
517
+ omega = np .sort (omega )
518
+ fresp = sys (1j * omega )
519
+ if len (fresp .shape ) == 1 :
520
+ fresp = fresp [np .newaxis , np .newaxis , :]
523
521
return FRD (fresp , omega , smooth = True )
524
522
525
523
elif isinstance (sys , (int , float , complex , np .number )):
0 commit comments