|
54 | 54 | from .exception import ControlMIMONotImplemented |
55 | 55 | from .statesp import StateSpace |
56 | 56 | from .xferfcn import TransferFunction |
| 57 | +from .lti import evalfr |
57 | 58 | from . import config |
58 | 59 |
|
59 | | -__all__ = ['bode_plot', 'nyquist_plot', 'gangof4_plot', |
| 60 | +__all__ = ['bode_plot', 'nyquist_plot', 'gangof4_plot', 'singular_values_plot', |
60 | 61 | 'bode', 'nyquist', 'gangof4'] |
61 | 62 |
|
62 | 63 | # Default values for module parameter variables |
@@ -172,7 +173,7 @@ def bode_plot(syslist, omega=None, |
172 | 173 | >>> mag, phase, omega = bode(sys) |
173 | 174 |
|
174 | 175 | """ |
175 | | - # Make a copy of the kwargs dictonary since we will modify it |
| 176 | + # Make a copy of the kwargs dictionary since we will modify it |
176 | 177 | kwargs = dict(kwargs) |
177 | 178 |
|
178 | 179 | # Check to see if legacy 'Plot' keyword was used |
@@ -1039,14 +1040,173 @@ def gangof4_plot(P, C, omega=None, **kwargs): |
1039 | 1040 |
|
1040 | 1041 | plt.tight_layout() |
1041 | 1042 |
|
| 1043 | +# |
| 1044 | +# Singular value plot |
| 1045 | +# |
| 1046 | + |
| 1047 | + |
| 1048 | +def singular_values_plot(syslist, omega=None, |
| 1049 | + plot=True, omega_limits=None, omega_num=None, |
| 1050 | + *args, **kwargs): |
| 1051 | + """Singular value plot for a system |
| 1052 | +
|
| 1053 | + Plots a Singular Value plot for the system over a (optional) frequency range. |
| 1054 | +
|
| 1055 | + Parameters |
| 1056 | + ---------- |
| 1057 | + syslist : linsys |
| 1058 | + List of linear systems (single system is OK) |
| 1059 | + omega : array_like |
| 1060 | + List of frequencies in rad/sec to be used for frequency response |
| 1061 | + plot : bool |
| 1062 | + If True (default), plot magnitude and phase |
| 1063 | + omega_limits : array_like of two values |
| 1064 | + Limits of the to generate frequency vector. |
| 1065 | + If Hz=True the limits are in Hz otherwise in rad/s. |
| 1066 | + omega_num : int |
| 1067 | + Number of samples to plot. Defaults to |
| 1068 | + config.defaults['freqplot.number_of_samples']. |
| 1069 | +
|
| 1070 | + Returns |
| 1071 | + ------- |
| 1072 | + sigma : ndarray (or list of ndarray if len(syslist) > 1)) |
| 1073 | + singular values |
| 1074 | + omega : ndarray (or list of ndarray if len(syslist) > 1)) |
| 1075 | + frequency in rad/sec |
| 1076 | +
|
| 1077 | + Other Parameters |
| 1078 | + ---------------- |
| 1079 | + grid : bool |
| 1080 | + If True, plot grid lines on gain and phase plots. Default is set by |
| 1081 | + `config.defaults['bode.grid']`. |
| 1082 | +
|
| 1083 | + Examples |
| 1084 | + -------- |
| 1085 | + >>> den = [75, 1] |
| 1086 | + >>> sys = ct.tf([[[87.8], [-86.4]], [[108.2], [-109.6]]], [[den, den], [den, den]]) |
| 1087 | + >>> omega = np.logspace(-4, 1, 1000) |
| 1088 | + >>> sigma, omega = singular_values_plot(sys) |
| 1089 | +
|
| 1090 | + """ |
| 1091 | + # Make a copy of the kwargs dictionary since we will modify it |
| 1092 | + kwargs = dict(kwargs) |
| 1093 | + |
| 1094 | + # Check to see if legacy 'Plot' keyword was used |
| 1095 | + if 'Plot' in kwargs: |
| 1096 | + import warnings |
| 1097 | + warnings.warn("'Plot' keyword is deprecated in bode_plot; use 'plot'", |
| 1098 | + FutureWarning) |
| 1099 | + # Map 'Plot' keyword to 'plot' keyword |
| 1100 | + plot = kwargs.pop('Plot') |
| 1101 | + |
| 1102 | + # Get values for params (and pop from list to allow keyword use in plot) |
| 1103 | + dB = config._get_param('bode', 'dB', kwargs, _bode_defaults, pop=True) |
| 1104 | + Hz = config._get_param('bode', 'Hz', kwargs, _bode_defaults, pop=True) |
| 1105 | + grid = config._get_param('bode', 'grid', kwargs, _bode_defaults, pop=True) |
| 1106 | + plot = config._get_param('bode', 'grid', plot, True) |
| 1107 | + |
| 1108 | + # If argument was a singleton, turn it into a tuple |
| 1109 | + if not hasattr(syslist, '__iter__'): |
| 1110 | + syslist = (syslist,) |
| 1111 | + |
| 1112 | + # Decide whether to go above Nyquist frequency |
| 1113 | + omega_range_given = True if omega is not None else False |
| 1114 | + |
| 1115 | + if omega is None: |
| 1116 | + omega_num = config._get_param( |
| 1117 | + 'freqplot', 'number_of_samples', omega_num) |
| 1118 | + if omega_limits is None: |
| 1119 | + # Select a default range if none is provided |
| 1120 | + omega = _default_frequency_range(syslist, number_of_samples=omega_num) |
| 1121 | + else: |
| 1122 | + omega_range_given = True |
| 1123 | + omega_limits = np.asarray(omega_limits) |
| 1124 | + if len(omega_limits) != 2: |
| 1125 | + raise ValueError("len(omega_limits) must be 2") |
| 1126 | + if Hz: |
| 1127 | + omega_limits *= 2. * math.pi |
| 1128 | + omega = np.logspace(np.log10(omega_limits[0]), |
| 1129 | + np.log10(omega_limits[1]), num=omega_num, |
| 1130 | + endpoint=True) |
| 1131 | + |
| 1132 | + if plot: |
| 1133 | + fig = plt.gcf() |
| 1134 | + ax_sigma = None |
| 1135 | + |
| 1136 | + # Get the current axes if they already exist |
| 1137 | + for ax in fig.axes: |
| 1138 | + if ax.get_label() == 'control-sigma': |
| 1139 | + ax_sigma = ax |
| 1140 | + |
| 1141 | + # If no axes present, create them from scratch |
| 1142 | + if ax_sigma is None: |
| 1143 | + plt.clf() |
| 1144 | + ax_sigma = plt.subplot(111, label='control-sigma') |
1042 | 1145 |
|
| 1146 | + color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color'] |
| 1147 | + |
| 1148 | + sigmas, omegas, nyquistfrqs = [], [], [] |
| 1149 | + for idx_sys, sys in enumerate(syslist): |
| 1150 | + omega_sys = np.asarray(omega) |
| 1151 | + if sys.isdtime(strict=True): |
| 1152 | + nyquistfrq = math.pi / sys.dt |
| 1153 | + if not omega_range_given: |
| 1154 | + # limit up to and including nyquist frequency |
| 1155 | + omega_sys = np.hstack(( |
| 1156 | + omega_sys[omega_sys < nyquistfrq], nyquistfrq)) |
| 1157 | + else: |
| 1158 | + nyquistfrq = None |
| 1159 | + |
| 1160 | + mag, phase, omega = sys.frequency_response(omega) |
| 1161 | + fresp = mag * np.exp(1j * phase) |
| 1162 | + #fresp = evalfr(sys, 1j * omega_sys) |
| 1163 | + |
| 1164 | + fresp = fresp.transpose((2, 0, 1)) |
| 1165 | + sigma = np.linalg.svd(fresp, compute_uv=False) |
| 1166 | + |
| 1167 | + sigmas.append(sigma) |
| 1168 | + omegas.append(omega_sys) |
| 1169 | + nyquistfrqs.append(nyquistfrq) |
| 1170 | + |
| 1171 | + if plot: |
| 1172 | + color = color_cycle[idx_sys % len(color_cycle)] |
| 1173 | + |
| 1174 | + nyquistfrq_plot = None |
| 1175 | + if Hz: |
| 1176 | + omega_plot = omega_sys / (2. * math.pi) |
| 1177 | + if nyquistfrq: |
| 1178 | + nyquistfrq_plot = nyquistfrq / (2. * math.pi) |
| 1179 | + else: |
| 1180 | + omega_plot = omega_sys |
| 1181 | + if nyquistfrq: |
| 1182 | + nyquistfrq_plot = nyquistfrq |
| 1183 | + sigma_plot = sigma |
| 1184 | + |
| 1185 | + if dB: |
| 1186 | + ax_sigma.semilogx(omega_plot, 20 * np.log10(sigma_plot), color=color, *args, **kwargs) |
| 1187 | + else: |
| 1188 | + ax_sigma.loglog(omega_plot, sigma_plot, color=color, *args, **kwargs) |
| 1189 | + |
| 1190 | + if nyquistfrq_plot is not None: |
| 1191 | + ax_sigma.axvline(x=nyquistfrq_plot, color=color) |
| 1192 | + |
| 1193 | + # Add a grid to the plot + labeling |
| 1194 | + ax_sigma.grid(grid, which='both') |
| 1195 | + ax_sigma.set_ylabel("Magnitude (dB)" if dB else "Magnitude") |
| 1196 | + ax_sigma.set_xlabel("Frequency (Hz)" if Hz else "Frequency (rad/sec)") |
| 1197 | + |
| 1198 | + if len(syslist) == 1: |
| 1199 | + return sigmas[0], omegas[0] |
| 1200 | + else: |
| 1201 | + return sigmas, omegas |
1043 | 1202 | # |
1044 | 1203 | # Utility functions |
1045 | 1204 | # |
1046 | 1205 | # This section of the code contains some utility functions for |
1047 | 1206 | # generating frequency domain plots |
1048 | 1207 | # |
1049 | 1208 |
|
| 1209 | + |
1050 | 1210 | # Compute reasonable defaults for axes |
1051 | 1211 | def _default_frequency_range(syslist, Hz=None, number_of_samples=None, |
1052 | 1212 | feature_periphery_decades=None): |
|
0 commit comments