@@ -387,21 +387,97 @@ def horner(self, s):
387387
388388 # Method for generating the frequency response of the system
389389 def freqresp (self , omega ):
390- """Evaluate the system's transfer func. at a list of ang. frequencies .
390+ """Evaluate the system's transfer func. at a list of freqs, omega .
391391
392392 mag, phase, omega = self.freqresp(omega)
393393
394- reports the value of the magnitude, phase, and angular frequency of the
395- system's transfer function matrix evaluated at s = i * omega, where
396- omega is a list of angular frequencies, and is a sorted version of the
397- input omega.
394+ Reports the frequency response of the system,
395+
396+ G(j*omega) = mag*exp(j*phase)
397+
398+ for continuous time. For discrete time systems, the response is
399+ evaluated around the unit circle such that
400+
401+ G(exp(j*omega*dt)) = mag*exp(j*phase).
402+
403+ Inputs:
404+ ------
405+ omega: A list of frequencies in radians/sec at which the system
406+ should be evaluated. The list can be either a python list
407+ or a numpy array and will be sorted before evaluation.
408+
409+ Returns:
410+ -------
411+ mag: The magnitude (absolute value, not dB or log10) of the system
412+ frequency response.
413+
414+ phase: The wrapped phase in radians of the system frequency
415+ response.
416+
417+ omega: The list of sorted frequencies at which the response
418+ was evaluated.
398419
399420 """
400- # when evaluating at many frequencies, much faster to convert to
401- # transfer function first and then evaluate, than to solve an
402- # n-dimensional linear system at each frequency
403- tf = _convertToTransferFunction (self )
404- return tf .freqresp (omega )
421+
422+ # In case omega is passed in as a list, rather than a proper array.
423+ omega = np .asarray (omega )
424+
425+ numFreqs = len (omega )
426+ Gfrf = np .empty ((self .outputs , self .inputs , numFreqs ),
427+ dtype = np .complex128 )
428+
429+ # Sort frequency and calculate complex frequencies on either imaginary
430+ # axis (continuous time) or unit circle (discrete time).
431+ omega .sort ()
432+ if isdtime (self , strict = True ):
433+ dt = timebase (self )
434+ cmplx_freqs = exp (1.j * omega * dt )
435+ if ((omega * dt ).any () > pi ):
436+ warn_message = ("evalfr: frequency evaluation"
437+ " above Nyquist frequency" )
438+ warnings .warn (warn_message )
439+ else :
440+ cmplx_freqs = omega * 1.j
441+
442+ # Do the frequency response evaluation. Use TB05AD from Slycot
443+ # if it's available, otherwise use the built-in horners function.
444+ try :
445+ from slycot import tb05ad
446+
447+ n = np .shape (self .A )[0 ]
448+ m = self .inputs
449+ p = self .outputs
450+ # The first call both evalates C(sI-A)^-1 B and also returns
451+ # hessenberg transformed matrices at, bt, ct.
452+ result = tb05ad (n , m , p , cmplx_freqs [0 ], self .A ,
453+ self .B , self .C , job = 'NG' )
454+ # When job='NG', result = (at, bt, ct, g_i, hinvb, info)
455+ at = result [0 ]
456+ bt = result [1 ]
457+ ct = result [2 ]
458+
459+ # TB05AD freqency evaluation does not include direct feedthrough.
460+ Gfrf [:, :, 0 ] = result [3 ] + self .D
461+
462+ # Now, iterate through the remaining frequencies using the
463+ # transformed state matrices, at, bt, ct.
464+
465+ # Start at the second frequency, already have the first.
466+ for kk , cmplx_freqs_kk in enumerate (cmplx_freqs [1 :numFreqs ]):
467+ result = tb05ad (n , m , p , cmplx_freqs_kk , at ,
468+ bt , ct , job = 'NH' )
469+ # When job='NH', result = (g_i, hinvb, info)
470+
471+ # kk+1 because enumerate starts at kk = 0.
472+ # but zero-th spot is already filled.
473+ Gfrf [:, :, kk + 1 ] = result [0 ] + self .D
474+
475+ except ImportError : # Slycot unavailable. Fall back to horner.
476+ for kk , cmplx_freqs_kk in enumerate (cmplx_freqs ):
477+ Gfrf [:, :, kk ] = self .horner (cmplx_freqs_kk )
478+
479+ # mag phase omega
480+ return np .abs (Gfrf ), np .angle (Gfrf ), omega
405481
406482 # Compute poles and zeros
407483 def pole (self ):
0 commit comments