Skip to content

Commit 2c7ca9a

Browse files
committed
updated iosys class/factory function documentation + docstring unit testing
1 parent a7fd763 commit 2c7ca9a

10 files changed

Lines changed: 415 additions & 237 deletions

File tree

control/flatsys/flatsys.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,5 @@
11
# flatsys.py - trajectory generation for differentially flat systems
22
# RMM, 10 Nov 2012
3-
#
4-
# This file contains routines for computing trajectories for differentially
5-
# flat nonlinear systems. It is (very) loosely based on the NTG software
6-
# package developed by Mark Milam and Kudah Mushambi, but rewritten from
7-
# scratch in python.
8-
#
9-
# Copyright (c) 2012 by California Institute of Technology
10-
# All rights reserved.
11-
#
12-
# Redistribution and use in source and binary forms, with or without
13-
# modification, are permitted provided that the following conditions
14-
# are met:
15-
#
16-
# 1. Redistributions of source code must retain the above copyright
17-
# notice, this list of conditions and the following disclaimer.
18-
#
19-
# 2. Redistributions in binary form must reproduce the above copyright
20-
# notice, this list of conditions and the following disclaimer in the
21-
# documentation and/or other materials provided with the distribution.
22-
#
23-
# 3. Neither the name of the California Institute of Technology nor
24-
# the names of its contributors may be used to endorse or promote
25-
# products derived from this software without specific prior
26-
# written permission.
27-
#
28-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29-
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30-
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
31-
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH
32-
# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33-
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
34-
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
35-
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
36-
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
37-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
38-
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
39-
# SUCH DAMAGE.
403

414
import itertools
425
import numpy as np

control/frdata.py

Lines changed: 95 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
# Author: M.M. (Rene) van Paassen (using xferfcn.py as basis)
44
# Date: 02 Oct 12
55

6-
"""
7-
Frequency response data representation and functions.
6+
"""Frequency response data representation and functions.
7+
8+
This module contains the FrequencyResponseData (FRD) class and also
9+
functions that operate on FRD data.
810
9-
This module contains the FRD class and also functions that operate on
10-
FRD data.
1111
"""
1212

1313
from collections.abc import Iterable
@@ -35,38 +35,29 @@ class FrequencyResponseData(LTI):
3535
3636
The FrequencyResponseData (FRD) class is used to represent systems in
3737
frequency response data form. It can be created manually using the
38-
class constructor, using the :func:`~control.frd` factory function or
38+
class constructor, using the :func:`~control.frd` factory function, or
3939
via the :func:`~control.frequency_response` function.
4040
4141
Parameters
4242
----------
43-
d : 1D or 3D complex array_like
43+
response : 1D or 3D complex array_like
4444
The frequency response at each frequency point. If 1D, the system is
4545
assumed to be SISO. If 3D, the system is MIMO, with the first
4646
dimension corresponding to the output index of the FRD, the second
4747
dimension corresponding to the input index, and the 3rd dimension
4848
corresponding to the frequency points in omega
49-
w : iterable of real frequencies
49+
omega : iterable of real frequencies
5050
List of frequency points for which data are available.
51-
sysname : str or None
52-
Name of the system that generated the data.
5351
smooth : bool, optional
5452
If ``True``, create an interpolation function that allows the
5553
frequency response to be computed at any frequency within the range of
5654
frequencies give in ``w``. If ``False`` (default), frequency response
5755
can only be obtained at the frequencies specified in ``w``.
58-
59-
Attributes
60-
----------
61-
ninputs, noutputs : int
62-
Number of input and output variables.
63-
omega : 1D array
64-
Frequency points of the response.
65-
fresp : 3D array
66-
Frequency response, indexed by output index, input index, and
67-
frequency point.
68-
dt : float, True, or None
69-
System timebase.
56+
dt : None, True or float, optional
57+
System timebase. 0 (default) indicates continuous time, True
58+
indicates discrete time with unspecified sampling time, positive
59+
number is discrete time with specified sampling time, None
60+
indicates unspecified timebase (either continuous or discrete time).
7061
squeeze : bool
7162
By default, if a system is single-input, single-output (SISO) then
7263
the outputs (and inputs) are returned as a 1D array (indexed by
@@ -79,16 +70,46 @@ class constructor, using the :func:`~control.frd` factory function or
7970
returned as a 3D array (indexed by the output, input, and
8071
frequency) even if the system is SISO. The default value can be set
8172
using config.defaults['control.squeeze_frequency_response'].
82-
ninputs, noutputs, nstates : int
83-
Number of inputs, outputs, and states of the underlying system.
73+
sysname : str or None
74+
Name of the system that generated the data.
75+
76+
Attributes
77+
----------
78+
fresp : 3D array
79+
Frequency response, indexed by output index, input index, and
80+
frequency point.
81+
frequency : 1D array
82+
Array of frequency points for which data are available.
83+
ninputs, noutputs : int
84+
Number of inputs and outputs signals.
85+
shape : tuple
86+
2-tuple of I/O system dimension, (noutputs, ninputs).
8487
input_labels, output_labels : array of str
85-
Names for the input and output variables.
86-
sysname : str, optional
87-
Name of the system. For data generated using
88-
:func:`~control.frequency_response`, stores the name of the system
89-
that created the data.
88+
Names for the input and output signals.
89+
name : str
90+
System name. For data generated using
91+
:func:`~control.frequency_response`, stores the name of the
92+
system that created the data.
93+
magnitude : array
94+
Magnitude of the frequency response, indexed by frequency.
95+
phase : array
96+
Magnitude of the frequency response, indexed by frequency.
97+
98+
Other Parameters
99+
----------------
100+
plot_type : str, optional
101+
Set the type of plot to generate with ``plot()`` ('bode', 'nichols').
90102
title : str, optional
91103
Set the title to use when plotting.
104+
plot_magnitude, plot_phase : bool, optional
105+
If set to `False`, don't plot the magnitude or phase, respectively.
106+
return_magphase : bool, optional
107+
If True, then a frequency response data object will enumerate as a
108+
tuple of the form (mag, phase, omega) where where ``mag`` is the
109+
magnitude (absolute value, not dB or log10) of the system
110+
frequency response, ``phase`` is the wrapped phase in radians of
111+
the system frequency response, and ``omega`` is the (sorted)
112+
frequencies at which the response was evaluated.
92113
93114
See Also
94115
--------
@@ -148,22 +169,26 @@ class constructor, using the :func:`~control.frd` factory function or
148169
_epsw = 1e-8 #: Bound for exact frequency match
149170

150171
def __init__(self, *args, **kwargs):
151-
"""Construct an FRD object.
172+
"""FrequencyResponseData(d, w[, dt])
152173
153-
The default constructor is FRD(d, w), where w is an iterable of
154-
frequency points, and d is the matching frequency data.
174+
Construct a frequency response data (FRD) object.
155175
156-
If d is a single list, 1D array, or tuple, a SISO system description
157-
is assumed. d can also be
158-
159-
To call the copy constructor, call FRD(sys), where sys is a
160-
FRD object.
176+
The default constructor is FrequencyResponseData(d, w), where w is
177+
an iterable of frequency points, and d is the matching frequency
178+
data. If d is a single list, 1D array, or tuple, a SISO system
179+
description is assumed. d can also be a 2D array, in which case a
180+
MIMO response is created. To call the copy constructor, call
181+
FrequencyResponseData(sys), where sys is a FRD object. The
182+
timebase for the frequency response can be provided using an
183+
optional third argument or the 'dt' keyword.
161184
162-
To construct frequency response data for an existing LTI
163-
object, other than an FRD, call FRD(sys, omega).
185+
To construct frequency response data for an existing LTI object,
186+
other than an FRD, call FrequencyResponseData(sys, omega). This
187+
functionality can also be obtained using :func:`frequency_response`
188+
(which has additional options available).
164189
165-
The timebase for the frequency response can be provided using an
166-
optional third argument or the 'dt' keyword.
190+
See :class:`FrequencyResponseData` and :func:`frd` for more
191+
information.
167192
168193
"""
169194
smooth = kwargs.pop('smooth', False)
@@ -182,11 +207,12 @@ def __init__(self, *args, **kwargs):
182207

183208
if len(args) == 2:
184209
if not isinstance(args[0], FRD) and isinstance(args[0], LTI):
185-
# not an FRD, but still a system, second argument should be
186-
# the frequency range
210+
# not an FRD, but still an LTI system, second argument
211+
# should be the frequency range
187212
otherlti = args[0]
188213
self.omega = sort(np.asarray(args[1], dtype=float))
189-
# calculate frequency response at my points
214+
215+
# calculate frequency response at specified points
190216
if otherlti.isctime():
191217
s = 1j * self.omega
192218
self.fresp = otherlti(s, squeeze=False)
@@ -267,11 +293,13 @@ def __init__(self, *args, **kwargs):
267293
self, 'output_index', None) else self.output_labels,
268294
'name': getattr(self, 'name', None)}
269295
if arg_dt is not None:
270-
defaults['dt'] = arg_dt # choose compatible timebase
271-
name, inputs, outputs, states, dt = _process_iosys_keywords(
272-
kwargs, defaults, end=True)
296+
if isinstance(args[0], LTI):
297+
arg_dt = common_timebase(args[0].dt, arg_dt)
298+
kwargs['dt'] = arg_dt
273299

274300
# Process signal names
301+
name, inputs, outputs, states, dt = _process_iosys_keywords(
302+
kwargs, defaults, end=True)
275303
InputOutputSystem.__init__(
276304
self, name=name, inputs=inputs, outputs=outputs, dt=dt)
277305

@@ -282,17 +310,17 @@ def __init__(self, *args, **kwargs):
282310
raise ValueError("can't smooth with only 1 frequency")
283311
degree = 3 if self.omega.size > 3 else self.omega.size - 1
284312

285-
self.ifunc = empty((self.fresp.shape[0], self.fresp.shape[1]),
313+
self._ifunc = empty((self.fresp.shape[0], self.fresp.shape[1]),
286314
dtype=tuple)
287315
for i in range(self.fresp.shape[0]):
288316
for j in range(self.fresp.shape[1]):
289-
self.ifunc[i, j], u = splprep(
317+
self._ifunc[i, j], u = splprep(
290318
u=self.omega, x=[real(self.fresp[i, j, :]),
291319
imag(self.fresp[i, j, :])],
292320
w=1.0/(absolute(self.fresp[i, j, :]) + 0.001),
293321
s=0.0, k=degree)
294322
else:
295-
self.ifunc = None
323+
self._ifunc = None
296324

297325
#
298326
# Frequency response properties
@@ -395,7 +423,7 @@ def __repr__(self):
395423
"""
396424
return "FrequencyResponseData({d}, {w}{smooth})".format(
397425
d=repr(self.fresp), w=repr(self.omega),
398-
smooth=(self.ifunc and ", smooth=True") or "")
426+
smooth=(self._ifunc and ", smooth=True") or "")
399427

400428
def __neg__(self):
401429
"""Negate a transfer function."""
@@ -454,7 +482,7 @@ def __mul__(self, other):
454482
# Convert the second argument to a transfer function.
455483
if isinstance(other, (int, float, complex, np.number)):
456484
return FRD(self.fresp * other, self.omega,
457-
smooth=(self.ifunc is not None))
485+
smooth=(self._ifunc is not None))
458486
else:
459487
other = _convert_to_frd(other, omega=self.omega)
460488

@@ -472,16 +500,16 @@ def __mul__(self, other):
472500
for i in range(len(self.omega)):
473501
fresp[:, :, i] = self.fresp[:, :, i] @ other.fresp[:, :, i]
474502
return FRD(fresp, self.omega,
475-
smooth=(self.ifunc is not None) and
476-
(other.ifunc is not None))
503+
smooth=(self._ifunc is not None) and
504+
(other._ifunc is not None))
477505

478506
def __rmul__(self, other):
479507
"""Right Multiply two LTI objects (serial connection)."""
480508

481509
# Convert the second argument to an frd function.
482510
if isinstance(other, (int, float, complex, np.number)):
483511
return FRD(self.fresp * other, self.omega,
484-
smooth=(self.ifunc is not None))
512+
smooth=(self._ifunc is not None))
485513
else:
486514
other = _convert_to_frd(other, omega=self.omega)
487515

@@ -500,16 +528,16 @@ def __rmul__(self, other):
500528
for i in range(len(self.omega)):
501529
fresp[:, :, i] = other.fresp[:, :, i] @ self.fresp[:, :, i]
502530
return FRD(fresp, self.omega,
503-
smooth=(self.ifunc is not None) and
504-
(other.ifunc is not None))
531+
smooth=(self._ifunc is not None) and
532+
(other._ifunc is not None))
505533

506534
# TODO: Division of MIMO transfer function objects is not written yet.
507535
def __truediv__(self, other):
508536
"""Divide two LTI objects."""
509537

510538
if isinstance(other, (int, float, complex, np.number)):
511539
return FRD(self.fresp * (1/other), self.omega,
512-
smooth=(self.ifunc is not None))
540+
smooth=(self._ifunc is not None))
513541
else:
514542
other = _convert_to_frd(other, omega=self.omega)
515543

@@ -520,15 +548,15 @@ def __truediv__(self, other):
520548
"systems.")
521549

522550
return FRD(self.fresp/other.fresp, self.omega,
523-
smooth=(self.ifunc is not None) and
524-
(other.ifunc is not None))
551+
smooth=(self._ifunc is not None) and
552+
(other._ifunc is not None))
525553

526554
# TODO: Division of MIMO transfer function objects is not written yet.
527555
def __rtruediv__(self, other):
528556
"""Right divide two LTI objects."""
529557
if isinstance(other, (int, float, complex, np.number)):
530558
return FRD(other / self.fresp, self.omega,
531-
smooth=(self.ifunc is not None))
559+
smooth=(self._ifunc is not None))
532560
else:
533561
other = _convert_to_frd(other, omega=self.omega)
534562

@@ -545,7 +573,7 @@ def __pow__(self, other):
545573
raise ValueError("Exponent must be an integer")
546574
if other == 0:
547575
return FRD(ones(self.fresp.shape), self.omega,
548-
smooth=(self.ifunc is not None)) # unity
576+
smooth=(self._ifunc is not None)) # unity
549577
if other > 0:
550578
return self * (self**(other-1))
551579
if other < 0:
@@ -598,7 +626,7 @@ def eval(self, omega, squeeze=None):
598626
if any(omega_array.imag > 0):
599627
raise ValueError("FRD.eval can only accept real-valued omega")
600628

601-
if self.ifunc is None:
629+
if self._ifunc is None:
602630
elements = np.isin(self.omega, omega) # binary array
603631
if sum(elements) < len(omega_array):
604632
raise ValueError(
@@ -612,7 +640,7 @@ def eval(self, omega, squeeze=None):
612640
for i in range(self.noutputs):
613641
for j in range(self.ninputs):
614642
for k, w in enumerate(omega_array):
615-
frraw = splev(w, self.ifunc[i, j], der=0)
643+
frraw = splev(w, self._ifunc[i, j], der=0)
616644
out[i, j, k] = frraw[0] + 1.0j * frraw[1]
617645

618646
return _process_frequency_response(self, omega, out, squeeze=squeeze)
@@ -767,7 +795,7 @@ def feedback(self, other=1, sign=-1):
767795
resfresp = (myfresp @ linalg.inv(I_AB))
768796
fresp = np.moveaxis(resfresp, 0, 2)
769797

770-
return FRD(fresp, other.omega, smooth=(self.ifunc is not None))
798+
return FRD(fresp, other.omega, smooth=(self._ifunc is not None))
771799

772800
# Plotting interface
773801
def plot(self, plot_type=None, *args, **kwargs):
@@ -917,6 +945,8 @@ def frd(*args, **kwargs):
917945
918946
Parameters
919947
----------
948+
sys : LTI (StateSpace or TransferFunction)
949+
A linear system that will be evaluated for frequency response data.
920950
response : array_like or LTI system
921951
Complex vector with the system response or an LTI system that can
922952
be used to copmute the frequency response at a list of frequencies.
@@ -933,7 +963,7 @@ def frd(*args, **kwargs):
933963
934964
Returns
935965
-------
936-
sys : :class:`FrequencyResponseData`
966+
sys : FrequencyResponseData
937967
New frequency response data system.
938968
939969
Other Parameters

control/freqplot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,7 @@ def nyquist_response(
13051305
"Nyquist plot currently only supports SISO systems.")
13061306

13071307
# Figure out the frequency range
1308-
if isinstance(sys, FrequencyResponseData) and sys.ifunc is None \
1308+
if isinstance(sys, FrequencyResponseData) and sys._ifunc is None \
13091309
and not omega_range_given:
13101310
omega_sys = sys.omega # use system frequencies
13111311
else:

0 commit comments

Comments
 (0)