def test_discrete_bode(self): # Create a simple discrete time system and check the calculation sys = TransferFunction([1], [1, 0.5], 1) omega = [1, 2, 3] mag_out, phase_out, omega_out = bode(sys, omega) H_z = list(map(lambda w: 1./(np.exp(1.j * w) + 0.5), omega)) np.testing.assert_array_almost_equal(omega, omega_out) np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z)) np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z))
def test_discrete_bode(self): # Create a simple discrete time system and check the calculation sys = TransferFunction([1], [1, 0.5], 1) omega = [1, 2, 3] mag_out, phase_out, omega_out = bode(sys, omega) H_z = list(map(lambda w: 1. / (np.exp(1.j * w) + 0.5), omega)) np.testing.assert_array_almost_equal(omega, omega_out) np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z)) np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z))
def bode(*args, **keywords): """Bode plot of the frequency response Examples -------- >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> mag, phase, omega = bode(sys) .. todo:: Document these use cases * >>> bode(sys, w) * >>> bode(sys1, sys2, ..., sysN) * >>> bode(sys1, sys2, ..., sysN, w) * >>> bode(sys1, 'plotstyle1', ..., sysN, 'plotstyleN') """ # If the first argument is a list, then assume python-control calling format if (getattr(args[0], '__iter__', False)): return freqplot.bode(*args, **keywords) # Otherwise, run through the arguments and collect up arguments syslist = []; plotstyle=[]; omega=None; i = 0; while i < len(args): # Check to see if this is a system of some sort if (ctrlutil.issys(args[i])): # Append the system to our list of systems syslist.append(args[i]) i += 1 # See if the next object is a plotsytle (string) if (i < len(args) and isinstance(args[i], str)): plotstyle.append(args[i]) i += 1 # Go on to the next argument continue # See if this is a frequency list elif (isinstance(args[i], (list, np.ndarray))): omega = args[i] i += 1 break else: raise ControlArgument("unrecognized argument type") # Check to make sure that we processed all arguments if (i < len(args)): raise ControlArgument("not all arguments processed") # Check to make sure we got the same number of plotstyles as systems if (len(plotstyle) != 0 and len(syslist) != len(plotstyle)): raise ControlArgument("number of systems and plotstyles should be equal") # Warn about unimplemented plotstyles #! TODO: remove this when plot styles are implemented in bode() #! TODO: uncomment unit test code that tests this out if (len(plotstyle) != 0): print("Warning (matabl.bode): plot styles not implemented"); # Call the bode command return freqplot.bode(syslist, omega, **keywords)
def testConvert(self): """Test state space to transfer function conversion.""" verbose = self.debug # print __doc__ # Machine precision for floats. # eps = np.finfo(float).eps for states in range(1, self.maxStates): for inputs in range(1, self.maxIO): for outputs in range(1, self.maxIO): # start with a random SS system and transform to TF then # back to SS, check that the matrices are the same. ssOriginal = matlab.rss(states, outputs, inputs) if (verbose): self.printSys(ssOriginal, 1) # Make sure the system is not degenerate Cmat = ctrb(ssOriginal.A, ssOriginal.B) if (np.linalg.matrix_rank(Cmat) != states): if (verbose): print(" skipping (not reachable)") continue Omat = obsv(ssOriginal.A, ssOriginal.C) if (np.linalg.matrix_rank(Omat) != states): if (verbose): print(" skipping (not observable)") continue tfOriginal = matlab.tf(ssOriginal) if (verbose): self.printSys(tfOriginal, 2) ssTransformed = matlab.ss(tfOriginal) if (verbose): self.printSys(ssTransformed, 3) tfTransformed = matlab.tf(ssTransformed) if (verbose): self.printSys(tfTransformed, 4) # Check to see if the state space systems have same dim if (ssOriginal.states != ssTransformed.states): print("WARNING: state space dimension mismatch: " + \ "%d versus %d" % \ (ssOriginal.states, ssTransformed.states)) # Now make sure the frequency responses match # Since bode() only handles SISO, go through each I/O pair # For phase, take sine and cosine to avoid +/- 360 offset for inputNum in range(inputs): for outputNum in range(outputs): if (verbose): print("Checking input %d, output %d" \ % (inputNum, outputNum)) ssorig_mag, ssorig_phase, ssorig_omega = \ bode(_mimo2siso(ssOriginal, \ inputNum, outputNum), \ deg=False, Plot=False) ssorig_real = ssorig_mag * np.cos(ssorig_phase) ssorig_imag = ssorig_mag * np.sin(ssorig_phase) # # Make sure TF has same frequency response # num = tfOriginal.num[outputNum][inputNum] den = tfOriginal.den[outputNum][inputNum] tforig = tf(num, den) tforig_mag, tforig_phase, tforig_omega = \ bode(tforig, ssorig_omega, \ deg=False, Plot=False) tforig_real = tforig_mag * np.cos(tforig_phase) tforig_imag = tforig_mag * np.sin(tforig_phase) np.testing.assert_array_almost_equal( \ ssorig_real, tforig_real) np.testing.assert_array_almost_equal( \ ssorig_imag, tforig_imag) # # Make sure xform'd SS has same frequency response # ssxfrm_mag, ssxfrm_phase, ssxfrm_omega = \ bode(_mimo2siso(ssTransformed, \ inputNum, outputNum), \ ssorig_omega, \ deg=False, Plot=False) ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase) ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase) np.testing.assert_array_almost_equal( \ ssorig_real, ssxfrm_real) np.testing.assert_array_almost_equal( \ ssorig_imag, ssxfrm_imag) # # Make sure xform'd TF has same frequency response # num = tfTransformed.num[outputNum][inputNum] den = tfTransformed.den[outputNum][inputNum] tfxfrm = tf(num, den) tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \ bode(tfxfrm, ssorig_omega, \ deg=False, Plot=False) tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase) tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase) np.testing.assert_array_almost_equal( \ ssorig_real, tfxfrm_real) np.testing.assert_array_almost_equal( \ ssorig_imag, tfxfrm_imag)
def testConvert(self): """Test state space to transfer function conversion.""" verbose = self.debug # print __doc__ # Machine precision for floats. # eps = np.finfo(float).eps for states in range(1, self.maxStates): for inputs in range(1, self.maxIO): for outputs in range(1, self.maxIO): # start with a random SS system and transform to TF then # back to SS, check that the matrices are the same. ssOriginal = matlab.rss(states, outputs, inputs) if (verbose): self.printSys(ssOriginal, 1) # Make sure the system is not degenerate Cmat = ctrb(ssOriginal.A, ssOriginal.B) if (np.linalg.matrix_rank(Cmat) != states): if (verbose): print(" skipping (not reachable)") continue Omat = obsv(ssOriginal.A, ssOriginal.C) if (np.linalg.matrix_rank(Omat) != states): if (verbose): print(" skipping (not observable)") continue tfOriginal = matlab.tf(ssOriginal) if (verbose): self.printSys(tfOriginal, 2) ssTransformed = matlab.ss(tfOriginal) if (verbose): self.printSys(ssTransformed, 3) tfTransformed = matlab.tf(ssTransformed) if (verbose): self.printSys(tfTransformed, 4) # Check to see if the state space systems have same dim if (ssOriginal.states != ssTransformed.states): print("WARNING: state space dimension mismatch: " + \ "%d versus %d" % \ (ssOriginal.states, ssTransformed.states)) # Now make sure the frequency responses match # Since bode() only handles SISO, go through each I/O pair # For phase, take sine and cosine to avoid +/- 360 offset for inputNum in range(inputs): for outputNum in range(outputs): if (verbose): print("Checking input %d, output %d" \ % (inputNum, outputNum)) ssorig_mag, ssorig_phase, ssorig_omega = \ bode(_mimo2siso(ssOriginal, \ inputNum, outputNum), \ deg=False, plot=False) ssorig_real = ssorig_mag * np.cos(ssorig_phase) ssorig_imag = ssorig_mag * np.sin(ssorig_phase) # # Make sure TF has same frequency response # num = tfOriginal.num[outputNum][inputNum] den = tfOriginal.den[outputNum][inputNum] tforig = tf(num, den) tforig_mag, tforig_phase, tforig_omega = \ bode(tforig, ssorig_omega, \ deg=False, plot=False) tforig_real = tforig_mag * np.cos(tforig_phase) tforig_imag = tforig_mag * np.sin(tforig_phase) np.testing.assert_array_almost_equal( \ ssorig_real, tforig_real) np.testing.assert_array_almost_equal( \ ssorig_imag, tforig_imag) # # Make sure xform'd SS has same frequency response # ssxfrm_mag, ssxfrm_phase, ssxfrm_omega = \ bode(_mimo2siso(ssTransformed, \ inputNum, outputNum), \ ssorig_omega, \ deg=False, plot=False) ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase) ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase) np.testing.assert_array_almost_equal( \ ssorig_real, ssxfrm_real) np.testing.assert_array_almost_equal( \ ssorig_imag, ssxfrm_imag) # # Make sure xform'd TF has same frequency response # num = tfTransformed.num[outputNum][inputNum] den = tfTransformed.den[outputNum][inputNum] tfxfrm = tf(num, den) tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \ bode(tfxfrm, ssorig_omega, \ deg=False, plot=False) tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase) tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase) np.testing.assert_array_almost_equal( \ ssorig_real, tfxfrm_real) np.testing.assert_array_almost_equal( \ ssorig_imag, tfxfrm_imag)
def bode(self, axes=None, pairs=None, label='bode', title=None, colors=['b', 'g', 'r', 'c', 'm', 'y', 'k'], styles=[(None,None), (3,3), (1,1), (3,2,1,2)], **kwargs): """Create a Bode plot of the system's response. The Bode plots of a MIMO system are overlayed. This is different than MATLAB\ :sup:`®`, which creates an array of subplots. **Arguments:** - *axes*: Tuple (pair) of axes for the magnitude and phase plots If *axes* is not provided, then axes will be created in a new figure. - *pairs*: List of (input index, output index) tuples of each transfer function to be evaluated If not provided, all of the transfer functions will be plotted. - *label*: Label for the figure (ignored if *axes* is provided) This will be used as the base filename if the figure is saved. - *title*: Title for the figure If *title* is *None* (default), then the title will be "Bode Plot of *fbase*", where *fbase* is the base filename of the data. Use '' for no title. - *colors*: Color or list of colors that will be used sequentially Each may be a character, grayscale, or rgb value. .. Seealso:: http://matplotlib.sourceforge.net/api/colors_api.html - *styles*: Line/dash style or list of line/dash styles that will be used sequentially Each style is a string representing a linestyle (e.g., "--") or a tuple of on/off lengths representing dashes. Use "" for no line and "-" for a solid line. .. Seealso:: http://matplotlib.sourceforge.net/api/collections_api.html - *\*\*kwargs*: Additional arguments for :meth:`control.freqplot.bode` **Returns:** 1. *axes*: Tuple (pair) of axes for the magnitude and phase plots **Example:** .. code-block:: python >>> from modelicares import LinRes, save >>> from numpy import pi, logspace >>> lin = LinRes('examples/PID.mat') >>> lin.bode(label='examples/PID-bode', omega=2*pi*logspace(-2, 3), ... title="Bode Plot of Modelica.Blocks.Continuous.PID") # doctest: +ELLIPSIS (<matplotlib.axes._subplots.AxesSubplot object at 0x...>, <matplotlib.axes._subplots.AxesSubplot object at 0x...>) >>> save() Saved examples/PID-bode.pdf Saved examples/PID-bode.png .. only:: html .. image:: ../examples/PID-bode.png :scale: 70 % :alt: example for LinRes.bode() .. only:: latex .. figure:: ../examples/PID-bode.pdf :scale: 80 % Results of example for :meth:`LinRes.bode`. """ # Create axes if necessary. if axes is None or (None, None): fig = base.figure(label) axes = (fig.add_subplot(211), fig.add_subplot(212)) # Create a title if necessary. if title is None: title = r"Bode Plot of %s" % self.fbase # Set up the color(s) and line style(s). if not iterable(colors): # Use the single color for all plots. colors = (colors,) if not iterable(styles): # Use the single line style for all plots. styles = [styles] elif type(styles[0]) is int: # One dashes tuple has been provided; use its value for all plots. styles = [styles] n_colors = len(colors) n_styles = len(styles) # If input/output pair(s) aren't specified, generate a list of all # pairs. if not pairs: pairs = [(i_u, i_y) for i_u in range(self.sys.inputs) for i_y in range(self.sys.outputs)] # Create the plots. for i, (i_u, i_y) in enumerate(pairs): # Extract the SISO TF. TODO: Is there a better way to do this? sys = ss(self.sys.A, self.sys.B[:, i_u], self.sys.C[i_y, :], self.sys.D[i_y, i_u]) bode(sys, Hz=True, label=r'$Y_{%i}/U_{%i}$' % (i_y, i_u), color=colors[np.mod(i, n_colors)], axes=axes, style=styles[np.mod(i, n_styles)], **kwargs) # Note: controls.freqplot.bode() is currently only implemented for # SISO systems. # 5/23/11: Since controls.freqplot.bode() already uses subplots for # the magnitude and phase plots, it would be difficult to modify # the code to put the Bode plots of a MIMO system into an array of # subfigures like MATLAB does. # Decorate and finish. axes[0].set_title(title) if len(pairs) > 1: axes[0].legend() axes[1].legend() return axes
def stability_margins(sysdata, deg=True): """Calculate gain, phase and stability margins and associated crossover frequencies. Usage ----- gm, pm, sm, wg, wp, ws = stability_margins(sysdata, deg=True) Parameters ---------- sysdata: linsys or (mag, phase, omega) sequence sys : linsys Linear SISO system mag, phase, omega : sequence of array_like Input magnitude, phase, and frequencies (rad/sec) sequence from bode frequency response data deg=True: boolean If true, all input and output phases in degrees, else in radians Returns ------- gm, pm, sm, wg, wp, ws: float Gain margin gm, phase margin pm, stability margin sm, and associated crossover frequencies wg, wp, and ws of SISO open-loop. If more than one crossover frequency is detected, returns the lowest corresponding margin. """ #TODO do this precisely without the effects of discretization of frequencies? #TODO assumes SISO #TODO unit tests, margin plot if (not getattr(sysdata, '__iter__', False)): sys = sysdata # TODO: implement for discrete time systems if (isdtime(sys, strict=True)): raise NotImplementedError("Function not implemented in discrete time") mag, phase, omega = bode(sys, deg=deg, Plot=False) elif len(sysdata) == 3: # TODO: replace with FRD object type? mag, phase, omega = sysdata else: raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.") if deg: cycle = 360. crossover = 180. else: cycle = 2 * np.pi crossover = np.pi wrapped_phase = -np.mod(phase, cycle) # phase margin from minimum phase among all gain crossovers neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0] mag_crossings_p = wrapped_phase[neg_mag_crossings_i] if len(neg_mag_crossings_i) == 0: if mag[0] < 1: # gain always less than one wp = np.nan pm = np.inf else: # gain always greater than one print("margin: no magnitude crossings found") wp = np.nan pm = np.nan else: min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)] wp = omega[min_mag_crossing_i] pm = crossover + phase[min_mag_crossing_i] if pm < 0: print("warning: system unstable: negative phase margin") # gain margin from minimum gain margin among all phase crossovers neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0] neg_phase_crossings_g = mag[neg_phase_crossings_i] if len(neg_phase_crossings_i) == 0: wg = np.nan gm = np.inf else: min_phase_crossing_i = neg_phase_crossings_i[ np.argmax(neg_phase_crossings_g)] wg = omega[min_phase_crossing_i] gm = abs(1/mag[min_phase_crossing_i]) if gm < 1: print("warning: system unstable: gain margin < 1") # stability margin from minimum abs distance from -1 point if deg: phase_rad = phase * np.pi / 180. else: phase_rad = phase L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt min_Lplus1_i = np.argmin(np.abs(L + 1)) sm = np.abs(L[min_Lplus1_i] + 1) ws = phase[min_Lplus1_i] return gm, pm, sm, wg, wp, ws
def testConvert(self, fixedseed, states, inputs, outputs): """Test state space to transfer function conversion. start with a random SS system and transform to TF then back to SS, check that the matrices are the same. """ ssOriginal = rss(states, outputs, inputs) if verbose: self.printSys(ssOriginal, 1) # Make sure the system is not degenerate Cmat = ctrb(ssOriginal.A, ssOriginal.B) if (np.linalg.matrix_rank(Cmat) != states): pytest.skip("not reachable") Omat = obsv(ssOriginal.A, ssOriginal.C) if (np.linalg.matrix_rank(Omat) != states): pytest.skip("not observable") tfOriginal = tf(ssOriginal) if (verbose): self.printSys(tfOriginal, 2) ssTransformed = ss(tfOriginal) if (verbose): self.printSys(ssTransformed, 3) tfTransformed = tf(ssTransformed) if (verbose): self.printSys(tfTransformed, 4) # Check to see if the state space systems have same dim if (ssOriginal.nstates != ssTransformed.nstates) and verbose: print("WARNING: state space dimension mismatch: %d versus %d" % (ssOriginal.nstates, ssTransformed.nstates)) # Now make sure the frequency responses match # Since bode() only handles SISO, go through each I/O pair # For phase, take sine and cosine to avoid +/- 360 offset for inputNum in range(inputs): for outputNum in range(outputs): if (verbose): print("Checking input %d, output %d" % (inputNum, outputNum)) ssorig_mag, ssorig_phase, ssorig_omega = \ bode(_mimo2siso(ssOriginal, inputNum, outputNum), deg=False, plot=False) ssorig_real = ssorig_mag * np.cos(ssorig_phase) ssorig_imag = ssorig_mag * np.sin(ssorig_phase) # # Make sure TF has same frequency response # num = tfOriginal.num[outputNum][inputNum] den = tfOriginal.den[outputNum][inputNum] tforig = tf(num, den) tforig_mag, tforig_phase, tforig_omega = \ bode(tforig, ssorig_omega, deg=False, plot=False) tforig_real = tforig_mag * np.cos(tforig_phase) tforig_imag = tforig_mag * np.sin(tforig_phase) np.testing.assert_array_almost_equal( ssorig_real, tforig_real) np.testing.assert_array_almost_equal( ssorig_imag, tforig_imag) # # Make sure xform'd SS has same frequency response # ssxfrm_mag, ssxfrm_phase, ssxfrm_omega = \ bode(_mimo2siso(ssTransformed, inputNum, outputNum), ssorig_omega, deg=False, plot=False) ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase) ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase) np.testing.assert_array_almost_equal( ssorig_real, ssxfrm_real, decimal=5) np.testing.assert_array_almost_equal( ssorig_imag, ssxfrm_imag, decimal=5) # Make sure xform'd TF has same frequency response # num = tfTransformed.num[outputNum][inputNum] den = tfTransformed.den[outputNum][inputNum] tfxfrm = tf(num, den) tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \ bode(tfxfrm, ssorig_omega, deg=False, plot=False) tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase) tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase) np.testing.assert_array_almost_equal( ssorig_real, tfxfrm_real, decimal=5) np.testing.assert_array_almost_equal( ssorig_imag, tfxfrm_imag, decimal=5)
def bode(*args, **keywords): """Bode plot of the frequency response Plots a bode gain and phase diagram Parameters ---------- sys : Lti, or list of Lti System for which the Bode response is plotted and give. Optionally a list of systems can be entered, or several systems can be specified (i.e. several parameters). The sys arguments may also be interspersed with format strings. A frequency argument (array_like) may also be added, some examples: * >>> bode(sys, w) # one system, freq vector * >>> bode(sys1, sys2, ..., sysN) # several systems * >>> bode(sys1, sys2, ..., sysN, w) * >>> bode(sys1, 'plotstyle1', ..., sysN, 'plotstyleN') # + plot formats omega: freq_range Range of frequencies in rad/s dB : boolean If True, plot result in dB Hz : boolean If True, plot frequency in Hz (omega must be provided in rad/sec) deg : boolean If True, return phase in degrees (else radians) Plot : boolean If True, plot magnitude and phase Examples -------- >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> mag, phase, omega = bode(sys) .. todo:: Document these use cases * >>> bode(sys, w) * >>> bode(sys1, sys2, ..., sysN) * >>> bode(sys1, sys2, ..., sysN, w) * >>> bode(sys1, 'plotstyle1', ..., sysN, 'plotstyleN') """ # If the first argument is a list, then assume python-control calling format if (getattr(args[0], '__iter__', False)): return freqplot.bode(*args, **keywords) # Otherwise, run through the arguments and collect up arguments syslist = []; plotstyle=[]; omega=None; i = 0; while i < len(args): # Check to see if this is a system of some sort if (ctrlutil.issys(args[i])): # Append the system to our list of systems syslist.append(args[i]) i += 1 # See if the next object is a plotsytle (string) if (i < len(args) and isinstance(args[i], str)): plotstyle.append(args[i]) i += 1 # Go on to the next argument continue # See if this is a frequency list elif (isinstance(args[i], (list, np.ndarray))): omega = args[i] i += 1 break else: raise ControlArgument("unrecognized argument type") # Check to make sure that we processed all arguments if (i < len(args)): raise ControlArgument("not all arguments processed") # Check to make sure we got the same number of plotstyles as systems if (len(plotstyle) != 0 and len(syslist) != len(plotstyle)): raise ControlArgument("number of systems and plotstyles should be equal") # Warn about unimplemented plotstyles #! TODO: remove this when plot styles are implemented in bode() #! TODO: uncomment unit test code that tests this out if (len(plotstyle) != 0): print("Warning (matabl.bode): plot styles not implemented"); # Call the bode command return freqplot.bode(syslist, omega, **keywords)
def multibode(lins, axes=None, pair=(0, 0), label='bode', title="Bode Plot", labels='', colors=['b', 'g', 'r', 'c', 'm', 'y', 'k'], styles=[(None,None), (3,3), (1,1), (3,2,1,2)], leg_kwargs={}, **kwargs): r"""Plot multiple linearizations onto a single Bode diagram. **Arguments:** - *lins*: Linearization result or list of results (instances of :class:`linres.LinRes`) - *axes*: Tuple (pair) of axes for the magnitude and phase plots If *axes* is not provided, then axes will be created in a new figure. - *pair*: Tuple of (input index, output index) for the transfer function to be chosen from each system (applied to all) This is ignored if the system is SISO. - *label*: Label for the figure (ignored if axes is provided) This will be used as the base filename if the figure is saved. - *title*: Title for the figure - *labels*: Label or list of labels for the legends If *labels* is *None*, then no label will be used. If it is an empty string (''), then the base filenames will be used. - *colors*: Color or list of colors that will be used sequentially Each may be a character, grayscale, or rgb value. .. Seealso:: http://matplotlib.sourceforge.net/api/colors_api.html - *styles*: Line/dash style or list of line/dash styles that will be used sequentially Each style is a string representing a linestyle (e.g., "--") or a tuple of on/off lengths representing dashes. Use "" for no line and "-" for a solid line. .. Seealso:: http://matplotlib.sourceforge.net/api/collections_api.html - *leg_kwargs*: Dictionary of keyword arguments for :meth:`matplotlib.pyplot.legend` If *leg_kwargs* is *None*, then no legend will be shown. - *\*\*kwargs*: Additional arguments for :meth:`control.freqplot.bode` **Returns:** 1. *axes*: Tuple (pair) of axes for the magnitude and phase plots **Example:** .. testsetup:: >>> from modelicares import closeall >>> closeall() .. code-block:: python >>> import os >>> from glob import glob >>> from modelicares import LinRes, multibode, save, read_params >>> from numpy import pi, logspace >>> lins = map(LinRes, glob('examples/PID/*/*.mat')) >>> labels = ["Ti=%g" % read_params('Ti', os.path.join(lin.dir, 'dsin.txt')) ... for lin in lins] >>> multibode(lins, ... title="Bode Plot of Modelica.Blocks.Continuous.PID", ... label='examples/PIDs-bode', omega=2*pi*logspace(-2, 3), ... labels=labels, leg_kwargs=dict(loc='lower right')) # doctest: +ELLIPSIS (<matplotlib.axes._subplots.AxesSubplot object at 0x...>, <matplotlib.axes._subplots.AxesSubplot object at 0x...>) >>> save() Saved examples/PIDs-bode.pdf Saved examples/PIDs-bode.png .. only:: html .. image:: ../examples/PIDs-bode.png :scale: 70 % :alt: Bode plot of PID with varying parameters .. only:: latex .. figure:: ../examples/PIDs-bode.pdf :scale: 70 % Bode plot of PID with varying parameters """ # Create axes if necessary. if not axes: fig = figure(label) axes = (fig.add_subplot(211), fig.add_subplot(212)) # Process the lins input. if not iterable(lins): lins = [lins] # Process the labels input. if labels == '': labels = [lin.fbase for lin in lins] elif labels == None: labels = ['']*len(lins) # Set up the color(s) and line style(s). if not iterable(colors): # Use the single color for all plots. colors = (colors,) if not iterable(styles): # Use the single line style for all plots. styles = [styles] elif type(styles[0]) is int: # One dashes tuple has been provided; use its value for all plots. styles = [styles] n_colors = len(colors) n_styles = len(styles) # Create the plots. for i, (lin, label) in enumerate(zip(lins, labels)): if lin.sys.inputs > 1 or lin.sys.outputs > 1: # Extract the SISO TF. TODO: Is there a better way to do this? sys = ss(self.sys.A, self.sys.B[:, pair[0]], self.sys.C[pair[1], :], self.sys.D[pair[1], pair[0]]) else: sys = lin.sys bode(sys, Hz=True, label=label, color=colors[np.mod(i, n_colors)], axes=axes, style=styles[np.mod(i, n_styles)], **kwargs) # Decorate and finish. axes[0].set_title(title) if leg_kwargs is not None: loc = leg_kwargs.pop('loc', 'best') axes[0].legend(loc=loc, **leg_kwargs) axes[1].legend(loc=loc, **leg_kwargs) return axes