4848from warnings import warn
4949import numpy as np
5050from numpy import angle , array , empty , ones , \
51- real , imag , absolute , eye , linalg , where , dot
51+ real , imag , absolute , eye , linalg , where , dot , sort
5252from scipy .interpolate import splprep , splev
5353from .lti import LTI
5454
@@ -100,24 +100,18 @@ def __init__(self, *args, **kwargs):
100100 object, other than an FRD, call FRD(sys, omega)
101101
102102 """
103+ # TODO: discrete-time FRD systems?
103104 smooth = kwargs .get ('smooth' , False )
104105
105106 if len (args ) == 2 :
106107 if not isinstance (args [0 ], FRD ) and isinstance (args [0 ], LTI ):
107108 # not an FRD, but still a system, second argument should be
108109 # the frequency range
109110 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 ))
112112 numfreq = len (self .omega )
113-
114113 # 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 )
121115 else :
122116 # The user provided a response and a freq vector
123117 self .fresp = array (args [0 ], dtype = complex )
@@ -141,7 +135,7 @@ def __init__(self, *args, **kwargs):
141135 self .omega = args [0 ].omega
142136 self .fresp = args [0 ].fresp
143137 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 ))
145139
146140 # create interpolation functions
147141 if smooth :
@@ -163,17 +157,17 @@ def __str__(self):
163157 mimo = self .inputs > 1 or self .outputs > 1
164158 outstr = ['Frequency response data' ]
165159
166- mt , pt , wt = self .freqresp (self .omega )
167160 for i in range (self .inputs ):
168161 for j in range (self .outputs ):
169162 if mimo :
170163 outstr .append ("Input %i to output %i:" % (i + 1 , j + 1 ))
171164 outstr .append ('Freq [rad/s] Response' )
172165 outstr .append ('------------ ---------------------' )
173166 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 , :]))])
177171
178172 return '\n ' .join (outstr )
179173
@@ -342,110 +336,115 @@ def __pow__(self, other):
342336 return (FRD (ones (self .fresp .shape ), self .omega ) / self ) * \
343337 (self ** (other + 1 ))
344338
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-
361339 # Define the `eval` function to evaluate an FRD at a given (real)
362340 # frequency. Note that we choose to use `eval` instead of `evalfr` to
363341 # avoid confusion with :func:`evalfr`, which takes a complex number as its
364342 # argument. Similarly, we don't use `__call__` to avoid confusion between
365343 # 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.
371348
372349 Note that a "normal" FRD only returns values for which there is an
373350 entry in the omega vector. An interpolating FRD can return
374351 intermediate values.
375352
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+
376367 """
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" )
387371
388372 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 ):
392375 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." )
402378 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 ]
408393 return out
409394
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.
413397
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)``
417402
418403 Parameters
419404 ----------
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`.
424410
425411 Returns
426412 -------
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``.
440416
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 )
442433
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.
447436
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 )
449448
450449 def feedback (self , other = 1 , sign = - 1 ):
451450 """Feedback interconnection between two FRD objects."""
@@ -515,11 +514,10 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
515514 "Frequency ranges of FRD do not match, conversion not implemented" )
516515
517516 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 , :]
523521 return FRD (fresp , omega , smooth = True )
524522
525523 elif isinstance (sys , (int , float , complex , np .number )):
0 commit comments