Skip to content

Commit 68fc99f

Browse files
committed
updated iosys class/factory function documentation + docstring unit testing
1 parent 79adabd commit 68fc99f

File tree

10 files changed

+398
-235
lines changed

10 files changed

+398
-235
lines changed

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: 91 additions & 64 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.
152-
153-
The default constructor is FRD(d, w), where w is an iterable of
154-
frequency points, and d is the matching frequency data.
172+
"""FrequencyResponseData(d, w[, dt])
155173
156-
If d is a single list, 1D array, or tuple, a SISO system description
157-
is assumed. d can also be
174+
Construct a frequency response data (FRD) object.
158175
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)
@@ -270,10 +296,9 @@ def __init__(self, *args, **kwargs):
270296
arg_dt = common_timebase(args[0].dt, arg_dt)
271297
kwargs['dt'] = arg_dt
272298

299+
# Process signal names
273300
name, inputs, outputs, states, dt = _process_iosys_keywords(
274301
kwargs, defaults, end=True)
275-
276-
# Process signal names
277302
InputOutputSystem.__init__(
278303
self, name=name, inputs=inputs, outputs=outputs, dt=dt)
279304

@@ -284,17 +309,17 @@ def __init__(self, *args, **kwargs):
284309
raise ValueError("can't smooth with only 1 frequency")
285310
degree = 3 if self.omega.size > 3 else self.omega.size - 1
286311

287-
self.ifunc = empty((self.fresp.shape[0], self.fresp.shape[1]),
312+
self._ifunc = empty((self.fresp.shape[0], self.fresp.shape[1]),
288313
dtype=tuple)
289314
for i in range(self.fresp.shape[0]):
290315
for j in range(self.fresp.shape[1]):
291-
self.ifunc[i, j], u = splprep(
316+
self._ifunc[i, j], u = splprep(
292317
u=self.omega, x=[real(self.fresp[i, j, :]),
293318
imag(self.fresp[i, j, :])],
294319
w=1.0/(absolute(self.fresp[i, j, :]) + 0.001),
295320
s=0.0, k=degree)
296321
else:
297-
self.ifunc = None
322+
self._ifunc = None
298323

299324
#
300325
# Frequency response properties
@@ -424,7 +449,7 @@ def iosys_repr(self, format='loadable'):
424449
# Loadable format
425450
out = "FrequencyResponseData(\n{d},\n{w}{smooth}".format(
426451
d=repr(self.fresp), w=repr(self.omega),
427-
smooth=(self.ifunc and ", smooth=True") or "")
452+
smooth=(self._ifunc and ", smooth=True") or "")
428453

429454
if config.defaults['control.default_dt'] != self.dt:
430455
out += ",\ndt={dt}".format(
@@ -493,7 +518,7 @@ def __mul__(self, other):
493518
# Convert the second argument to a transfer function.
494519
if isinstance(other, (int, float, complex, np.number)):
495520
return FRD(self.fresp * other, self.omega,
496-
smooth=(self.ifunc is not None))
521+
smooth=(self._ifunc is not None))
497522
else:
498523
other = _convert_to_frd(other, omega=self.omega)
499524

@@ -511,16 +536,16 @@ def __mul__(self, other):
511536
for i in range(len(self.omega)):
512537
fresp[:, :, i] = self.fresp[:, :, i] @ other.fresp[:, :, i]
513538
return FRD(fresp, self.omega,
514-
smooth=(self.ifunc is not None) and
515-
(other.ifunc is not None))
539+
smooth=(self._ifunc is not None) and
540+
(other._ifunc is not None))
516541

517542
def __rmul__(self, other):
518543
"""Right Multiply two LTI objects (serial connection)."""
519544

520545
# Convert the second argument to an frd function.
521546
if isinstance(other, (int, float, complex, np.number)):
522547
return FRD(self.fresp * other, self.omega,
523-
smooth=(self.ifunc is not None))
548+
smooth=(self._ifunc is not None))
524549
else:
525550
other = _convert_to_frd(other, omega=self.omega)
526551

@@ -539,16 +564,16 @@ def __rmul__(self, other):
539564
for i in range(len(self.omega)):
540565
fresp[:, :, i] = other.fresp[:, :, i] @ self.fresp[:, :, i]
541566
return FRD(fresp, self.omega,
542-
smooth=(self.ifunc is not None) and
543-
(other.ifunc is not None))
567+
smooth=(self._ifunc is not None) and
568+
(other._ifunc is not None))
544569

545570
# TODO: Division of MIMO transfer function objects is not written yet.
546571
def __truediv__(self, other):
547572
"""Divide two LTI objects."""
548573

549574
if isinstance(other, (int, float, complex, np.number)):
550575
return FRD(self.fresp * (1/other), self.omega,
551-
smooth=(self.ifunc is not None))
576+
smooth=(self._ifunc is not None))
552577
else:
553578
other = _convert_to_frd(other, omega=self.omega)
554579

@@ -559,15 +584,15 @@ def __truediv__(self, other):
559584
"systems.")
560585

561586
return FRD(self.fresp/other.fresp, self.omega,
562-
smooth=(self.ifunc is not None) and
563-
(other.ifunc is not None))
587+
smooth=(self._ifunc is not None) and
588+
(other._ifunc is not None))
564589

565590
# TODO: Division of MIMO transfer function objects is not written yet.
566591
def __rtruediv__(self, other):
567592
"""Right divide two LTI objects."""
568593
if isinstance(other, (int, float, complex, np.number)):
569594
return FRD(other / self.fresp, self.omega,
570-
smooth=(self.ifunc is not None))
595+
smooth=(self._ifunc is not None))
571596
else:
572597
other = _convert_to_frd(other, omega=self.omega)
573598

@@ -584,7 +609,7 @@ def __pow__(self, other):
584609
raise ValueError("Exponent must be an integer")
585610
if other == 0:
586611
return FRD(ones(self.fresp.shape), self.omega,
587-
smooth=(self.ifunc is not None)) # unity
612+
smooth=(self._ifunc is not None)) # unity
588613
if other > 0:
589614
return self * (self**(other-1))
590615
if other < 0:
@@ -637,7 +662,7 @@ def eval(self, omega, squeeze=None):
637662
if any(omega_array.imag > 0):
638663
raise ValueError("FRD.eval can only accept real-valued omega")
639664

640-
if self.ifunc is None:
665+
if self._ifunc is None:
641666
elements = np.isin(self.omega, omega) # binary array
642667
if sum(elements) < len(omega_array):
643668
raise ValueError(
@@ -651,7 +676,7 @@ def eval(self, omega, squeeze=None):
651676
for i in range(self.noutputs):
652677
for j in range(self.ninputs):
653678
for k, w in enumerate(omega_array):
654-
frraw = splev(w, self.ifunc[i, j], der=0)
679+
frraw = splev(w, self._ifunc[i, j], der=0)
655680
out[i, j, k] = frraw[0] + 1.0j * frraw[1]
656681

657682
return _process_frequency_response(self, omega, out, squeeze=squeeze)
@@ -806,7 +831,7 @@ def feedback(self, other=1, sign=-1):
806831
resfresp = (myfresp @ linalg.inv(I_AB))
807832
fresp = np.moveaxis(resfresp, 0, 2)
808833

809-
return FRD(fresp, other.omega, smooth=(self.ifunc is not None))
834+
return FRD(fresp, other.omega, smooth=(self._ifunc is not None))
810835

811836
# Plotting interface
812837
def plot(self, plot_type=None, *args, **kwargs):
@@ -956,6 +981,8 @@ def frd(*args, **kwargs):
956981
957982
Parameters
958983
----------
984+
sys : LTI (StateSpace or TransferFunction)
985+
A linear system that will be evaluated for frequency response data.
959986
response : array_like or LTI system
960987
Complex vector with the system response or an LTI system that can
961988
be used to copmute the frequency response at a list of frequencies.
@@ -972,7 +999,7 @@ def frd(*args, **kwargs):
972999
9731000
Returns
9741001
-------
975-
sys : :class:`FrequencyResponseData`
1002+
sys : FrequencyResponseData
9761003
New frequency response data system.
9771004
9781005
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)