Merge pull request #1011 from murrayrm/freqresp_improvements-16Apr2024 · python-control/python-control@feeb56a

1-

# Copyright (c) 2010 by California Institute of Technology

2-

# Copyright (c) 2012 by Delft University of Technology

3-

# All rights reserved.

4-

#

5-

# Redistribution and use in source and binary forms, with or without

6-

# modification, are permitted provided that the following conditions

7-

# are met:

8-

#

9-

# 1. Redistributions of source code must retain the above copyright

10-

# notice, this list of conditions and the following disclaimer.

11-

#

12-

# 2. Redistributions in binary form must reproduce the above copyright

13-

# notice, this list of conditions and the following disclaimer in the

14-

# documentation and/or other materials provided with the distribution.

15-

#

16-

# 3. Neither the names of the California Institute of Technology nor

17-

# the Delft University of Technology nor

18-

# the names of its contributors may be used to endorse or promote

19-

# products derived from this software without specific prior

20-

# written permission.

21-

#

22-

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS

23-

# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT

24-

# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS

25-

# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH

26-

# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,

27-

# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT

28-

# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF

29-

# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND

30-

# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

31-

# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT

32-

# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF

33-

# SUCH DAMAGE.

1+

# frdata.py - frequency response data representation and functions

342

#

353

# Author: M.M. (Rene) van Paassen (using xferfcn.py as basis)

364

# Date: 02 Oct 12

37538-396

"""

407

Frequency response data representation and functions.

418429

This module contains the FRD class and also functions that operate on

4310

FRD data.

4411

"""

451246-

# External function declarations

4713

from copy import copy

4814

from warnings import warn

49155016

import numpy as np

51-

from numpy import angle, array, empty, ones, \

52-

real, imag, absolute, eye, linalg, where, sort

53-

from scipy.interpolate import splprep, splev

17+

from numpy import absolute, angle, array, empty, eye, imag, linalg, ones, \

18+

real, sort, where

19+

from scipy.interpolate import splev, splprep

542055-

from .lti import LTI, _process_frequency_response

21+

from . import config

5622

from .exception import pandas_check

5723

from .iosys import InputOutputSystem, _process_iosys_keywords, common_timebase

58-

from . import config

24+

from .lti import LTI, _process_frequency_response

59256026

__all__ = ['FrequencyResponseData', 'FRD', 'frd']

6127

@@ -100,6 +66,10 @@ class constructor, using the :func:~~control.frd` factory function

10066

dt : float, True, or None

10167

System timebase.

1026869+

See Also

70+

--------

71+

frd

72+10373

Notes

10474

-----

10575

The main data members are 'omega' and 'fresp', where 'omega' is a 1D array

@@ -120,7 +90,6 @@ class constructor, using the :func:~~control.frd` factory function

12090

for a more detailed description.

1219112292

"""

123-12493

#

12594

# Class attributes

12695

#

@@ -206,11 +175,12 @@ def __init__(self, *args, **kwargs):

206175

"Needs 1 or 2 arguments; received %i." % len(args))

207176208177

#

209-

# Process key word arguments

178+

# Process keyword arguments

210179

#

211180212-

# If data was generated by a system, keep track of that

213-

self.sysname = kwargs.pop('sysname', None)

181+

# If data was generated by a system, keep track of that (used when

182+

# plotting data). Otherwise, use the system name, if given.

183+

self.sysname = kwargs.pop('sysname', kwargs.get('name', None))

214184215185

# Keep track of default properties for plotting

216186

self.plot_phase = kwargs.pop('plot_phase', None)

@@ -280,7 +250,7 @@ def __str__(self):

280250

"""String representation of the transfer function."""

281251282252

mimo = self.ninputs > 1 or self.noutputs > 1

283-

outstr = ['Frequency response data']

253+

outstr = [f"{InputOutputSystem.__str__(self)}"]

284254285255

for i in range(self.ninputs):

286256

for j in range(self.noutputs):

@@ -322,7 +292,7 @@ def __add__(self, other):

322292323293

# Convert the second argument to a frequency response function.

324294

# or re-base the frd to the current omega (if needed)

325-

other = _convert_to_FRD(other, omega=self.omega)

295+

other = _convert_to_frd(other, omega=self.omega)

326296327297

# Check that the input-output sizes are consistent.

328298

if self.ninputs != other.ninputs:

@@ -359,7 +329,7 @@ def __mul__(self, other):

359329

return FRD(self.fresp * other, self.omega,

360330

smooth=(self.ifunc is not None))

361331

else:

362-

other = _convert_to_FRD(other, omega=self.omega)

332+

other = _convert_to_frd(other, omega=self.omega)

363333364334

# Check that the input-output sizes are consistent.

365335

if self.ninputs != other.noutputs:

@@ -386,7 +356,7 @@ def __rmul__(self, other):

386356

return FRD(self.fresp * other, self.omega,

387357

smooth=(self.ifunc is not None))

388358

else:

389-

other = _convert_to_FRD(other, omega=self.omega)

359+

other = _convert_to_frd(other, omega=self.omega)

390360391361

# Check that the input-output sizes are consistent.

392362

if self.noutputs != other.ninputs:

@@ -414,7 +384,7 @@ def __truediv__(self, other):

414384

return FRD(self.fresp * (1/other), self.omega,

415385

smooth=(self.ifunc is not None))

416386

else:

417-

other = _convert_to_FRD(other, omega=self.omega)

387+

other = _convert_to_frd(other, omega=self.omega)

418388419389

if (self.ninputs > 1 or self.noutputs > 1 or

420390

other.ninputs > 1 or other.noutputs > 1):

@@ -433,7 +403,7 @@ def __rtruediv__(self, other):

433403

return FRD(other / self.fresp, self.omega,

434404

smooth=(self.ifunc is not None))

435405

else:

436-

other = _convert_to_FRD(other, omega=self.omega)

406+

other = _convert_to_frd(other, omega=self.omega)

437407438408

if (self.ninputs > 1 or self.noutputs > 1 or

439409

other.ninputs > 1 or other.noutputs > 1):

@@ -572,8 +542,8 @@ def __call__(self, s=None, squeeze=None, return_magphase=None):

572542

------

573543

ValueError

574544

If `s` is not purely imaginary, because

575-

:class:`FrequencyDomainData` systems are only defined at imaginary

576-

frequency values.

545+

:class:`FrequencyResponseData` systems are only defined at

546+

imaginary values (corresponding to real frequencies).

577547578548

"""

579549

if s is None:

@@ -638,7 +608,7 @@ def freqresp(self, omega):

638608

def feedback(self, other=1, sign=-1):

639609

"""Feedback interconnection between two FRD objects."""

640610641-

other = _convert_to_FRD(other, omega=self.omega)

611+

other = _convert_to_frd(other, omega=self.omega)

642612643613

if (self.noutputs != other.ninputs or self.ninputs != other.noutputs):

644614

raise ValueError(

@@ -710,7 +680,7 @@ def to_pandas(self):

710680

FRD = FrequencyResponseData

711681712682713-

def _convert_to_FRD(sys, omega, inputs=1, outputs=1):

683+

def _convert_to_frd(sys, omega, inputs=1, outputs=1):

714684

"""Convert a system to frequency response data form (if needed).

715685716686

If sys is already an frd, and its frequency range matches or

@@ -721,14 +691,14 @@ def _convert_to_FRD(sys, omega, inputs=1, outputs=1):

721691

manually, as in:

722692723693

>>> import numpy as np

724-

>>> from control.frdata import _convert_to_FRD

694+

>>> from control.frdata import _convert_to_frd

725695726696

>>> omega = np.logspace(-1, 1)

727-

>>> frd = _convert_to_FRD(3., omega) # Assumes inputs = outputs = 1

697+

>>> frd = _convert_to_frd(3., omega) # Assumes inputs = outputs = 1

728698

>>> frd.ninputs, frd.noutputs

729699

(1, 1)

730700731-

>>> frd = _convert_to_FRD(1., omega, inputs=3, outputs=2)

701+

>>> frd = _convert_to_frd(1., omega, inputs=3, outputs=2)

732702

>>> frd.ninputs, frd.noutputs

733703

(3, 2)

734704

@@ -777,51 +747,67 @@ def _convert_to_FRD(sys, omega, inputs=1, outputs=1):

777747

sys.__class__)

778748779749780-

def frd(*args):

781-

"""frd(d, w)

782-783-

Construct a frequency response data model.

750+

def frd(*args, **kwargs):

751+

"""frd(response, omega[, dt])

784752785-

frd models store the (measured) frequency response of a system.

753+

Construct a frequency response data (FRD) model.

786754787-

This function can be called in different ways:

755+

A frequency response data model stores the (measured) frequency response

756+

of a system. This factory function can be called in different ways:

788757789-

``frd(response, freqs)``

758+

``frd(response, omega)``

790759

Create an frd model with the given response data, in the form of

791-

complex response vector, at matching frequency freqs [in rad/s]

760+

complex response vector, at matching frequencies ``omega`` [in rad/s].

792761793-

``frd(sys, freqs)``

762+

``frd(sys, omega)``

794763

Convert an LTI system into an frd model with data at frequencies

795-

freqs.

764+

``omega``.

796765797766

Parameters

798767

----------

799-

response: array_like, or list

800-

complex vector with the system response

801-

freq: array_lik or lis

802-

vector with frequencies

803-

sys: LTI (StateSpace or TransferFunction)

804-

A linear system

768+

response : array_like or LTI system

769+

Complex vector with the system response or an LTI system that can

770+

be used to copmute the frequency response at a list of frequencies.

771+

omega : array_like

772+

Vector of frequencies at which the response is evaluated.

773+

dt : float, True, or None

774+

System timebase.

775+

smooth : bool, optional

776+

If ``True``, create an interpolation function that allows the

777+

frequency response to be computed at any frequency within the range

778+

of frequencies give in ``omega``. If ``False`` (default),

779+

frequency response can only be obtained at the frequencies

780+

specified in ``omega``.

805781806782

Returns

807783

-------

808-

sys: FRD

809-

New frequency response system

784+

sys : :class:`FrequencyResponseData`

785+

New frequency response data system.

786+787+

Other Parameters

788+

----------------

789+

inputs, outputs : str, or list of str, optional

790+

List of strings that name the individual signals of the transformed

791+

system. If not given, the inputs and outputs are the same as the

792+

original system.

793+

name : string, optional

794+

System name. If unspecified, a generic name <sys[id]> is generated

795+

with a unique integer id.

810796811797

See Also

812798

--------

813-

FRD, ss, tf

799+

FrequencyResponseData, frequency_response, ss, tf

814800815801

Examples

816802

--------

817803

>>> # Create from measurements

818804

>>> response = [1.0, 1.0, 0.5]

819-

>>> freqs = [1, 10, 100]

820-

>>> F = ct.frd(response, freqs)

805+

>>> omega = [1, 10, 100]

806+

>>> F = ct.frd(response, omega)

821807822808

>>> G = ct.tf([1], [1, 1])

823-

>>> freqs = [1, 10, 100]

824-

>>> F = ct.frd(G, freqs)

809+

>>> omega = [1, 10, 100]

810+

>>> F = ct.frd(G, omega)

825811826812

"""

827-

return FRD(*args)

813+

return FrequencyResponseData(*args, **kwargs)