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,116 @@ 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 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``.
376366 """
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" )
387370
388371 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 ):
392374 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." )
402377 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 ]
408392 return out
409393
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.
413400
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)``
417403
418404 Parameters
419405 ----------
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.
424411
425412 Returns
426413 -------
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``.
440417
441- omega .sort ()
442418
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 )
447434
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 )
449449
450450 def feedback (self , other = 1 , sign = - 1 ):
451451 """Feedback interconnection between two FRD objects."""
@@ -515,11 +515,10 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
515515 "Frequency ranges of FRD do not match, conversion not implemented" )
516516
517517 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 , :]
523522 return FRD (fresp , omega , smooth = True )
524523
525524 elif isinstance (sys , (int , float , complex , np .number )):
0 commit comments