Skip to content

Commit feeb56a

Browse files
authored
Merge pull request #1011 from murrayrm/freqresp_improvements-16Apr2024
Frequency plot improvements
2 parents e5394c4 + 343df2c commit feeb56a

28 files changed

+2365
-506
lines changed

LICENSE

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Copyright (c) 2009-2016 by California Institute of Technology
2-
Copyright (c) 2016-2023 by python-control developers
2+
Copyright (c) 2012 by Delft University of Technology
3+
Copyright (c) 2016-2024 by python-control developers
34
All rights reserved.
45

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

control/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
from .timeplot import *
8484

8585
from .bdalg import *
86+
from .ctrlplot import *
8687
from .delay import *
8788
from .descfcn import *
8889
from .dtime import *

control/bdalg.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ def feedback(sys1, sys2=1, sign=-1):
279279
if isinstance(sys2, (int, float, complex, np.number, np.ndarray,
280280
tf.TransferFunction)):
281281
sys1 = tf._convert_to_transfer_function(sys1)
282-
elif isinstance(sys2, frd.FRD):
283-
sys1 = frd._convert_to_FRD(sys1, sys2.omega)
282+
elif isinstance(sys2, frd.FrequencyResponseData):
283+
sys1 = frd._convert_to_frd(sys1, sys2.omega)
284284
else:
285285
sys1 = ss._convert_to_statespace(sys1)
286286

control/ctrlplot.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# ctrlplot.py - utility functions for plotting
2+
# Richard M. Murray, 14 Jun 2024
3+
#
4+
# Collection of functions that are used by various plotting functions.
5+
6+
import matplotlib.pyplot as plt
7+
import numpy as np
8+
9+
from . import config
10+
11+
__all__ = ['suptitle']
12+
13+
14+
def suptitle(
15+
title, fig=None, frame='axes', **kwargs):
16+
"""Add a centered title to a figure.
17+
18+
This is a wrapper for the matplotlib `suptitle` function, but by
19+
setting ``frame`` to 'axes' (default) then the title is centered on the
20+
midpoint of the axes in the figure, rather than the center of the
21+
figure. This usually looks better (particularly with multi-panel
22+
plots), though it takes longer to render.
23+
24+
Parameters
25+
----------
26+
title : str
27+
Title text.
28+
fig : Figure, optional
29+
Matplotlib figure. Defaults to current figure.
30+
frame : str, optional
31+
Coordinate frame to use for centering: 'axes' (default) or 'figure'.
32+
**kwargs : :func:`matplotlib.pyplot.suptitle` keywords, optional
33+
Additional keywords (passed to matplotlib).
34+
35+
"""
36+
rcParams = config._get_param('freqplot', 'rcParams', kwargs, pop=True)
37+
38+
if fig is None:
39+
fig = plt.gcf()
40+
41+
if frame == 'figure':
42+
with plt.rc_context(rcParams):
43+
fig.suptitle(title, **kwargs)
44+
45+
elif frame == 'axes':
46+
# TODO: move common plotting params to 'ctrlplot'
47+
rcParams = config._get_param('freqplot', 'rcParams', rcParams)
48+
with plt.rc_context(rcParams):
49+
plt.tight_layout() # Put the figure into proper layout
50+
xc, _ = _find_axes_center(fig, fig.get_axes())
51+
52+
fig.suptitle(title, x=xc, **kwargs)
53+
plt.tight_layout() # Update the layout
54+
55+
else:
56+
raise ValueError(f"unknown frame '{frame}'")
57+
58+
59+
def _find_axes_center(fig, axs):
60+
"""Find the midpoint between axes in display coordinates.
61+
62+
This function finds the middle of a plot as defined by a set of axes.
63+
64+
"""
65+
inv_transform = fig.transFigure.inverted()
66+
xlim = ylim = [1, 0]
67+
for ax in axs:
68+
ll = inv_transform.transform(ax.transAxes.transform((0, 0)))
69+
ur = inv_transform.transform(ax.transAxes.transform((1, 1)))
70+
71+
xlim = [min(ll[0], xlim[0]), max(ur[0], xlim[1])]
72+
ylim = [min(ll[1], ylim[0]), max(ur[1], ylim[1])]
73+
74+
return (np.sum(xlim)/2, np.sum(ylim)/2)

control/frdata.py

+67-81
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,27 @@
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
375

38-
396
"""
407
Frequency response data representation and functions.
418
429
This module contains the FRD class and also functions that operate on
4310
FRD data.
4411
"""
4512

46-
# External function declarations
4713
from copy import copy
4814
from warnings import warn
4915

5016
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
5420

55-
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
5925

6026
__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.
10268
69+
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.
12191
12292
"""
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))
207176

208177
#
209-
# Process key word arguments
178+
# Process keyword arguments
210179
#
211180

212-
# 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))
214184

215185
# 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."""
281251

282252
mimo = self.ninputs > 1 or self.noutputs > 1
283-
outstr = ['Frequency response data']
253+
outstr = [f"{InputOutputSystem.__str__(self)}"]
284254

285255
for i in range(self.ninputs):
286256
for j in range(self.noutputs):
@@ -322,7 +292,7 @@ def __add__(self, other):
322292

323293
# 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)
326296

327297
# 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)
363333

364334
# 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)
390360

391361
# 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)
418388

419389
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)
437407

438408
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).
577547
578548
"""
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."""
640610

641-
other = _convert_to_FRD(other, omega=self.omega)
611+
other = _convert_to_frd(other, omega=self.omega)
642612

643613
if (self.noutputs != other.ninputs or self.ninputs != other.noutputs):
644614
raise ValueError(
@@ -710,7 +680,7 @@ def to_pandas(self):
710680
FRD = FrequencyResponseData
711681

712682

713-
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).
715685
716686
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:
722692
723693
>>> import numpy as np
724-
>>> from control.frdata import _convert_to_FRD
694+
>>> from control.frdata import _convert_to_frd
725695
726696
>>> 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)
730700
731-
>>> 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__)
778748

779749

780-
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])
784752
785-
frd models store the (measured) frequency response of a system.
753+
Construct a frequency response data (FRD) model.
786754
787-
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:
788757
789-
``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].
792761
793-
``frd(sys, freqs)``
762+
``frd(sys, omega)``
794763
Convert an LTI system into an frd model with data at frequencies
795-
freqs.
764+
``omega``.
796765
797766
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``.
805781
806782
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.
810796
811797
See Also
812798
--------
813-
FRD, ss, tf
799+
FrequencyResponseData, frequency_response, ss, tf
814800
815801
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)
821807
822808
>>> 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)
825811
826812
"""
827-
return FRD(*args)
813+
return FrequencyResponseData(*args, **kwargs)

0 commit comments

Comments
 (0)