diff --git a/.github/conda-env/doctest-env.yml b/.github/conda-env/doctest-env.yml index a03b64ae7..4c0d36728 100644 --- a/.github/conda-env/doctest-env.yml +++ b/.github/conda-env/doctest-env.yml @@ -7,7 +7,7 @@ dependencies: - numpy - matplotlib - scipy - - sphinx + - sphinx<8.2 - sphinx_rtd_theme - ipykernel - nbsphinx diff --git a/.github/scripts/set-conda-test-matrix.py b/.github/scripts/set-conda-test-matrix.py index 69ef0a57e..49714e7eb 100644 --- a/.github/scripts/set-conda-test-matrix.py +++ b/.github/scripts/set-conda-test-matrix.py @@ -1,6 +1,8 @@ """Create test matrix for conda packages in OS/BLAS test matrix workflow.""" +import json from pathlib import Path +import re osmap = {'linux': 'ubuntu', 'osx': 'macos', @@ -25,5 +27,19 @@ 'blas_lib': cbl} conda_jobs.append(cjob) +# Make sure Windows jobs are included even if we didn't build any +windows_pythons = ['3.11'] # Whatever you want to test + +for py in windows_pythons: + for blas in combinations['windows']: + cjob = { + 'packagekey': f'windows-{py}', + 'os': 'windows', + 'python': py, + 'blas_lib': blas, + 'package_source': 'conda-forge' + } + conda_jobs.append(cjob) + matrix = { 'include': conda_jobs } print(json.dumps(matrix)) diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index 9006c3687..590d4a97f 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -19,7 +19,7 @@ jobs: activate-environment: doctest-env environment-file: .github/conda-env/doctest-env.yml miniforge-version: latest - channels: conda-forge + channels: conda-forge,defaults channel-priority: strict auto-update-conda: false auto-activate-base: false diff --git a/.github/workflows/install_examples.yml b/.github/workflows/install_examples.yml index cfbf40fe7..6893a99fb 100644 --- a/.github/workflows/install_examples.yml +++ b/.github/workflows/install_examples.yml @@ -20,7 +20,8 @@ jobs: --quiet --yes \ python=3.12 pip \ numpy matplotlib scipy \ - slycot pmw jupyter + slycot pmw jupyter \ + ipython!=9.0 - name: Install from source run: | diff --git a/.github/workflows/os-blas-test-matrix.yml b/.github/workflows/os-blas-test-matrix.yml index 242d89732..3661c0499 100644 --- a/.github/workflows/os-blas-test-matrix.yml +++ b/.github/workflows/os-blas-test-matrix.yml @@ -107,7 +107,6 @@ jobs: os: - 'ubuntu' - 'macos' - - 'windows' python: # build on one, expand matrix in conda-build from the Sylcot/conda-recipe/conda_build_config.yaml - '3.11' @@ -126,6 +125,7 @@ jobs: activate-environment: build-env environment-file: .github/conda-env/build-env.yml miniforge-version: latest + channels: conda-forge,defaults channel-priority: strict auto-update-conda: false auto-activate-base: false @@ -170,7 +170,10 @@ jobs: name: slycot-wheels path: slycot-wheels - id: set-matrix - run: echo "matrix=$(python3 .github/scripts/set-pip-test-matrix.py)" >> $GITHUB_OUTPUT + run: | + TEMPFILE="$(mktemp)" + python3 .github/scripts/set-pip-test-matrix.py | tee $TEMPFILE + echo "matrix=$(cat $TEMPFILE)" >> $GITHUB_OUTPUT create-conda-test-matrix: @@ -194,7 +197,10 @@ jobs: name: slycot-conda-pkgs path: slycot-conda-pkgs - id: set-matrix - run: echo "matrix=$(python3 .github/scripts/set-conda-test-matrix.py)" >> $GITHUB_OUTPUT + run: | + TEMPFILE="$(mktemp)" + python3 .github/scripts/set-conda-test-matrix.py | tee $TEMPFILE + echo "matrix=$(cat $TEMPFILE)" >> $GITHUB_OUTPUT test-wheel: @@ -297,6 +303,7 @@ jobs: miniforge-version: latest activate-environment: test-env environment-file: .github/conda-env/test-env.yml + channels: conda-forge,defaults channel-priority: strict auto-activate-base: false - name: Download conda packages @@ -324,7 +331,13 @@ jobs: echo "libblas * *mkl" >> $CONDA_PREFIX/conda-meta/pinned ;; esac - conda install -c ./slycot-conda-pkgs slycot + if [ "${{ matrix.os }}" = "windows" ]; then + echo "Installing slycot from conda-forge on Windows" + conda install slycot + else + echo "Installing built conda package from local channel" + conda install -c ./slycot-conda-pkgs slycot + fi conda list - name: Test with pytest run: JOBNAME="$JOBNAME" pytest control/tests diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index 00e03e0e0..0aabf33bf 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -38,7 +38,7 @@ jobs: activate-environment: test-env environment-file: .github/conda-env/test-env.yml miniforge-version: latest - channels: conda-forge + channels: conda-forge,defaults channel-priority: strict auto-update-conda: false auto-activate-base: false diff --git a/benchmarks/flatsys_bench.py b/benchmarks/flatsys_bench.py index 05a2e7066..a2f8ae1d2 100644 --- a/benchmarks/flatsys_bench.py +++ b/benchmarks/flatsys_bench.py @@ -7,7 +7,6 @@ import numpy as np import math -import control as ct import control.flatsys as flat import control.optimal as opt diff --git a/benchmarks/optestim_bench.py b/benchmarks/optestim_bench.py index 612ee6bb3..534d1024d 100644 --- a/benchmarks/optestim_bench.py +++ b/benchmarks/optestim_bench.py @@ -6,7 +6,6 @@ # used for optimization-based estimation. import numpy as np -import math import control as ct import control.optimal as opt diff --git a/benchmarks/optimal_bench.py b/benchmarks/optimal_bench.py index 997b5a241..bd0c0cd6b 100644 --- a/benchmarks/optimal_bench.py +++ b/benchmarks/optimal_bench.py @@ -6,7 +6,6 @@ # performance of the functions used for optimization-base control. import numpy as np -import math import control as ct import control.flatsys as fs import control.optimal as opt @@ -21,7 +20,6 @@ 'RK23': ('RK23', {}), 'RK23_sloppy': ('RK23', {'atol': 1e-4, 'rtol': 1e-2}), 'RK45': ('RK45', {}), - 'RK45': ('RK45', {}), 'RK45_sloppy': ('RK45', {'atol': 1e-4, 'rtol': 1e-2}), 'LSODA': ('LSODA', {}), } @@ -129,9 +127,6 @@ def time_optimal_lq_methods(integrator_name, minimizer_name, method): Tf = 10 timepts = np.linspace(0, Tf, 20) - # Create the basis function to use - basis = get_basis('poly', 12, Tf) - res = opt.solve_ocp( sys, timepts, x0, traj_cost, constraints, terminal_cost=term_cost, solve_ivp_method=integrator[0], solve_ivp_kwargs=integrator[1], @@ -223,8 +218,6 @@ def time_discrete_aircraft_mpc(minimizer_name): # compute the steady state values for a particular value of the input ud = np.array([0.8, -0.3]) xd = np.linalg.inv(np.eye(5) - A) @ B @ ud - yd = C @ xd - # provide constraints on the system signals constraints = [opt.input_range_constraint(sys, [-5, -6], [5, 6])] @@ -234,7 +227,6 @@ def time_discrete_aircraft_mpc(minimizer_name): cost = opt.quadratic_cost(model, Q, R, x0=xd, u0=ud) # Set the time horizon and time points - Tf = 3 timepts = np.arange(0, 6) * 0.2 # Get the minimizer parameters to use diff --git a/control/config.py b/control/config.py index 8da7e2fc2..12d7b0052 100644 --- a/control/config.py +++ b/control/config.py @@ -297,7 +297,7 @@ def use_legacy_defaults(version): Parameters ---------- version : string - Version number of the defaults desired. Ranges from '0.1' to '0.10.1'. + Version number of `python-control` to use for setting defaults. Examples -------- @@ -342,6 +342,14 @@ def use_legacy_defaults(version): # reset_defaults() # start from a clean slate + # Version 0.10.2: + if major == 0 and minor < 10 or (minor == 10 and patch < 2): + from math import inf + + # Reset Nyquist defaults + set_defaults('nyquist', arrows=2, max_curve_magnitude=20, + blend_fraction=0, indent_points=50) + # Version 0.9.2: if major == 0 and minor < 9 or (minor == 9 and patch < 2): from math import inf diff --git a/control/ctrlplot.py b/control/ctrlplot.py index dbdb4e1ec..50b2751d8 100644 --- a/control/ctrlplot.py +++ b/control/ctrlplot.py @@ -41,7 +41,10 @@ # # Customize axes (curvilinear grids, shared axes, etc) # # # Plot the data -# lines = np.full(ax_array.shape, []) +# lines = np.empty(ax_array.shape, dtype=object) +# for i in range(ax_array.shape[0]): +# for j in range(ax_array.shape[1]): +# lines[i, j] = [] # line_labels = _process_line_labels(label, ntraces, nrows, ncols) # color_offset, color_cycle = _get_color_offset(ax) # for i, j in itertools.product(range(nrows), range(ncols)): @@ -355,7 +358,7 @@ def _process_ax_keyword( the calling function to do the actual axis creation (needed for curvilinear grids that use the AxisArtist module). - Legacy behavior: some of the older plotting commands use a axes label + Legacy behavior: some of the older plotting commands use an axes label to identify the proper axes for plotting. This behavior is supported through the use of the label keyword, but will only work if shape == (1, 1) and squeeze == True. diff --git a/control/descfcn.py b/control/descfcn.py index bfe2d1a7e..6f3f5169d 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -9,7 +9,6 @@ import math from warnings import warn -import matplotlib.pyplot as plt import numpy as np import scipy @@ -521,16 +520,18 @@ def describing_function_plot( # Plot the Nyquist response cplt = dfresp.response.plot(**kwargs) - lines[0] = cplt.lines[0] # Return Nyquist lines for first system + ax = cplt.axes[0, 0] # Get the axes where the plot was made + lines[0] = np.concatenate( # Return Nyquist lines for first system + cplt.lines.flatten()).tolist() # Add the describing function curve to the plot - lines[1] = plt.plot(dfresp.N_vals.real, dfresp.N_vals.imag) + lines[1] = ax.plot(dfresp.N_vals.real, dfresp.N_vals.imag) # Label the intersection points if point_label: for pos, (a, omega) in zip(dfresp.positions, dfresp.intersections): # Add labels to the intersection points - plt.text(pos.real, pos.imag, point_label % (a, omega)) + ax.text(pos.real, pos.imag, point_label % (a, omega)) return ControlPlot(lines, cplt.axes, cplt.figure) diff --git a/control/freqplot.py b/control/freqplot.py index fe258d636..475467147 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -1100,13 +1100,14 @@ def gen_zero_centered_series(val_min, val_max, period): _nyquist_defaults = { 'nyquist.primary_style': ['-', '-.'], # style for primary curve 'nyquist.mirror_style': ['--', ':'], # style for mirror curve - 'nyquist.arrows': 2, # number of arrows around curve + 'nyquist.arrows': 3, # number of arrows around curve 'nyquist.arrow_size': 8, # pixel size for arrows 'nyquist.encirclement_threshold': 0.05, # warning threshold 'nyquist.indent_radius': 1e-4, # indentation radius 'nyquist.indent_direction': 'right', # indentation direction - 'nyquist.indent_points': 50, # number of points to insert - 'nyquist.max_curve_magnitude': 20, # clip large values + 'nyquist.indent_points': 200, # number of points to insert + 'nyquist.max_curve_magnitude': 15, # rescale large values + 'nyquist.blend_fraction': 0.15, # when to start scaling 'nyquist.max_curve_offset': 0.02, # offset of primary/mirror 'nyquist.start_marker': 'o', # marker at start of curve 'nyquist.start_marker_size': 4, # size of the marker @@ -1638,6 +1639,10 @@ def nyquist_plot( The matplotlib axes to draw the figure on. If not specified and the current figure has a single axes, that axes is used. Otherwise, a new figure is created. + blend_fraction : float, optional + For portions of the Nyquist curve that are scaled to have a maximum + magnitude of `max_curve_magnitude`, begin a smooth rescaling at + this fraction of `max_curve_magnitude`. Default value is 0.15. encirclement_threshold : float, optional Define the threshold for generating a warning if the number of net encirclements is a non-integer value. Default value is 0.05 and can @@ -1654,7 +1659,7 @@ def nyquist_plot( portions of the contour are plotted using a different line style. label : str or array_like of str, optional If present, replace automatically generated label(s) with the given - label(s). If sysdata is a list, strings should be specified for each + label(s). If `data` is a list, strings should be specified for each system. label_freq : int, optional Label every nth frequency on the plot. If not specified, no labels @@ -1685,8 +1690,8 @@ def nyquist_plot( elements is equivalent to providing `omega_limits`. omega_num : int, optional Number of samples to use for the frequency range. Defaults to - `config.defaults['freqplot.number_of_samples']`. Ignored if data is - not a list of systems. + `config.defaults['freqplot.number_of_samples']`. Ignored if `data` + is not a system or list of systems. plot : bool, optional (legacy) If given, `nyquist_plot` returns the legacy return values of (counts, contours). If False, return the values with no plot. @@ -1751,8 +1756,8 @@ def nyquist_plot( to avoid poles, resulting in a scaling of the Nyquist plot, the line styles are according to the settings of the `primary_style` and `mirror_style` keywords. By default the scaled portions of the primary - curve use a dotted line style and the scaled portion of the mirror - image use a dashdot line style. + curve use a dashdot line style and the scaled portions of the mirror + image use a dotted line style. Examples -------- @@ -1784,6 +1789,8 @@ def nyquist_plot( ax_user = ax max_curve_magnitude = config._get_param( 'nyquist', 'max_curve_magnitude', kwargs, _nyquist_defaults, pop=True) + blend_fraction = config._get_param( + 'nyquist', 'blend_fraction', kwargs, _nyquist_defaults, pop=True) max_curve_offset = config._get_param( 'nyquist', 'max_curve_offset', kwargs, _nyquist_defaults, pop=True) rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True) @@ -1878,10 +1885,16 @@ def _parse_linestyle(style_name, allow_false=False): legend_loc, _, show_legend = _process_legend_keywords( kwargs, None, 'upper right') + # Figure out where the blended curve should start + if blend_fraction < 0 or blend_fraction > 1: + raise ValueError("blend_fraction must be between 0 and 1") + blend_curve_start = (1 - blend_fraction) * max_curve_magnitude + # Create a list of lines for the output - out = np.empty(len(nyquist_responses), dtype=object) - for i in range(out.shape[0]): - out[i] = [] # unique list in each element + out = np.empty((len(nyquist_responses), 4), dtype=object) + for i in range(len(nyquist_responses)): + for j in range(4): + out[i, j] = [] # unique list in each element for idx, response in enumerate(nyquist_responses): resp = response.response @@ -1892,20 +1905,31 @@ def _parse_linestyle(style_name, allow_false=False): # Find the different portions of the curve (with scaled pts marked) reg_mask = np.logical_or( - np.abs(resp) > max_curve_magnitude, - splane_contour.real != 0) - # reg_mask = np.logical_or( - # np.abs(resp.real) > max_curve_magnitude, - # np.abs(resp.imag) > max_curve_magnitude) + np.abs(resp) > blend_curve_start, + np.logical_not(np.isclose(splane_contour.real, 0))) scale_mask = ~reg_mask \ & np.concatenate((~reg_mask[1:], ~reg_mask[-1:])) \ & np.concatenate((~reg_mask[0:1], ~reg_mask[:-1])) # Rescale the points with large magnitude - rescale = np.logical_and( - reg_mask, abs(resp) > max_curve_magnitude) - resp[rescale] *= max_curve_magnitude / abs(resp[rescale]) + rescale_idx = (np.abs(resp) > blend_curve_start) + + if np.any(rescale_idx): # Only process if rescaling is needed + subset = resp[rescale_idx] + abs_subset = np.abs(subset) + unit_vectors = subset / abs_subset # Preserve phase/direction + + if blend_curve_start == max_curve_magnitude: + # Clip at max_curve_magnitude + resp[rescale_idx] = max_curve_magnitude * unit_vectors + else: + # Logistic scaling + newmag = blend_curve_start + \ + (max_curve_magnitude - blend_curve_start) * \ + (abs_subset - blend_curve_start) / \ + (abs_subset + max_curve_magnitude - 2 * blend_curve_start) + resp[rescale_idx] = newmag * unit_vectors # Get the label to use for the line label = response.sysname if line_labels is None else line_labels[idx] @@ -1913,10 +1937,10 @@ def _parse_linestyle(style_name, allow_false=False): # Plot the regular portions of the curve (and grab the color) x_reg = np.ma.masked_where(reg_mask, resp.real) y_reg = np.ma.masked_where(reg_mask, resp.imag) - p = plt.plot( + p = ax.plot( x_reg, y_reg, primary_style[0], color=color, label=label, **kwargs) c = p[0].get_color() - out[idx] += p + out[idx, 0] += p # Figure out how much to offset the curve: the offset goes from # zero at the start of the scaled section to max_curve_offset as @@ -1928,54 +1952,56 @@ def _parse_linestyle(style_name, allow_false=False): x_scl = np.ma.masked_where(scale_mask, resp.real) y_scl = np.ma.masked_where(scale_mask, resp.imag) if x_scl.count() >= 1 and y_scl.count() >= 1: - out[idx] += plt.plot( + out[idx, 1] += ax.plot( x_scl * (1 + curve_offset), y_scl * (1 + curve_offset), primary_style[1], color=c, **kwargs) else: - out[idx] += [None] + out[idx, 1] += [None] # Plot the primary curve (invisible) for setting arrows x, y = resp.real.copy(), resp.imag.copy() x[reg_mask] *= (1 + curve_offset[reg_mask]) y[reg_mask] *= (1 + curve_offset[reg_mask]) - p = plt.plot(x, y, linestyle='None', color=c) + p = ax.plot(x, y, linestyle='None', color=c) # Add arrows - ax = plt.gca() _add_arrows_to_line2D( ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=1) # Plot the mirror image if mirror_style is not False: # Plot the regular and scaled segments - out[idx] += plt.plot( + out[idx, 2] += ax.plot( x_reg, -y_reg, mirror_style[0], color=c, **kwargs) if x_scl.count() >= 1 and y_scl.count() >= 1: - out[idx] += plt.plot( + out[idx, 3] += ax.plot( x_scl * (1 - curve_offset), -y_scl * (1 - curve_offset), mirror_style[1], color=c, **kwargs) else: - out[idx] += [None] + out[idx, 3] += [None] # Add the arrows (on top of an invisible contour) x, y = resp.real.copy(), resp.imag.copy() x[reg_mask] *= (1 - curve_offset[reg_mask]) y[reg_mask] *= (1 - curve_offset[reg_mask]) - p = plt.plot(x, -y, linestyle='None', color=c, **kwargs) + p = ax.plot(x, -y, linestyle='None', color=c, **kwargs) _add_arrows_to_line2D( ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=-1) else: - out[idx] += [None, None] + out[idx, 2] += [None] + out[idx, 3] += [None] # Mark the start of the curve if start_marker: - plt.plot(resp[0].real, resp[0].imag, start_marker, - color=c, markersize=start_marker_size) + segment = 0 if 0 in rescale_idx else 1 # regular vs scaled + out[idx, segment] += ax.plot( + resp[0].real, resp[0].imag, start_marker, + color=c, markersize=start_marker_size) # Mark the -1 point - plt.plot([-1], [0], 'r+') + ax.plot([-1], [0], 'r+') # # Draw circles for gain crossover and sensitivity functions @@ -1987,16 +2013,16 @@ def _parse_linestyle(style_name, allow_false=False): # Display the unit circle, to read gain crossover frequency if unit_circle: - plt.plot(cos, sin, **config.defaults['nyquist.circle_style']) + ax.plot(cos, sin, **config.defaults['nyquist.circle_style']) # Draw circles for given magnitudes of sensitivity if ms_circles is not None: for ms in ms_circles: pos_x = -1 + (1/ms)*cos pos_y = (1/ms)*sin - plt.plot( + ax.plot( pos_x, pos_y, **config.defaults['nyquist.circle_style']) - plt.text(pos_x[label_pos], pos_y[label_pos], ms) + ax.text(pos_x[label_pos], pos_y[label_pos], ms) # Draw circles for given magnitudes of complementary sensitivity if mt_circles is not None: @@ -2006,17 +2032,17 @@ def _parse_linestyle(style_name, allow_false=False): rt = mt/(mt**2-1) # Mt radius pos_x = ct+rt*cos pos_y = rt*sin - plt.plot( + ax.plot( pos_x, pos_y, **config.defaults['nyquist.circle_style']) - plt.text(pos_x[label_pos], pos_y[label_pos], mt) + ax.text(pos_x[label_pos], pos_y[label_pos], mt) else: - _, _, ymin, ymax = plt.axis() + _, _, ymin, ymax = ax.axis() pos_y = np.linspace(ymin, ymax, 100) - plt.vlines( + ax.vlines( -0.5, ymin=ymin, ymax=ymax, **config.defaults['nyquist.circle_style']) - plt.text(-0.5, pos_y[label_pos], 1) + ax.text(-0.5, pos_y[label_pos], 1) # Label the frequencies of the points on the Nyquist curve if label_freq: @@ -2039,7 +2065,7 @@ def _parse_linestyle(style_name, allow_false=False): # np.round() is used because 0.99... appears # instead of 1.0, and this would otherwise be # truncated to 0. - plt.text(xpt, ypt, ' ' + + ax.text(xpt, ypt, ' ' + str(int(np.round(f / 1000 ** pow1000, 0))) + ' ' + prefix + 'Hz') @@ -2556,12 +2582,12 @@ def singular_values_plot( nyq_freq = None # Determine the color to use for this response - color = _get_color( + current_color = _get_color( color, fmt=fmt, offset=color_offset + idx_sys, color_cycle=color_cycle) # To avoid conflict with *fmt, only pass color kw if non-None - color_arg = {} if color is None else {'color': color} + color_arg = {} if current_color is None else {'color': current_color} # Decide on the system name sysname = response.sysname if response.sysname is not None \ diff --git a/control/iosys.py b/control/iosys.py index 110552138..42cf4094d 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -46,6 +46,24 @@ class NamedSignal(np.ndarray): This class modifies the `numpy.ndarray` class and allows signals to be accessed using the signal name in addition to indices and slices. + Signals can be either a 2D array, index by signal and time, or a 3D + array, indexed by signal, trace, and time. + + Attributes + ---------- + signal_labels : list of str + Label names for each of the signal elements in the signal. + trace_labels : list of str, optional + Label names for each of the traces in the signal (if multi-trace). + + Examples + -------- + >>> sys = ct.rss( + ... states=['p1', 'p2', 'p3'], inputs=['u1', 'u2'], outputs=['y']) + >>> resp = ct.step_response(sys) + >>> resp.states['p1', 'u1'] # Step response from u1 to p1 + NamedSignal(...) + """ def __new__(cls, input_array, signal_labels=None, trace_labels=None): # See https://numpy.org/doc/stable/user/basics.subclassing.html @@ -315,6 +333,9 @@ def _repr_html_(self): # Defaults to using __repr__; override in subclasses return None + def _repr_markdown_(self): + return self._repr_html_() + @property def repr_format(self): """String representation format. diff --git a/control/margins.py b/control/margins.py index d7c7992be..57e825c65 100644 --- a/control/margins.py +++ b/control/margins.py @@ -11,12 +11,17 @@ import numpy as np import scipy as sp -from . import frdata, freqplot, xferfcn +from . import frdata, freqplot, xferfcn, statesp from .exception import ControlMIMONotImplemented from .iosys import issiso +from .ctrlutil import mag2db +try: + from slycot import ab13md +except ImportError: + ab13md = None -__all__ = ['stability_margins', 'phase_crossover_frequencies', 'margin'] - +__all__ = ['stability_margins', 'phase_crossover_frequencies', 'margin', + 'disk_margins'] # private helper functions def _poly_iw(sys): @@ -168,6 +173,7 @@ def fun(wdt): return z, w + def _likely_numerical_inaccuracy(sys): # crude, conservative check for if # num(z)*num(1/z) << den(z)*den(1/z) for DT systems @@ -517,3 +523,137 @@ def margin(*args): % len(args)) return margin[0], margin[1], margin[3], margin[4] + + +def disk_margins(L, omega, skew=0.0, returnall=False): + """Disk-based stability margins of loop transfer function. + + Parameters + ---------- + L : `StateSpace` or `TransferFunction` + Linear SISO or MIMO loop transfer function. + omega : sequence of array_like + 1D array of (non-negative) frequencies (rad/s) at which + to evaluate the disk-based stability margins. + skew : float or array_like, optional + Skew parameter(s) for disk margin (default = 0.0): + + * skew = 0.0 "balanced" sensitivity function 0.5*(S - T) + * skew = 1.0 sensitivity function S + * skew = -1.0 complementary sensitivity function T + + returnall : bool, optional + If True, return frequency-dependent margins. + If False (default), return worst-case (minimum) margins. + + Returns + ------- + DM : float or array_like + Disk margin. + DGM : float or array_like + Disk-based gain margin. + DPM : float or array_like + Disk-based phase margin. + + Examples + -------- + >> omega = np.logspace(-1, 3, 1001) + >> P = control.ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], + [-10, 1]], 0) + >> K = control.ss([], [], [], [[1, -2], [0, 1]]) + >> L = P * K + >> DM, DGM, DPM = control.disk_margins(L, omega, skew=0.0) + """ + + # First argument must be a system + if not isinstance(L, (statesp.StateSpace, xferfcn.TransferFunction)): + raise ValueError( + "Loop gain must be state-space or transfer function object") + + # Loop transfer function must be square + if statesp.ss(L).B.shape[1] != statesp.ss(L).C.shape[0]: + raise ValueError("Loop gain must be square (n_inputs = n_outputs)") + + # Need slycot if L is MIMO, for mu calculation + if not L.issiso() and ab13md == None: + raise ControlMIMONotImplemented( + "Need slycot to compute MIMO disk_margins") + + # Get dimensions of feedback system + num_loops = statesp.ss(L).C.shape[0] + I = statesp.ss([], [], [], np.eye(num_loops)) + + # Loop sensitivity function + S = I.feedback(L) + + # Compute frequency response of the "balanced" (according + # to the skew parameter "sigma") sensitivity function [1-2] + ST = S + 0.5 * (skew - 1) * I + ST_mag, ST_phase, _ = ST.frequency_response(omega) + ST_jw = (ST_mag * np.exp(1j * ST_phase)) + if not L.issiso(): + ST_jw = ST_jw.transpose(2, 0, 1) + + # Frequency-dependent complex disk margin, computed using + # upper bound of the structured singular value, a.k.a. "mu", + # of (S + (skew - I)/2). + DM = np.zeros(omega.shape) + DGM = np.zeros(omega.shape) + DPM = np.zeros(omega.shape) + for ii in range(0, len(omega)): + # Disk margin (a.k.a. "alpha") vs. frequency + if L.issiso() and ab13md == None: + # For the SISO case, the norm on (S + (skew - I)/2) is + # unstructured, and can be computed as the magnitude + # of the frequency response. + DM[ii] = 1.0 / ST_mag[ii] + else: + # For the MIMO case, the norm on (S + (skew - I)/2) + # assumes a single complex uncertainty block diagonal + # uncertainty structure. AB13MD provides an upper bound + # on this norm at the given frequency omega[ii]. + DM[ii] = 1.0 / ab13md(ST_jw[ii], np.array(num_loops * [1]), + np.array(num_loops * [2]))[0] + + # Disk-based gain margin (dB) and phase margin (deg) + with np.errstate(divide='ignore', invalid='ignore'): + # Real-axis intercepts with the disk + gamma_min = (1 - 0.5 * DM[ii] * (1 - skew)) \ + / (1 + 0.5 * DM[ii] * (1 + skew)) + gamma_max = (1 + 0.5 * DM[ii] * (1 - skew)) \ + / (1 - 0.5 * DM[ii] * (1 + skew)) + + # Gain margin (dB) + DGM[ii] = mag2db(np.minimum(1 / gamma_min, gamma_max)) + if np.isnan(DGM[ii]): + DGM[ii] = float('inf') + + # Phase margin (deg) + if np.isinf(gamma_max): + DPM[ii] = 90.0 + else: + DPM[ii] = (1 + gamma_min * gamma_max) \ + / (gamma_min + gamma_max) + if abs(DPM[ii]) >= 1.0: + DPM[ii] = float('Inf') + else: + DPM[ii] = np.rad2deg(np.arccos(DPM[ii])) + + if returnall: + # Frequency-dependent disk margin, gain margin and phase margin + return DM, DGM, DPM + else: + # Worst-case disk margin, gain margin and phase margin + if DGM.shape[0] and not np.isinf(DGM).all(): + with np.errstate(all='ignore'): + gmidx = np.where(DGM == np.min(DGM)) + else: + gmidx = -1 + + if DPM.shape[0]: + pmidx = np.where(DPM == np.min(DPM)) + + return ( + float('inf') if DM.shape[0] == 0 else np.amin(DM), + float('inf') if gmidx == -1 else DGM[gmidx][0], + float('inf') if DPM.shape[0] == 0 else DPM[pmidx][0]) diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index 6414c9131..facaf28bb 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -6,7 +6,7 @@ This subpackage contains a number of functions that emulate some of the functionality of MATLAB. The intent of these functions is to -provide a simple interface to the python control systems library +provide a simple interface to the Python Control Systems Library (python-control) for people who are familiar with the MATLAB Control Systems Toolbox (tm). diff --git a/control/nichols.py b/control/nichols.py index 3c4edcdbd..98775ddaf 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -132,15 +132,15 @@ def nichols_plot( out[idx] = ax_nichols.plot(x, y, *fmt, label=label_, **kwargs) # Label the plot axes - plt.xlabel('Phase [deg]') - plt.ylabel('Magnitude [dB]') + ax_nichols.set_xlabel('Phase [deg]') + ax_nichols.set_ylabel('Magnitude [dB]') # Mark the -180 point - plt.plot([-180], [0], 'r+') + ax_nichols.plot([-180], [0], 'r+') # Add grid if grid: - nichols_grid() + nichols_grid(ax=ax_nichols) # List of systems that are included in this plot lines, labels = _get_line_labels(ax_nichols) diff --git a/control/nlsys.py b/control/nlsys.py index 30f06f819..04fda2e40 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -1572,12 +1572,13 @@ def input_output_response( If discontinuous inputs are given, the underlying SciPy numerical integration algorithms can sometimes produce erroneous results due to - the default tolerances that are used. The `ivp_method` and - `ivp_keywords` parameters can be used to tune the ODE solver and - produce better results. In particular, using 'LSODA' as the - `ivp_method` or setting the `rtol` parameter to a smaller value - (e.g. using ``ivp_kwargs={'rtol': 1e-4}``) can provide more accurate - results. + the default tolerances that are used. The `solve_ivp_method` and + `solve_ivp_keywords` parameters can be used to tune the ODE solver and + produce better results. In particular, using 'LSODA' as the + `solve_ivp_method`, setting the `rtol` parameter to a smaller value + (e.g. using ``solve_ivp_kwargs={'rtol': 1e-4}``), or setting the + maximum step size to a smaller value (e.g. ``solve_ivp_kwargs= + {'max_step': 0.01}``) can provide more accurate results. """ # diff --git a/control/optimal.py b/control/optimal.py index 3242ac3fb..6b60c5b25 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -746,9 +746,9 @@ def _compute_states_inputs(self, coeffs): states = self.last_states else: states = self._simulate_states(self.x, inputs) - self.last_x = self.x - self.last_states = states - self.last_coeffs = coeffs + self.last_x = self.x.copy() # save initial state + self.last_states = states # always a new object + self.last_coeffs = coeffs.copy() # save coefficients return states, inputs diff --git a/control/phaseplot.py b/control/phaseplot.py index c17f5fbad..cf73d62a0 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -45,16 +45,19 @@ 'phaseplot.separatrices_radius': 0.1 # initial radius for separatrices } + def phase_plane_plot( sys, pointdata=None, timedata=None, gridtype=None, gridspec=None, - plot_streamlines=True, plot_vectorfield=False, plot_equilpoints=True, - plot_separatrices=True, ax=None, suppress_warnings=False, title=None, - **kwargs + plot_streamlines=None, plot_vectorfield=None, plot_streamplot=None, + plot_equilpoints=True, plot_separatrices=True, ax=None, + suppress_warnings=False, title=None, **kwargs ): """Plot phase plane diagram. This function plots phase plane data, including vector fields, stream lines, equilibrium points, and contour curves. + If none of plot_streamlines, plot_vectorfield, or plot_streamplot are + set, then plot_streamplot is used by default. Parameters ---------- @@ -105,6 +108,7 @@ def phase_plane_plot( - lines[0] = list of Line2D objects (streamlines, separatrices). - lines[1] = Quiver object (vector field arrows). - lines[2] = list of Line2D objects (equilibrium points). + - lines[3] = StreamplotSet object (lines with arrows). cplt.axes : 2D array of `matplotlib.axes.Axes` Axes for each subplot. @@ -128,13 +132,17 @@ def phase_plane_plot( 'both' to flow both forward and backward. The amount of time to simulate in each direction is given by the `timedata` argument. plot_streamlines : bool or dict, optional - If True (default) then plot streamlines based on the pointdata - and gridtype. If set to a dict, pass on the key-value pairs in - the dict as keywords to `streamlines`. + If True then plot streamlines based on the pointdata and gridtype. + If set to a dict, pass on the key-value pairs in the dict as + keywords to `streamlines`. plot_vectorfield : bool or dict, optional - If True (default) then plot the vector field based on the pointdata - and gridtype. If set to a dict, pass on the key-value pairs in - the dict as keywords to `phaseplot.vectorfield`. + If True then plot the vector field based on the pointdata and + gridtype. If set to a dict, pass on the key-value pairs in the + dict as keywords to `phaseplot.vectorfield`. + plot_streamplot : bool or dict, optional + If True then use `matplotlib.axes.Axes.streamplot` function + to plot the streamlines. If set to a dict, pass on the key-value + pairs in the dict as keywords to `phaseplot.streamplot`. plot_equilpoints : bool or dict, optional If True (default) then plot equilibrium points based in the phase plot boundary. If set to a dict, pass on the key-value pairs in the @@ -151,7 +159,39 @@ def phase_plane_plot( title : str, optional Set the title of the plot. Defaults to plot type and system name(s). + Notes + ----- + The default method for producing streamlines is determined based on which + keywords are specified, with `plot_streamplot` serving as the generic + default. If any of the `arrows`, `arrow_size`, `arrow_style`, or `dir` + keywords are used and neither `plot_streamlines` nor `plot_streamplot` is + set, then `plot_streamlines` will be set to True. If neither + `plot_streamlines` nor `plot_vectorfield` set set to True, then + `plot_streamplot` will be set to True. + """ + # Check for legacy usage of plot_streamlines + streamline_keywords = [ + 'arrows', 'arrow_size', 'arrow_style', 'dir'] + if plot_streamlines is None: + if any([kw in kwargs for kw in streamline_keywords]): + warnings.warn( + "detected streamline keywords; use plot_streamlines to set", + FutureWarning) + plot_streamlines = True + if gridtype not in [None, 'meshgrid']: + warnings.warn( + "streamplots only support gridtype='meshgrid'; " + "falling back to streamlines") + plot_streamlines = True + + if plot_streamlines is None and plot_vectorfield is None \ + and plot_streamplot is None: + plot_streamplot = True + + if plot_streamplot and not plot_streamlines and not plot_vectorfield: + gridspec = gridspec or [25, 25] + # Process arguments params = kwargs.get('params', None) sys = _create_system(sys, params) @@ -174,7 +214,10 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): return new_kwargs # Create list for storing outputs - out = np.array([[], None, None], dtype=object) + out = np.array([[], None, None, None], dtype=object) + + # the maximum zorder of stramlines, vectorfield or streamplot + flow_zorder = None # Plot out the main elements if plot_streamlines: @@ -185,6 +228,10 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): sys, pointdata, timedata, _check_kwargs=False, suppress_warnings=suppress_warnings, **kwargs_local) + new_zorder = max(elem.get_zorder() for elem in out[0]) + flow_zorder = max(flow_zorder, new_zorder) if flow_zorder \ + else new_zorder + # Get rid of keyword arguments handled by streamlines for kw in ['arrows', 'arrow_size', 'arrow_style', 'color', 'dir', 'params']: @@ -194,29 +241,60 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): if gridtype not in [None, 'boxgrid', 'meshgrid']: gridspec = None - if plot_separatrices: + if plot_vectorfield: kwargs_local = _create_kwargs( - kwargs, plot_separatrices, gridspec=gridspec, ax=ax) - out[0] += separatrices( + kwargs, plot_vectorfield, gridspec=gridspec, ax=ax) + out[1] = vectorfield( sys, pointdata, _check_kwargs=False, **kwargs_local) - # Get rid of keyword arguments handled by separatrices - for kw in ['arrows', 'arrow_size', 'arrow_style', 'params']: + new_zorder = out[1].get_zorder() + flow_zorder = max(flow_zorder, new_zorder) if flow_zorder \ + else new_zorder + + # Get rid of keyword arguments handled by vectorfield + for kw in ['color', 'params']: initial_kwargs.pop(kw, None) - if plot_vectorfield: + if plot_streamplot: + if gridtype not in [None, 'meshgrid']: + raise ValueError( + "gridtype must be 'meshgrid' when using streamplot") + kwargs_local = _create_kwargs( - kwargs, plot_vectorfield, gridspec=gridspec, ax=ax) - out[1] = vectorfield( + kwargs, plot_streamplot, gridspec=gridspec, ax=ax) + out[3] = streamplot( sys, pointdata, _check_kwargs=False, **kwargs_local) - # Get rid of keyword arguments handled by vectorfield + new_zorder = max(out[3].lines.get_zorder(), out[3].arrows.get_zorder()) + flow_zorder = max(flow_zorder, new_zorder) if flow_zorder \ + else new_zorder + + # Get rid of keyword arguments handled by streamplot for kw in ['color', 'params']: initial_kwargs.pop(kw, None) + sep_zorder = flow_zorder + 1 if flow_zorder else None + + if plot_separatrices: + kwargs_local = _create_kwargs( + kwargs, plot_separatrices, gridspec=gridspec, ax=ax) + kwargs_local['zorder'] = kwargs_local.get('zorder', sep_zorder) + out[0] += separatrices( + sys, pointdata, _check_kwargs=False, **kwargs_local) + + sep_zorder = max(elem.get_zorder() for elem in out[0]) if out[0] \ + else None + + # Get rid of keyword arguments handled by separatrices + for kw in ['arrows', 'arrow_size', 'arrow_style', 'params']: + initial_kwargs.pop(kw, None) + + equil_zorder = sep_zorder + 1 if sep_zorder else None + if plot_equilpoints: kwargs_local = _create_kwargs( kwargs, plot_equilpoints, gridspec=gridspec, ax=ax) + kwargs_local['zorder'] = kwargs_local.get('zorder', equil_zorder) out[2] = equilpoints( sys, pointdata, _check_kwargs=False, **kwargs_local) @@ -240,8 +318,8 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): def vectorfield( - sys, pointdata, gridspec=None, ax=None, suppress_warnings=False, - _check_kwargs=True, **kwargs): + sys, pointdata, gridspec=None, zorder=None, ax=None, + suppress_warnings=False, _check_kwargs=True, **kwargs): """Plot a vector field in the phase plane. This function plots a vector field for a two-dimensional state @@ -289,6 +367,9 @@ def vectorfield( Default is set by `config.defaults['ctrlplot.rcParams']`. suppress_warnings : bool, optional If set to True, suppress warning messages in generating trajectories. + zorder : float, optional + Set the zorder for the vectorfield. In not specified, it will be + automatically chosen by `matplotlib.axes.Axes.quiver`. """ # Process keywords @@ -327,14 +408,127 @@ def vectorfield( with plt.rc_context(rcParams): out = ax.quiver( vfdata[:, 0], vfdata[:, 1], vfdata[:, 2], vfdata[:, 3], - angles='xy', color=color) + angles='xy', color=color, zorder=zorder) + + return out + + +def streamplot( + sys, pointdata, gridspec=None, zorder=None, ax=None, vary_color=False, + vary_linewidth=False, cmap=None, norm=None, suppress_warnings=False, + _check_kwargs=True, **kwargs): + """Plot streamlines in the phase plane. + + This function plots the streamlines for a two-dimensional state + space system using the `matplotlib.axes.Axes.streamplot` function. + + Parameters + ---------- + sys : `NonlinearIOSystem` or callable(t, x, ...) + I/O system or function used to generate phase plane data. If a + function is given, the remaining arguments are drawn from the + `params` keyword. + pointdata : list or 2D array + List of the form [xmin, xmax, ymin, ymax] describing the + boundaries of the phase plot. + gridspec : list, optional + Specifies the size of the grid in the x and y axes on which to + generate points. + params : dict or list, optional + Parameters to pass to system. For an I/O system, `params` should be + a dict of parameters and values. For a callable, `params` should be + dict with key 'args' and value given by a tuple (passed to callable). + color : matplotlib color spec, optional + Plot the vector field in the given color. + ax : `matplotlib.axes.Axes`, optional + Use the given axes for the plot, otherwise use the current axes. + + Returns + ------- + out : StreamplotSet + Containter object with lines and arrows contained in the + streamplot. See `matplotlib.axes.Axes.streamplot` for details. + + Other Parameters + ---------------- + cmap : str or Colormap, optional + Colormap to use for varying the color of the streamlines. + norm : `matplotlib.colors.Normalize`, optional + Normalization map to use for scaling the colormap and linewidths. + rcParams : dict + Override the default parameters used for generating plots. + Default is set by `config.default['ctrlplot.rcParams']`. + suppress_warnings : bool, optional + If set to True, suppress warning messages in generating trajectories. + vary_color : bool, optional + If set to True, vary the color of the streamlines based on the + magnitude of the vector field. + vary_linewidth : bool, optional. + If set to True, vary the linewidth of the streamlines based on the + magnitude of the vector field. + zorder : float, optional + Set the zorder for the streamlines. In not specified, it will be + automatically chosen by `matplotlib.axes.Axes.streamplot`. + + """ + # Process keywords + rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True) + + # Get system parameters + params = kwargs.pop('params', None) + + # Create system from callable, if needed + sys = _create_system(sys, params) + + # Determine the points on which to generate the streamplot field + points, gridspec = _make_points(pointdata, gridspec, 'meshgrid') + grid_arr_shape = gridspec[::-1] + xs = points[:, 0].reshape(grid_arr_shape) + ys = points[:, 1].reshape(grid_arr_shape) + + # Create axis if needed + if ax is None: + ax = plt.gca() + + # Set the plotting limits + xlim, ylim, maxlim = _set_axis_limits(ax, pointdata) + + # Figure out the color to use + color = _get_color(kwargs, ax=ax) + + # Make sure all keyword arguments were processed + if _check_kwargs and kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + + # Generate phase plane (quiver) data + sys._update_params(params) + us_flat, vs_flat = np.transpose( + [sys._rhs(0, x, np.zeros(sys.ninputs)) for x in points]) + us, vs = us_flat.reshape(grid_arr_shape), vs_flat.reshape(grid_arr_shape) + + magnitudes = np.linalg.norm([us, vs], axis=0) + norm = norm or mpl.colors.Normalize() + normalized = norm(magnitudes) + cmap = plt.get_cmap(cmap) + + with plt.rc_context(rcParams): + default_lw = plt.rcParams['lines.linewidth'] + min_lw, max_lw = 0.25*default_lw, 2*default_lw + linewidths = normalized * (max_lw - min_lw) + min_lw \ + if vary_linewidth else None + color = magnitudes if vary_color else color + + out = ax.streamplot( + xs, ys, us, vs, color=color, linewidth=linewidths, cmap=cmap, + norm=norm, zorder=zorder) return out def streamlines( sys, pointdata, timedata=1, gridspec=None, gridtype=None, dir=None, - ax=None, _check_kwargs=True, suppress_warnings=False, **kwargs): + zorder=None, ax=None, _check_kwargs=True, suppress_warnings=False, + **kwargs): """Plot stream lines in the phase plane. This function plots stream lines for a two-dimensional state space @@ -399,6 +593,9 @@ def streamlines( Default is set by `config.defaults['ctrlplot.rcParams']`. suppress_warnings : bool, optional If set to True, suppress warning messages in generating trajectories. + zorder : float, optional + Set the zorder for the streamlines. In not specified, it will be + automatically chosen by `matplotlib.axes.Axes.plot`. """ # Process keywords @@ -454,7 +651,7 @@ def streamlines( # Plot the trajectory (if there is one) if traj.shape[1] > 1: with plt.rc_context(rcParams): - out += ax.plot(traj[0], traj[1], color=color) + out += ax.plot(traj[0], traj[1], color=color, zorder=zorder) # Add arrows to the lines at specified intervals _add_arrows_to_line2D( @@ -463,7 +660,7 @@ def streamlines( def equilpoints( - sys, pointdata, gridspec=None, color='k', ax=None, + sys, pointdata, gridspec=None, color='k', zorder=None, ax=None, _check_kwargs=True, **kwargs): """Plot equilibrium points in the phase plane. @@ -509,6 +706,9 @@ def equilpoints( rcParams : dict Override the default parameters used for generating plots. Default is set by `config.defaults['ctrlplot.rcParams']`. + zorder : float, optional + Set the zorder for the equilibrium points. In not specified, it will + be automatically chosen by `matplotlib.axes.Axes.plot`. """ # Process keywords @@ -542,12 +742,13 @@ def equilpoints( out = [] for xeq in equilpts: with plt.rc_context(rcParams): - out += ax.plot(xeq[0], xeq[1], marker='o', color=color) + out += ax.plot( + xeq[0], xeq[1], marker='o', color=color, zorder=zorder) return out def separatrices( - sys, pointdata, timedata=None, gridspec=None, ax=None, + sys, pointdata, timedata=None, gridspec=None, zorder=None, ax=None, _check_kwargs=True, suppress_warnings=False, **kwargs): """Plot separatrices in the phase plane. @@ -603,6 +804,9 @@ def separatrices( Default is set by `config.defaults['ctrlplot.rcParams']`. suppress_warnings : bool, optional If set to True, suppress warning messages in generating trajectories. + zorder : float, optional + Set the zorder for the separatrices. In not specified, it will be + automatically chosen by `matplotlib.axes.Axes.plot`. Notes ----- @@ -663,10 +867,6 @@ def separatrices( # Plot separatrices by flowing backwards in time along eigenspaces out = [] for i, xeq in enumerate(equilpts): - # Plot the equilibrium points - with plt.rc_context(rcParams): - out += ax.plot(xeq[0], xeq[1], marker='o', color='k') - # Figure out the linearization and eigenvectors evals, evecs = np.linalg.eig(sys.linearize(xeq, 0, params=params).A) @@ -707,7 +907,8 @@ def separatrices( if traj.shape[1] > 1: with plt.rc_context(rcParams): out += ax.plot( - traj[0], traj[1], color=color, linestyle=linestyle) + traj[0], traj[1], color=color, + linestyle=linestyle, zorder=zorder) # Add arrows to the lines at specified intervals with plt.rc_context(rcParams): @@ -807,6 +1008,7 @@ def circlegrid(centers, radius, num): theta in np.linspace(0, 2 * math.pi, num, endpoint=False)]) return grid + # # Internal utility functions # @@ -827,6 +1029,7 @@ def _create_system(sys, params): return NonlinearIOSystem( _update, _output, states=2, inputs=0, outputs=0, name="_callable") + # Set axis limits for the plot def _set_axis_limits(ax, pointdata): # Get the current axis limits diff --git a/control/pzmap.py b/control/pzmap.py index 42ba8e087..6bf928f56 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -124,6 +124,17 @@ def plot(self, *args, **kwargs): """ return pole_zero_plot(self, *args, **kwargs) + def replot(self, cplt: ControlPlot): + """Update the pole/zero loci of an existing plot. + + Parameters + ---------- + cplt : `ControlPlot` + Graphics handles of the existing plot. + """ + pole_zero_replot(self, cplt) + + # Pole/zero map def pole_zero_map(sysdata): @@ -513,6 +524,35 @@ def _click_dispatcher(event): return ControlPlot(out, ax, fig, legend=legend) +def pole_zero_replot(pzmap_responses, cplt): + """Update the loci of a plot after zooming/panning. + + Parameters + ---------- + pzmap_responses : PoleZeroMap list + Responses to update. + cplt : ControlPlot + Collection of plot handles. + """ + + for idx, response in enumerate(pzmap_responses): + + # remove the old data + for l in cplt.lines[idx, 2]: + l.set_data([], []) + + # update the line data + if response.loci is not None: + + for il, locus in enumerate(response.loci.transpose()): + try: + cplt.lines[idx,2][il].set_data(real(locus), imag(locus)) + except IndexError: + # not expected, but more lines apparently needed + cplt.lines[idx,2].append(cplt.ax[0,0].plot( + real(locus), imag(locus))) + + # Utility function to find gain corresponding to a click event def _find_root_locus_gain(event, sys, ax): # Get the current axis limits to set various thresholds diff --git a/control/rlocus.py b/control/rlocus.py index c4ef8b40e..b7344e31d 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -33,7 +33,7 @@ # Root locus map -def root_locus_map(sysdata, gains=None): +def root_locus_map(sysdata, gains=None, xlim=None, ylim=None): """Compute the root locus map for an LTI system. Calculate the root locus by finding the roots of 1 + k * G(s) where G @@ -46,6 +46,10 @@ def root_locus_map(sysdata, gains=None): gains : array_like, optional Gains to use in computing plot of closed-loop poles. If not given, gains are chosen to include the main features of the root locus map. + xlim : tuple or list, optional + Set limits of x axis (see `matplotlib.axes.Axes.set_xlim`). + ylim : tuple or list, optional + Set limits of y axis (see `matplotlib.axes.Axes.set_ylim`). Returns ------- @@ -75,7 +79,7 @@ def root_locus_map(sysdata, gains=None): nump, denp = _systopoly1d(sys[0, 0]) if gains is None: - kvect, root_array, _, _ = _default_gains(nump, denp, None, None) + kvect, root_array, _, _ = _default_gains(nump, denp, xlim, ylim) else: kvect = np.atleast_1d(gains) root_array = _RLFindRoots(nump, denp, kvect) @@ -205,6 +209,11 @@ def root_locus_plot( # Plot the root loci cplt = responses.plot(grid=grid, **kwargs) + # Add a reaction to axis scale changes, if given LTI systems, and + # there is no set of pre-defined gains + if gains is None: + add_loci_recalculate(sysdata, cplt, cplt.axes[0,0]) + # Legacy processing: return locations of poles and zeros as a tuple if plot is True: return responses.loci, responses.gains @@ -212,6 +221,40 @@ def root_locus_plot( return ControlPlot(cplt.lines, cplt.axes, cplt.figure) +def add_loci_recalculate(sysdata, cplt, axis): + """Add a callback to re-calculate the loci data fitting a zoom action. + + Parameters + ---------- + sysdata: LTI object or list + Linear input/output systems (SISO only, for now). + cplt: ControlPlot + Collection of plot handles. + axis: matplotlib.axes.Axis + Axis on which callbacks are installed. + """ + + # if LTI, treat everything as a list of lti + if isinstance(sysdata, LTI): + sysdata = [sysdata] + + # check that we can actually recalculate the loci + if isinstance(sysdata, list) and all( + [isinstance(sys, LTI) for sys in sysdata]): + + # callback function for axis change (zoom, pan) events + # captures the sysdata object and cplt + def _zoom_adapter(_ax): + newresp = root_locus_map(sysdata, None, + _ax.get_xlim(), + _ax.get_ylim()) + newresp.replot(cplt) + + # connect the callback to axis changes + axis.callbacks.connect('xlim_changed', _zoom_adapter) + axis.callbacks.connect('ylim_changed', _zoom_adapter) + + def _default_gains(num, den, xlim, ylim): """Unsupervised gains calculation for root locus plot. @@ -288,7 +331,7 @@ def _default_gains(num, den, xlim, ylim): # Root locus is on imaginary axis (rare), use just y distance tolerance = y_tolerance elif y_tolerance == 0: - # Root locus is on imaginary axis (common), use just x distance + # Root locus is on real axis (common), use just x distance tolerance = x_tolerance else: tolerance = np.min([x_tolerance, y_tolerance]) diff --git a/control/sisotool.py b/control/sisotool.py index 78be86b16..2d4506f52 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -22,6 +22,7 @@ from .statesp import ss, summing_junction from .timeresp import step_response from .xferfcn import tf +from .rlocus import add_loci_recalculate _sisotool_defaults = { 'sisotool.initial_gain': 1 @@ -105,7 +106,7 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, fig = plt.gcf() if fig.canvas.manager.get_window_title() != 'Sisotool': plt.close(fig) - fig,axes = plt.subplots(2, 2) + fig, axes = plt.subplots(2, 2) fig.canvas.manager.set_window_title('Sisotool') else: axes = np.array(fig.get_axes()).reshape(2, 2) @@ -137,8 +138,8 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, # sys[0, 0], initial_gain=initial_gain, xlim=xlim_rlocus, # ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid, # ax=fig.axes[1]) - ax_rlocus = fig.axes[1] - root_locus_map(sys[0, 0]).plot( + ax_rlocus = axes[0,1] # fig.axes[1] + cplt = root_locus_map(sys[0, 0]).plot( xlim=xlim_rlocus, ylim=ylim_rlocus, initial_gain=initial_gain, ax=ax_rlocus) if rlocus_grid is False: @@ -146,6 +147,9 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, from .grid import nogrid nogrid(sys.dt, ax=ax_rlocus) + # install a zoom callback on the root-locus axis + add_loci_recalculate(sys, cplt, ax_rlocus) + # Reset the button release callback so that we can update all plots fig.canvas.mpl_connect( 'button_release_event', partial( @@ -155,9 +159,8 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, def _click_dispatcher(event, sys, ax, bode_plot_params, tvect): # Zoom handled by specialized callback in rlocus, only handle gain plot - if event.inaxes == ax.axes and \ - plt.get_current_fig_manager().toolbar.mode not in \ - {'zoom rect', 'pan/zoom'}: + if event.inaxes == ax.axes: + fig = ax.figure # if a point is clicked on the rootlocus plot visually emphasize it diff --git a/control/statefbk.py b/control/statefbk.py index b6e9c9655..414673fcf 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -251,7 +251,7 @@ def place_acker(A, B, poles): pmat = pmat + p[n-i-1] * np.linalg.matrix_power(A, i) K = np.linalg.solve(ct, pmat) - K = K[-1, :] # Extract the last row + K = K[-1:, :] # Extract the last row return K diff --git a/control/tests/bdalg_test.py b/control/tests/bdalg_test.py index 50ae9e8a9..cec10f904 100644 --- a/control/tests/bdalg_test.py +++ b/control/tests/bdalg_test.py @@ -347,19 +347,19 @@ def test_bdalg_udpate_names_errors(): sys2 = ctrl.rss(2, 1, 1) with pytest.raises(ValueError, match="number of inputs does not match"): - sys = ctrl.series(sys1, sys2, inputs=2) + ctrl.series(sys1, sys2, inputs=2) with pytest.raises(ValueError, match="number of outputs does not match"): - sys = ctrl.series(sys1, sys2, outputs=2) + ctrl.series(sys1, sys2, outputs=2) with pytest.raises(ValueError, match="number of states does not match"): - sys = ctrl.series(sys1, sys2, states=2) + ctrl.series(sys1, sys2, states=2) with pytest.raises(ValueError, match="number of states does not match"): - sys = ctrl.series(ctrl.tf(sys1), ctrl.tf(sys2), states=2) + ctrl.series(ctrl.tf(sys1), ctrl.tf(sys2), states=2) with pytest.raises(TypeError, match="unrecognized keywords"): - sys = ctrl.series(sys1, sys2, dt=1) + ctrl.series(sys1, sys2, dt=1) class TestEnsureTf: diff --git a/control/tests/bspline_test.py b/control/tests/bspline_test.py index 0ac59094d..e15915182 100644 --- a/control/tests/bspline_test.py +++ b/control/tests/bspline_test.py @@ -11,11 +11,9 @@ import numpy as np import pytest -import scipy as sp import control as ct import control.flatsys as fs -import control.optimal as opt def test_bspline_basis(): Tf = 10 @@ -182,40 +180,40 @@ def test_kinematic_car_multivar(): def test_bspline_errors(): # Breakpoints must be a 1D array, in increasing order with pytest.raises(NotImplementedError, match="not yet supported"): - basis = fs.BSplineFamily([[0, 1, 3], [0, 2, 3]], [3, 3]) + fs.BSplineFamily([[0, 1, 3], [0, 2, 3]], [3, 3]) with pytest.raises(ValueError, match="breakpoints must be convertable to a 1D array"): - basis = fs.BSplineFamily([[[0, 1], [0, 1]], [[0, 1], [0, 1]]], [3, 3]) + fs.BSplineFamily([[[0, 1], [0, 1]], [[0, 1], [0, 1]]], [3, 3]) with pytest.raises(ValueError, match="must have at least 2 values"): - basis = fs.BSplineFamily([10], 2) + fs.BSplineFamily([10], 2) with pytest.raises(ValueError, match="must be strictly increasing"): - basis = fs.BSplineFamily([1, 3, 2], 2) + fs.BSplineFamily([1, 3, 2], 2) # Smoothness can't be more than dimension of splines - basis = fs.BSplineFamily([0, 1], 4, 3) # OK + fs.BSplineFamily([0, 1], 4, 3) # OK with pytest.raises(ValueError, match="degree must be greater"): - basis = fs.BSplineFamily([0, 1], 4, 4) # not OK + fs.BSplineFamily([0, 1], 4, 4) # not OK # nvars must be an integer with pytest.raises(TypeError, match="vars must be an integer"): - basis = fs.BSplineFamily([0, 1], 4, 3, vars=['x1', 'x2']) + fs.BSplineFamily([0, 1], 4, 3, vars=['x1', 'x2']) # degree, smoothness must match nvars with pytest.raises(ValueError, match="length of 'degree' does not match"): - basis = fs.BSplineFamily([0, 1], [4, 4, 4], 3, vars=2) + fs.BSplineFamily([0, 1], [4, 4, 4], 3, vars=2) # degree, smoothness must be list of ints - basis = fs.BSplineFamily([0, 1], [4, 4], 3, vars=2) # OK + fs.BSplineFamily([0, 1], [4, 4], 3, vars=2) # OK with pytest.raises(ValueError, match="could not parse 'degree'"): - basis = fs.BSplineFamily([0, 1], [4, '4'], 3, vars=2) + fs.BSplineFamily([0, 1], [4, '4'], 3, vars=2) # degree must be strictly positive with pytest.raises(ValueError, match="'degree'; must be at least 1"): - basis = fs.BSplineFamily([0, 1], 0, 1) + fs.BSplineFamily([0, 1], 0, 1) # smoothness must be non-negative with pytest.raises(ValueError, match="'smoothness'; must be at least 0"): - basis = fs.BSplineFamily([0, 1], 2, -1) + fs.BSplineFamily([0, 1], 2, -1) diff --git a/control/tests/canonical_test.py b/control/tests/canonical_test.py index ecdaa04cb..63afd51c3 100644 --- a/control/tests/canonical_test.py +++ b/control/tests/canonical_test.py @@ -4,8 +4,6 @@ import pytest import scipy.linalg -from control.tests.conftest import slycotonly - from control import ss, tf, tf2ss from control.canonical import canonical_form, reachable_form, \ observable_form, modal_form, similarity_transform, bdschur @@ -244,7 +242,7 @@ def block_diag_from_eig(eigvals): return scipy.linalg.block_diag(*blocks) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "eigvals, condmax, blksizes", [ @@ -269,7 +267,7 @@ def test_bdschur_ref(eigvals, condmax, blksizes): np.testing.assert_array_almost_equal(solve(t, a) @ t, b) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "eigvals, sorted_blk_eigvals, sort", [ @@ -300,7 +298,7 @@ def test_bdschur_sort(eigvals, sorted_blk_eigvals, sort): blk_eigval.imag) -@slycotonly +@pytest.mark.slycot def test_bdschur_defective(): # the eigenvalues of this simple defective matrix cannot be separated # a previous version of the bdschur would fail on this @@ -323,14 +321,14 @@ def test_bdschur_condmax_lt_1(): bdschur(1, condmax=np.nextafter(1, 0)) -@slycotonly +@pytest.mark.slycot def test_bdschur_invalid_sort(): # sort must be in ('continuous', 'discrete') with pytest.raises(ValueError): bdschur(1, sort='no-such-sort') -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "A_true, B_true, C_true, D_true", [(np.diag([4.0, 3.0, 2.0, 1.0]), # order from largest to smallest @@ -390,7 +388,7 @@ def test_modal_form(A_true, B_true, C_true, D_true): C @ np.linalg.matrix_power(A, i) @ B) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "condmax, len_blksizes", [(1.1, 1), @@ -409,7 +407,7 @@ def test_modal_form_condmax(condmax, len_blksizes): np.testing.assert_array_almost_equal(zsys.D, xsys.D) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "sys_type", ['continuous', diff --git a/control/tests/config_test.py b/control/tests/config_test.py index 646a20a16..be3fba5c9 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -339,11 +339,8 @@ def test_system_indexing(self): {'dt': 0.1} ]) def test_repr_format(self, kwargs): - from ..statesp import StateSpace - from numpy import array - sys = ct.ss([[1]], [[1]], [[1]], [[0]], **kwargs) - new = eval(repr(sys)) + new = eval(repr(sys), None, {'StateSpace':ct.StateSpace, 'array':np.array}) for attr in ['A', 'B', 'C', 'D']: assert getattr(new, attr) == getattr(sys, attr) for prop in ['input_labels', 'output_labels', 'state_labels']: diff --git a/control/tests/conftest.py b/control/tests/conftest.py index bf3920a02..d055690d1 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -1,21 +1,31 @@ """conftest.py - pytest local plugins, fixtures, marks and functions.""" -import os -from contextlib import contextmanager - import matplotlib as mpl import numpy as np import pytest import control +def pytest_runtest_setup(item): + if not control.exception.slycot_check(): + if any(mark.name == 'slycot' + for mark in item.iter_markers()): + pytest.skip("slycot not installed") + elif any(mark.name == 'noslycot' + for mark in item.iter_markers()): + # used, e.g., for tests checking ControlSlycot + pytest.skip("slycot installed") + + if (not control.exception.cvxopt_check() + and any(mark.name == 'cvxopt' + for mark in item.iter_markers())): + pytest.skip("cvxopt not installed") + + if (not control.exception.pandas_check() + and any(mark.name == 'pandas' + for mark in item.iter_markers())): + pytest.skip("pandas not installed") -# some common pytest marks. These can be used as test decorators or in -# pytest.param(marks=) -slycotonly = pytest.mark.skipif( - not control.exception.slycot_check(), reason="slycot not installed") -cvxoptonly = pytest.mark.skipif( - not control.exception.cvxopt_check(), reason="cvxopt not installed") @pytest.fixture(scope="session", autouse=True) diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index 7975bbe5a..9cdabbe6c 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -21,17 +21,13 @@ from control import rss, ss, ss2tf, tf, tf2ss from control.statefbk import ctrb, obsv from control.freqplot import bode -from control.exception import slycot_check, ControlMIMONotImplemented -from control.tests.conftest import slycotonly +from control.exception import ControlMIMONotImplemented # Set to True to print systems to the output. verbose = False # Maximum number of states to test + 1 maxStates = 4 -# Maximum number of inputs and outputs to test + 1 -# If slycot is not installed, just check SISO -maxIO = 5 if slycot_check() else 2 @pytest.fixture @@ -50,8 +46,13 @@ def printSys(self, sys, ind): @pytest.mark.usefixtures("legacy_plot_signature") @pytest.mark.parametrize("states", range(1, maxStates)) - @pytest.mark.parametrize("inputs", range(1, maxIO)) - @pytest.mark.parametrize("outputs", range(1, maxIO)) + # If slycot is not installed, just check SISO + @pytest.mark.parametrize("inputs", + [1] + [pytest.param(i, marks=pytest.mark.slycot) + for i in range(2, 5)]) + @pytest.mark.parametrize("outputs", + [1] + [pytest.param(i, marks=pytest.mark.slycot) + for i in range(2, 5)]) def testConvert(self, fixedseed, states, inputs, outputs): """Test state space to transfer function conversion. @@ -148,7 +149,11 @@ def testConvert(self, fixedseed, states, inputs, outputs): np.testing.assert_array_almost_equal( ssorig_imag, tfxfrm_imag, decimal=5) - def testConvertMIMO(self): + + @pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) + def testConvertMIMO(self, have_slycot): """Test state space to transfer function conversion. Do a MIMO conversion and make sure that it is processed @@ -166,7 +171,7 @@ def testConvertMIMO(self): [0.008, 1.39, 48.78]]]) # Convert to state space and look for an error - if (not slycot_check()): + if not have_slycot: with pytest.raises(ControlMIMONotImplemented): tf2ss(tsys) else: @@ -214,7 +219,7 @@ def testSs2tfStaticMimo(self): np.testing.assert_allclose(numref, np.array(gtf.num) / np.array(gtf.den)) - @slycotonly + @pytest.mark.slycot def testTf2SsDuplicatePoles(self): """Tests for 'too few poles for MIMO tf gh-111'""" num = [[[1], [0]], @@ -225,7 +230,7 @@ def testTf2SsDuplicatePoles(self): s = ss(g) np.testing.assert_allclose(g.poles(), s.poles()) - @slycotonly + @pytest.mark.slycot def test_tf2ss_robustness(self): """Unit test to make sure that tf2ss is working correctly. gh-240""" num = [ [[0], [1]], [[1], [0]] ] diff --git a/control/tests/ctrlplot_test.py b/control/tests/ctrlplot_test.py index b7192c844..bf8a075ae 100644 --- a/control/tests/ctrlplot_test.py +++ b/control/tests/ctrlplot_test.py @@ -74,7 +74,6 @@ def setup_plot_arguments(resp_fcn, plot_fcn, compute_time_response=True): case ct.gangof4_response, _: args1 = (sys1, sys1c) args2 = (sys2, sys1c) - default_labels = ["P=sys[1]", "P=sys[2]"] case ct.frequency_response, ct.nichols_plot: args1 = (sys1, None) # to allow *fmt in linestyle test @@ -116,6 +115,11 @@ def setup_plot_arguments(resp_fcn, plot_fcn, compute_time_response=True): args2 = (sys2, ) argsc = ([sys1, sys2], ) + case (None, ct.phase_plane_plot): + args1 = (sys1, ) + args2 = (sys2, ) + plot_kwargs = {'plot_streamlines': True} + case _, _: args1 = (sys1, ) args2 = (sys2, ) @@ -234,10 +238,19 @@ def test_plot_ax_processing(resp_fcn, plot_fcn): # Call the plotting function, passing the axes if resp_fcn is not None: resp = resp_fcn(*args, **kwargs, **resp_kwargs) - cplt4 = resp.plot(**kwargs, **meth_kwargs, ax=ax) + resp.plot(**kwargs, **meth_kwargs, ax=ax) else: # No response function available; just plot the data - cplt4 = plot_fcn(*args, **kwargs, **plot_kwargs, ax=ax) + plot_fcn(*args, **kwargs, **plot_kwargs, ax=ax) + + # Make sure the plot ended up in the right place + assert len(axs[0, 0].get_lines()) == 0 # upper left + assert len(axs[0, 1].get_lines()) != 0 # top middle + assert len(axs[1, 0].get_lines()) == 0 # lower left + if resp_fcn != ct.gangof4_response: + assert len(axs[1, 2].get_lines()) == 0 # lower right (normally empty) + else: + assert len(axs[1, 2].get_lines()) != 0 # gangof4 uses this axes # Check to make sure original settings did not change assert fig._suptitle.get_text() == title @@ -326,19 +339,9 @@ def test_plot_label_processing(resp_fcn, plot_fcn): @pytest.mark.parametrize("resp_fcn, plot_fcn", resp_plot_fcns) @pytest.mark.usefixtures('mplcleanup') def test_plot_linestyle_processing(resp_fcn, plot_fcn): - # Create some systems to use - sys1 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[1]") - sys1c = ct.rss(4, 1, 1, strictly_proper=True, name="sys[1]_C") - sys2 = ct.rss(4, 1, 1, strictly_proper=True, name="sys[2]") - # Set up arguments args1, args2, _, kwargs, meth_kwargs, plot_kwargs, resp_kwargs = \ setup_plot_arguments(resp_fcn, plot_fcn) - default_labels = ["sys[1]", "sys[2]"] - expected_labels = ["sys1_", "sys2_"] - match resp_fcn, plot_fcn: - case ct.gangof4_response, _: - default_labels = ["P=sys[1]", "P=sys[2]"] # Set line color cplt1 = plot_fcn(*args1, **kwargs, **plot_kwargs, color='r') @@ -486,16 +489,10 @@ def test_mimo_plot_legend_processing(resp_fcn, plot_fcn): @pytest.mark.parametrize("resp_fcn, plot_fcn", resp_plot_fcns) @pytest.mark.usefixtures('mplcleanup') def test_plot_title_processing(resp_fcn, plot_fcn): - # Create some systems to use - sys1 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[1]") - sys1c = ct.rss(4, 1, 1, strictly_proper=True, name="sys[1]_C") - sys2 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[2]") - # Set up arguments args1, args2, argsc, kwargs, meth_kwargs, plot_kwargs, resp_kwargs = \ setup_plot_arguments(resp_fcn, plot_fcn) default_title = "sys[1], sys[2]" - expected_title = "sys1_, sys2_" match resp_fcn, plot_fcn: case ct.gangof4_response, _: default_title = "P=sys[1], C=sys[1]_C, P=sys[2], C=sys[1]_C" @@ -531,7 +528,7 @@ def test_plot_title_processing(resp_fcn, plot_fcn): case ct.input_output_response, _: title_prefix = "Input/output response for " case _: - raise RuntimeError(f"didn't recognize {resp_fnc}, {plot_fnc}") + raise RuntimeError(f"didn't recognize {resp_fcn}, {plot_fcn}") # Generate the first plot, with default title cplt1 = plot_fcn(*args1, **kwargs, **plot_kwargs) @@ -582,11 +579,9 @@ def test_plot_title_processing(resp_fcn, plot_fcn): @pytest.mark.usefixtures('mplcleanup') def test_tickmark_label_processing(plot_fcn): # Generate the response that we will use for plotting - top_row, bot_row = 0, -1 match plot_fcn: case ct.bode_plot: resp = ct.frequency_response(ct.rss(4, 2, 2)) - top_row = 1 case ct.time_response_plot: resp = ct.step_response(ct.rss(4, 2, 2)) case ct.gangof4_plot: @@ -620,20 +615,9 @@ def test_tickmark_label_processing(plot_fcn): @pytest.mark.parametrize("resp_fcn, plot_fcn", resp_plot_fcns) @pytest.mark.usefixtures('mplcleanup', 'editsdefaults') def test_rcParams(resp_fcn, plot_fcn): - # Create some systems to use - sys1 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[1]") - sys1c = ct.rss(4, 1, 1, strictly_proper=True, name="sys[1]_C") - sys2 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[2]") - # Set up arguments args1, args2, argsc, kwargs, meth_kwargs, plot_kwargs, resp_kwargs = \ setup_plot_arguments(resp_fcn, plot_fcn) - default_title = "sys[1], sys[2]" - expected_title = "sys1_, sys2_" - match resp_fcn, plot_fcn: - case ct.gangof4_response, _: - default_title = "P=sys[1], C=sys[1]_C, P=sys[2], C=sys[1]_C" - # Create new set of rcParams my_rcParams = {} for key in ct.ctrlplot.rcParams: diff --git a/control/tests/descfcn_test.py b/control/tests/descfcn_test.py index a5f7a06c2..4fbb42913 100644 --- a/control/tests/descfcn_test.py +++ b/control/tests/descfcn_test.py @@ -190,11 +190,11 @@ def test_describing_function_plot(): cplt = response.plot() assert len(plt.gcf().get_axes()) == 1 # make sure there is a plot - assert len(cplt.lines[0]) == 4 and len(cplt.lines[1]) == 1 + assert len(cplt.lines[0]) == 5 and len(cplt.lines[1]) == 1 # Call plot directly cplt = ct.describing_function_plot(H_larger, F_saturation, amp, omega) - assert len(cplt.lines[0]) == 4 and len(cplt.lines[1]) == 1 + assert len(cplt.lines[0]) == 5 and len(cplt.lines[1]) == 1 def test_describing_function_exceptions(): @@ -205,12 +205,12 @@ def test_describing_function_exceptions(): assert saturation(3) == 2 # Turn off the bias check - bias = ct.describing_function(saturation, 0, zero_check=False) + ct.describing_function(saturation, 0, zero_check=False) # Function should evaluate to zero at zero amplitude f = lambda x: x + 0.5 with pytest.raises(ValueError, match="must evaluate to zero"): - bias = ct.describing_function(f, 0, zero_check=True) + ct.describing_function(f, 0, zero_check=True) # Evaluate at a negative amplitude with pytest.raises(ValueError, match="cannot evaluate"): @@ -236,4 +236,4 @@ def test_describing_function_exceptions(): # Describing function plot for non-describing function object resp = ct.frequency_response(H_simple) with pytest.raises(TypeError, match="data must be DescribingFunction"): - cplt = ct.describing_function_plot(resp) + ct.describing_function_plot(resp) diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 9dbc3eb00..7296c0f31 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -26,9 +26,11 @@ class Tsys: sys = rss(3, 1, 1) T.siso_ss1 = StateSpace(sys.A, sys.B, sys.C, sys.D, None) T.siso_ss1c = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.0) - T.siso_ss1d = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.1) - T.siso_ss2d = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.2) - T.siso_ss3d = StateSpace(sys.A, sys.B, sys.C, sys.D, True) + + dsys = ct.sample_system(sys, 1) + T.siso_ss1d = StateSpace(dsys.A, dsys.B, dsys.C, dsys.D, 0.1) + T.siso_ss2d = StateSpace(dsys.A, dsys.B, dsys.C, dsys.D, 0.2) + T.siso_ss3d = StateSpace(dsys.A, dsys.B, dsys.C, dsys.D, True) # Two input, two output continuous-time system A = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] @@ -39,17 +41,18 @@ class Tsys: T.mimo_ss1c = StateSpace(A, B, C, D, 0) # Two input, two output discrete-time system - T.mimo_ss1d = StateSpace(A, B, C, D, 0.1) + T.mimo_ss1d = ct.sample_system(T.mimo_ss1c, 0.1) # Same system, but with a different sampling time - T.mimo_ss2d = StateSpace(A, B, C, D, 0.2) + T.mimo_ss2d = StateSpace( + T.mimo_ss1d.A, T.mimo_ss1d.B, T.mimo_ss1d.C, T.mimo_ss1d.D, 0.2) # Single input, single output continuus and discrete transfer function T.siso_tf1 = TransferFunction([1, 1], [1, 2, 1], None) - T.siso_tf1c = TransferFunction([1, 1], [1, 2, 1], 0) - T.siso_tf1d = TransferFunction([1, 1], [1, 2, 1], 0.1) - T.siso_tf2d = TransferFunction([1, 1], [1, 2, 1], 0.2) - T.siso_tf3d = TransferFunction([1, 1], [1, 2, 1], True) + T.siso_tf1c = TransferFunction([1, 1], [1, 0.2, 1], 0) + T.siso_tf1d = TransferFunction([1, 1], [1, 0.2, 0.1], 0.1) + T.siso_tf2d = TransferFunction([1, 1], [1, 0.2, 0.1], 0.2) + T.siso_tf3d = TransferFunction([1, 1], [1, 0.2, 0.1], True) return T @@ -231,14 +234,14 @@ def testisctime(self, tsys): def testAddition(self, tsys): # State space addition - sys = tsys.siso_ss1 + tsys.siso_ss1d - sys = tsys.siso_ss1 + tsys.siso_ss1c - sys = tsys.siso_ss1c + tsys.siso_ss1 - sys = tsys.siso_ss1d + tsys.siso_ss1 - sys = tsys.siso_ss1c + tsys.siso_ss1c - sys = tsys.siso_ss1d + tsys.siso_ss1d - sys = tsys.siso_ss3d + tsys.siso_ss3d - sys = tsys.siso_ss1d + tsys.siso_ss3d + _sys = tsys.siso_ss1 + tsys.siso_ss1d + _sys = tsys.siso_ss1 + tsys.siso_ss1c + _sys = tsys.siso_ss1c + tsys.siso_ss1 + _sys = tsys.siso_ss1d + tsys.siso_ss1 + _sys = tsys.siso_ss1c + tsys.siso_ss1c + _sys = tsys.siso_ss1d + tsys.siso_ss1d + _sys = tsys.siso_ss3d + tsys.siso_ss3d + _sys = tsys.siso_ss1d + tsys.siso_ss3d with pytest.raises(ValueError): StateSpace.__add__(tsys.mimo_ss1c, tsys.mimo_ss1d) @@ -246,14 +249,14 @@ def testAddition(self, tsys): StateSpace.__add__(tsys.mimo_ss1d, tsys.mimo_ss2d) # Transfer function addition - sys = tsys.siso_tf1 + tsys.siso_tf1d - sys = tsys.siso_tf1 + tsys.siso_tf1c - sys = tsys.siso_tf1c + tsys.siso_tf1 - sys = tsys.siso_tf1d + tsys.siso_tf1 - sys = tsys.siso_tf1c + tsys.siso_tf1c - sys = tsys.siso_tf1d + tsys.siso_tf1d - sys = tsys.siso_tf2d + tsys.siso_tf2d - sys = tsys.siso_tf1d + tsys.siso_tf3d + _sys = tsys.siso_tf1 + tsys.siso_tf1d + _sys = tsys.siso_tf1 + tsys.siso_tf1c + _sys = tsys.siso_tf1c + tsys.siso_tf1 + _sys = tsys.siso_tf1d + tsys.siso_tf1 + _sys = tsys.siso_tf1c + tsys.siso_tf1c + _sys = tsys.siso_tf1d + tsys.siso_tf1d + _sys = tsys.siso_tf2d + tsys.siso_tf2d + _sys = tsys.siso_tf1d + tsys.siso_tf3d with pytest.raises(ValueError): TransferFunction.__add__(tsys.siso_tf1c, tsys.siso_tf1d) @@ -261,22 +264,22 @@ def testAddition(self, tsys): TransferFunction.__add__(tsys.siso_tf1d, tsys.siso_tf2d) # State space + transfer function - sys = tsys.siso_ss1c + tsys.siso_tf1c - sys = tsys.siso_tf1c + tsys.siso_ss1c - sys = tsys.siso_ss1d + tsys.siso_tf1d - sys = tsys.siso_tf1d + tsys.siso_ss1d + _sys = tsys.siso_ss1c + tsys.siso_tf1c + _sys = tsys.siso_tf1c + tsys.siso_ss1c + _sys = tsys.siso_ss1d + tsys.siso_tf1d + _sys = tsys.siso_tf1d + tsys.siso_ss1d with pytest.raises(ValueError): TransferFunction.__add__(tsys.siso_tf1c, tsys.siso_ss1d) def testMultiplication(self, tsys): # State space multiplication - sys = tsys.siso_ss1 * tsys.siso_ss1d - sys = tsys.siso_ss1 * tsys.siso_ss1c - sys = tsys.siso_ss1c * tsys.siso_ss1 - sys = tsys.siso_ss1d * tsys.siso_ss1 - sys = tsys.siso_ss1c * tsys.siso_ss1c - sys = tsys.siso_ss1d * tsys.siso_ss1d - sys = tsys.siso_ss1d * tsys.siso_ss3d + _sys = tsys.siso_ss1 * tsys.siso_ss1d + _sys = tsys.siso_ss1 * tsys.siso_ss1c + _sys = tsys.siso_ss1c * tsys.siso_ss1 + _sys = tsys.siso_ss1d * tsys.siso_ss1 + _sys = tsys.siso_ss1c * tsys.siso_ss1c + _sys = tsys.siso_ss1d * tsys.siso_ss1d + _sys = tsys.siso_ss1d * tsys.siso_ss3d with pytest.raises(ValueError): StateSpace.__mul__(tsys.mimo_ss1c, tsys.mimo_ss1d) @@ -284,13 +287,13 @@ def testMultiplication(self, tsys): StateSpace.__mul__(tsys.mimo_ss1d, tsys.mimo_ss2d) # Transfer function multiplication - sys = tsys.siso_tf1 * tsys.siso_tf1d - sys = tsys.siso_tf1 * tsys.siso_tf1c - sys = tsys.siso_tf1c * tsys.siso_tf1 - sys = tsys.siso_tf1d * tsys.siso_tf1 - sys = tsys.siso_tf1c * tsys.siso_tf1c - sys = tsys.siso_tf1d * tsys.siso_tf1d - sys = tsys.siso_tf1d * tsys.siso_tf3d + _sys = tsys.siso_tf1 * tsys.siso_tf1d + _sys = tsys.siso_tf1 * tsys.siso_tf1c + _sys = tsys.siso_tf1c * tsys.siso_tf1 + _sys = tsys.siso_tf1d * tsys.siso_tf1 + _sys = tsys.siso_tf1c * tsys.siso_tf1c + _sys = tsys.siso_tf1d * tsys.siso_tf1d + _sys = tsys.siso_tf1d * tsys.siso_tf3d with pytest.raises(ValueError): TransferFunction.__mul__(tsys.siso_tf1c, tsys.siso_tf1d) @@ -298,10 +301,10 @@ def testMultiplication(self, tsys): TransferFunction.__mul__(tsys.siso_tf1d, tsys.siso_tf2d) # State space * transfer function - sys = tsys.siso_ss1c * tsys.siso_tf1c - sys = tsys.siso_tf1c * tsys.siso_ss1c - sys = tsys.siso_ss1d * tsys.siso_tf1d - sys = tsys.siso_tf1d * tsys.siso_ss1d + _sys = tsys.siso_ss1c * tsys.siso_tf1c + _sys = tsys.siso_tf1c * tsys.siso_ss1c + _sys = tsys.siso_ss1d * tsys.siso_tf1d + _sys = tsys.siso_tf1d * tsys.siso_ss1d with pytest.raises(ValueError): TransferFunction.__mul__(tsys.siso_tf1c, tsys.siso_ss1d) @@ -309,13 +312,13 @@ def testMultiplication(self, tsys): def testFeedback(self, tsys): # State space feedback - sys = feedback(tsys.siso_ss1, tsys.siso_ss1d) - sys = feedback(tsys.siso_ss1, tsys.siso_ss1c) - sys = feedback(tsys.siso_ss1c, tsys.siso_ss1) - sys = feedback(tsys.siso_ss1d, tsys.siso_ss1) - sys = feedback(tsys.siso_ss1c, tsys.siso_ss1c) - sys = feedback(tsys.siso_ss1d, tsys.siso_ss1d) - sys = feedback(tsys.siso_ss1d, tsys.siso_ss3d) + _sys = feedback(tsys.siso_ss1, tsys.siso_ss1d) + _sys = feedback(tsys.siso_ss1, tsys.siso_ss1c) + _sys = feedback(tsys.siso_ss1c, tsys.siso_ss1) + _sys = feedback(tsys.siso_ss1d, tsys.siso_ss1) + _sys = feedback(tsys.siso_ss1c, tsys.siso_ss1c) + _sys = feedback(tsys.siso_ss1d, tsys.siso_ss1d) + _sys = feedback(tsys.siso_ss1d, tsys.siso_ss3d) with pytest.raises(ValueError): feedback(tsys.mimo_ss1c, tsys.mimo_ss1d) @@ -323,13 +326,13 @@ def testFeedback(self, tsys): feedback(tsys.mimo_ss1d, tsys.mimo_ss2d) # Transfer function feedback - sys = feedback(tsys.siso_tf1, tsys.siso_tf1d) - sys = feedback(tsys.siso_tf1, tsys.siso_tf1c) - sys = feedback(tsys.siso_tf1c, tsys.siso_tf1) - sys = feedback(tsys.siso_tf1d, tsys.siso_tf1) - sys = feedback(tsys.siso_tf1c, tsys.siso_tf1c) - sys = feedback(tsys.siso_tf1d, tsys.siso_tf1d) - sys = feedback(tsys.siso_tf1d, tsys.siso_tf3d) + _sys = feedback(tsys.siso_tf1, tsys.siso_tf1d) + _sys = feedback(tsys.siso_tf1, tsys.siso_tf1c) + _sys = feedback(tsys.siso_tf1c, tsys.siso_tf1) + _sys = feedback(tsys.siso_tf1d, tsys.siso_tf1) + _sys = feedback(tsys.siso_tf1c, tsys.siso_tf1c) + _sys = feedback(tsys.siso_tf1d, tsys.siso_tf1d) + _sys = feedback(tsys.siso_tf1d, tsys.siso_tf3d) with pytest.raises(ValueError): feedback(tsys.siso_tf1c, tsys.siso_tf1d) @@ -337,10 +340,11 @@ def testFeedback(self, tsys): feedback(tsys.siso_tf1d, tsys.siso_tf2d) # State space, transfer function - sys = feedback(tsys.siso_ss1c, tsys.siso_tf1c) - sys = feedback(tsys.siso_tf1c, tsys.siso_ss1c) - sys = feedback(tsys.siso_ss1d, tsys.siso_tf1d) - sys = feedback(tsys.siso_tf1d, tsys.siso_ss1d) + _sys = feedback(tsys.siso_ss1c, tsys.siso_tf1c) + _sys = feedback(tsys.siso_tf1c, tsys.siso_ss1c) + _sys = feedback(tsys.siso_ss1d, tsys.siso_tf1d) + + _sys = feedback(tsys.siso_tf1d, tsys.siso_ss1d) with pytest.raises(ValueError): feedback(tsys.siso_tf1c, tsys.siso_ss1d) @@ -416,11 +420,11 @@ def test_sample_system_prewarp_warning(self, tsys, plantname, discretization_typ wwarp = 1 Ts = 0.1 with pytest.warns(UserWarning, match="prewarp_frequency ignored: incompatible conversion"): - plant_d_warped = plant.sample(Ts, discretization_type, prewarp_frequency=wwarp) + plant.sample(Ts, discretization_type, prewarp_frequency=wwarp) with pytest.warns(UserWarning, match="prewarp_frequency ignored: incompatible conversion"): - plant_d_warped = sample_system(plant, Ts, discretization_type, prewarp_frequency=wwarp) + sample_system(plant, Ts, discretization_type, prewarp_frequency=wwarp) with pytest.warns(UserWarning, match="prewarp_frequency ignored: incompatible conversion"): - plant_d_warped = c2d(plant, Ts, discretization_type, prewarp_frequency=wwarp) + c2d(plant, Ts, discretization_type, prewarp_frequency=wwarp) def test_sample_system_errors(self, tsys): # Check errors diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py index aa243b607..496df42a3 100644 --- a/control/tests/docstrings_test.py +++ b/control/tests/docstrings_test.py @@ -143,14 +143,6 @@ def test_parameter_docs(module, prefix): continue # Don't fail on non-top-level functions without parameter lists - # TODO: may be able to delete this - if prefix != "" and inspect.getmodule(obj) != module and \ - doc is not None and doc["Parameters"] == [] and \ - doc["Returns"] == [] and doc["Yields"] == []: - fail_if_missing = False - else: - fail_if_missing = True - _info(f"Checking function {objname} against numpydoc", 2) _check_numpydoc_style(obj, doc) @@ -309,7 +301,7 @@ def test_deprecated_functions(module, prefix): # Get the docstring (skip w/ warning if there isn't one) if obj.__doc__ is None: - _warn(f"{objname} is missing docstring") + _warn(f"{obj} is missing docstring") continue else: docstring = inspect.getdoc(obj) @@ -320,13 +312,13 @@ def test_deprecated_functions(module, prefix): if ".. deprecated::" in doc_extended: # Make sure a FutureWarning is issued if not re.search("FutureWarning", source): - _fail(f"{objname} deprecated but does not issue " + _fail(f"{obj} deprecated but does not issue " "FutureWarning") else: if re.search(name + r"(\(\))? is deprecated", docstring) or \ re.search(name + r"(\(\))? is deprecated", source): _fail( - f"{objname} deprecated but with non-standard " + f"{obj} deprecated but with non-standard " "docs/warnings") # @@ -505,13 +497,6 @@ def test_iosys_attribute_lists(cls, ignore_future_warning): # Skip hidden and ignored attributes; methods checked elsewhere continue - # Get the object associated with this attribute - obj = getattr(cls, name, getattr(sys, name)) - if getattr(obj, '__module__', None): - objname = ".".join([obj.__module__.removeprefix("control."), name]) - else: - objname = name - # Try to find documentation in primary class if _check_parameter_docs( cls.__name__, name, docstring, fail_if_missing=False): @@ -539,7 +524,6 @@ def test_iosys_container_classes(cls): # Create a system that we can scan for attributes sys = cls(states=2, outputs=1, inputs=1) - docstring = inspect.getdoc(cls) with warnings.catch_warnings(): warnings.simplefilter('ignore') # debug via sphinx, not here doc = npd.FunctionDoc(cls) diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py index 10c512bca..c53cf2e9c 100644 --- a/control/tests/flatsys_test.py +++ b/control/tests/flatsys_test.py @@ -543,7 +543,6 @@ def test_point_to_point_errors(self): x0 = [1, 0]; u0 = [0] xf = [0, 0]; uf = [0] Tf = 10 - T = np.linspace(0, Tf, 500) # Cost function timepts = np.linspace(0, Tf, 10) @@ -658,7 +657,6 @@ def test_solve_flat_ocp_errors(self): x0 = [1, 0]; u0 = [0] xf = [0, 0]; uf = [0] Tf = 10 - T = np.linspace(0, Tf, 500) # Cost function timepts = np.linspace(0, Tf, 10) diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index 54cc94b51..ab8ce3be6 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -3,8 +3,6 @@ RvP, 4 Oct 2012 """ -import sys as pysys - import numpy as np import matplotlib.pyplot as plt import pytest @@ -13,9 +11,7 @@ from control.statesp import StateSpace from control.xferfcn import TransferFunction from control.frdata import frd, _convert_to_frd, FrequencyResponseData -from control import bdalg, evalfr, freqplot -from control.tests.conftest import slycotonly -from control.exception import pandas_check +from control import bdalg, freqplot class TestFRD: @@ -182,11 +178,6 @@ def testFeedback(self, frd_fcn): f1.feedback().frequency_response(chkpts)[0], h1.feedback().frequency_response(chkpts)[0]) - def testFeedback2(self): - h2 = StateSpace([[-1.0, 0], [0, -2.0]], [[0.4], [0.1]], - [[1.0, 0], [0, 1]], [[0.0], [0.0]]) - # h2.feedback([[0.3, 0.2], [0.1, 0.1]]) - def testAppendSiso(self): # Create frequency responses d1 = np.array([1 + 2j, 1 - 2j, 1 + 4j, 1 - 4j, 1 + 6j, 1 - 6j]) @@ -362,7 +353,6 @@ def testAgainstOctave(self): np.array([[1.0, 0], [0, 0], [0, 1]]), np.eye(3), np.zeros((3, 2))) omega = np.logspace(-1, 2, 10) - chkpts = omega[::3] f1 = frd(sys, omega) np.testing.assert_array_almost_equal( (f1.frequency_response([1.0])[0] * @@ -379,13 +369,13 @@ def test_frequency_mismatch(self, recwarn): sys1 = frd([1, 2, 3], [4, 5, 6]) sys2 = frd([2, 3, 4], [5, 6, 7]) with pytest.raises(NotImplementedError): - sys = sys1 + sys2 + sys1 + sys2 # One frequency range is a subset of another sys1 = frd([1, 2, 3], [4, 5, 6]) sys2 = frd([2, 3], [4, 5]) with pytest.raises(NotImplementedError): - sys = sys1 + sys2 + sys1 + sys2 def test_size_mismatch(self): sys1 = frd(ct.rss(2, 2, 2), np.logspace(-1, 1, 10)) @@ -393,16 +383,16 @@ def test_size_mismatch(self): # Different number of inputs sys2 = frd(ct.rss(3, 1, 2), np.logspace(-1, 1, 10)) with pytest.raises(ValueError): - sys = sys1 + sys2 + sys1 + sys2 # Different number of outputs sys2 = frd(ct.rss(3, 2, 1), np.logspace(-1, 1, 10)) with pytest.raises(ValueError): - sys = sys1 + sys2 + sys1 + sys2 # Inputs and outputs don't match with pytest.raises(ValueError): - sys = sys2 * sys1 + sys2 * sys1 # Feedback mismatch with pytest.raises(ValueError): @@ -575,12 +565,11 @@ def test_mul_mimo_siso(self, left, right, expected): np.testing.assert_array_almost_equal(expected_frd.omega, result.omega) np.testing.assert_array_almost_equal(expected_frd.frdata, result.frdata) - @slycotonly + @pytest.mark.slycot def test_truediv_mimo_siso(self): omega = np.logspace(-1, 1, 10) tf_mimo = TransferFunction([1], [1, 0]) * np.eye(2) frd_mimo = frd(tf_mimo, omega) - ss_mimo = ct.tf2ss(tf_mimo) tf_siso = TransferFunction([1], [1, 1]) frd_siso = frd(tf_siso, omega) expected = frd(tf_mimo.__truediv__(tf_siso), omega) @@ -601,7 +590,7 @@ def test_truediv_mimo_siso(self): np.testing.assert_array_almost_equal(expected.omega, result.omega) np.testing.assert_array_almost_equal(expected.frdata, result.frdata) - @slycotonly + @pytest.mark.slycot def test_rtruediv_mimo_siso(self): omega = np.logspace(-1, 1, 10) tf_mimo = TransferFunction([1], [1, 0]) * np.eye(2) @@ -609,7 +598,6 @@ def test_rtruediv_mimo_siso(self): ss_mimo = ct.tf2ss(tf_mimo) tf_siso = TransferFunction([1], [1, 1]) frd_siso = frd(tf_siso, omega) - ss_siso = ct.tf2ss(tf_siso) expected = frd(tf_siso.__rtruediv__(tf_mimo), omega) # Test division of MIMO FRD by SISO FRD @@ -801,9 +789,9 @@ def test_unrecognized_keyword(self): h = TransferFunction([1], [1, 2, 2]) omega = np.logspace(-1, 2, 10) with pytest.raises(TypeError, match="unrecognized keyword"): - sys = FrequencyResponseData(h, omega, unknown=None) + FrequencyResponseData(h, omega, unknown=None) with pytest.raises(TypeError, match="unrecognized keyword"): - sys = ct.frd(h, omega, unknown=None) + ct.frd(h, omega, unknown=None) def test_named_signals(): @@ -831,7 +819,7 @@ def test_named_signals(): assert f1.output_labels == ['y0'] -@pytest.mark.skipif(not pandas_check(), reason="pandas not installed") +@pytest.mark.pandas def test_to_pandas(): # Create a SISO frequency response h1 = TransferFunction([1], [1, 2, 2]) diff --git a/control/tests/freqplot_test.py b/control/tests/freqplot_test.py index 0b951865a..b3770486c 100644 --- a/control/tests/freqplot_test.py +++ b/control/tests/freqplot_test.py @@ -8,7 +8,6 @@ import pytest import control as ct -from control.tests.conftest import editsdefaults, slycotonly pytestmark = pytest.mark.usefixtures("mplcleanup") @@ -167,7 +166,7 @@ def test_line_styles(plt_fcn): sys3 = ct.tf([0.2, 0.1], [1, 0.1, 0.3, 0.1, 0.1], name='sys3') # Create a plot for the first system, with custom styles - lines_default = plt_fcn(sys1) + plt_fcn(sys1) # Now create a plot using *fmt customization lines_fmt = plt_fcn(sys2, None, 'r--') @@ -681,6 +680,39 @@ def test_display_margins(nsys, display_margins, gridkw, match): assert cplt.axes[0, 0].get_title() == '' +def test_singular_values_plot_colors(): + # Define some systems for testing + sys1 = ct.rss(4, 2, 2, strictly_proper=True) + sys2 = ct.rss(4, 2, 2, strictly_proper=True) + + # Get the default color cycle + color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color'] + + # Plot the systems individually and make sure line colors are OK + cplt = ct.singular_values_plot(sys1) + assert cplt.lines.size == 1 + assert len(cplt.lines[0]) == 2 + assert cplt.lines[0][0].get_color() == color_cycle[0] + assert cplt.lines[0][1].get_color() == color_cycle[0] + + cplt = ct.singular_values_plot(sys2) + assert cplt.lines.size == 1 + assert len(cplt.lines[0]) == 2 + assert cplt.lines[0][0].get_color() == color_cycle[1] + assert cplt.lines[0][1].get_color() == color_cycle[1] + plt.close('all') + + # Plot the systems as a list and make sure colors are OK + cplt = ct.singular_values_plot([sys1, sys2]) + assert cplt.lines.size == 2 + assert len(cplt.lines[0]) == 2 + assert len(cplt.lines[1]) == 2 + assert cplt.lines[0][0].get_color() == color_cycle[0] + assert cplt.lines[0][1].get_color() == color_cycle[0] + assert cplt.lines[1][0].get_color() == color_cycle[1] + assert cplt.lines[1][1].get_color() == color_cycle[1] + + if __name__ == "__main__": # # Interactive mode: generate plots for manual viewing diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index a268d38eb..5112a99e9 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -19,7 +19,6 @@ singular_values_plot, singular_values_response) from control.matlab import bode, rss, ss, tf from control.statesp import StateSpace -from control.tests.conftest import slycotonly from control.xferfcn import TransferFunction pytestmark = pytest.mark.usefixtures("mplcleanup") @@ -61,7 +60,7 @@ def test_freqresp_siso(ss_siso): @pytest.mark.filterwarnings(r"ignore:freqresp\(\) is deprecated") -@slycotonly +@pytest.mark.slycot def test_freqresp_mimo_legacy(ss_mimo): """Test MIMO frequency response calls""" omega = np.linspace(10e-2, 10e2, 1000) @@ -70,7 +69,7 @@ def test_freqresp_mimo_legacy(ss_mimo): ctrl.freqresp(tf_mimo, omega) -@slycotonly +@pytest.mark.slycot def test_freqresp_mimo(ss_mimo): """Test MIMO frequency response calls""" omega = np.linspace(10e-2, 10e2, 1000) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index d124859fc..ccce76f34 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -15,7 +15,6 @@ import pytest import numpy as np -import scipy as sp import math import control as ct @@ -46,25 +45,23 @@ def test_summing_junction(inputs, output, dimension, D): def test_summation_exceptions(): # Bad input description with pytest.raises(ValueError, match="could not parse input"): - sumblk = ct.summing_junction(np.pi, 'y') + ct.summing_junction(np.pi, 'y') # Bad output description with pytest.raises(ValueError, match="could not parse output"): - sumblk = ct.summing_junction('u', np.pi) + ct.summing_junction('u', np.pi) # Bad input dimension with pytest.raises(ValueError, match="unrecognized dimension"): - sumblk = ct.summing_junction('u', 'y', dimension=False) + ct.summing_junction('u', 'y', dimension=False) -@pytest.mark.parametrize("dim", [1, 3]) +@pytest.mark.parametrize("dim", + [1, pytest.param(3, marks=pytest.mark.slycot)]) def test_interconnect_implicit(dim): """Test the use of implicit connections in interconnect()""" import random - if dim != 1 and not ct.slycot_check(): - pytest.xfail("slycot not installed") - # System definition P = ct.rss(2, dim, dim, strictly_proper=True, name='P') @@ -346,7 +343,7 @@ def test_interconnect_exceptions(): # NonlinearIOSytem with pytest.raises(TypeError, match="unrecognized keyword"): - nlios = ct.NonlinearIOSystem( + ct.NonlinearIOSystem( None, lambda t, x, u, params: u*u, input_count=1, output_count=1) # Summing junction diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 10eb7fb68..5d741ae83 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -18,7 +18,6 @@ import control as ct import control.flatsys as fs -from control.tests.conftest import slycotonly class TestIOSys: @@ -1412,7 +1411,7 @@ def test_operand_incompatible(self, Pout, Pin, C, op): C = ct.rss(2, 2, 3) with pytest.raises(ValueError, match="incompatible"): - PC = op(P, C) + op(P, C) @pytest.mark.parametrize( "C, op", [ @@ -1709,9 +1708,9 @@ def test_interconnect_unused_input(): with pytest.warns( UserWarning, match=r"Unused input\(s\) in InterconnectedSystem"): - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y']) with warnings.catch_warnings(): # no warning if output explicitly ignored, various argument forms @@ -1719,45 +1718,43 @@ def test_interconnect_unused_input(): # strip out matrix warnings warnings.filterwarnings("ignore", "the matrix subclass", category=PendingDeprecationWarning) - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_inputs=['n']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['n']) - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_inputs=['s.n']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['s.n']) # no warning if auto-connect disabled - h = ct.interconnect([g,s,k], - connections=False) + ct.interconnect([g,s,k], + connections=False) # warn if explicity ignored input in fact used with pytest.warns( UserWarning, - match=r"Input\(s\) specified as ignored is \(are\) used:") \ - as record: - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_inputs=['u','n']) + match=r"Input\(s\) specified as ignored is \(are\) used:"): + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['u','n']) with pytest.warns( UserWarning, - match=r"Input\(s\) specified as ignored is \(are\) used:") \ - as record: - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_inputs=['k.e','n']) + match=r"Input\(s\) specified as ignored is \(are\) used:"): + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['k.e','n']) # error if ignored signal doesn't exist with pytest.raises(ValueError): - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_inputs=['v']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['v']) def test_interconnect_unused_output(): @@ -1779,10 +1776,10 @@ def test_interconnect_unused_output(): with pytest.warns( UserWarning, - match=r"Unused output\(s\) in InterconnectedSystem:") as record: - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y']) + match=r"Unused output\(s\) in InterconnectedSystem:"): + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y']) # no warning if output explicitly ignored @@ -1791,43 +1788,43 @@ def test_interconnect_unused_output(): # strip out matrix warnings warnings.filterwarnings("ignore", "the matrix subclass", category=PendingDeprecationWarning) - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_outputs=['dy']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['dy']) - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_outputs=['g.dy']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['g.dy']) # no warning if auto-connect disabled - h = ct.interconnect([g,s,k], - connections=False) + ct.interconnect([g,s,k], + connections=False) # warn if explicity ignored output in fact used with pytest.warns( UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"): - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_outputs=['dy','u']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['dy','u']) with pytest.warns( UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"): - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_outputs=['dy', ('k.u')]) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['dy', ('k.u')]) # error if ignored signal doesn't exist with pytest.raises(ValueError): - h = ct.interconnect([g,s,k], - inputs=['r'], - outputs=['y'], - ignore_outputs=['v']) + ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['v']) def test_interconnect_add_unused(): @@ -1900,11 +1897,11 @@ def test_input_output_broadcasting(): # Specify only some of the initial conditions with pytest.warns(UserWarning, match="X0 too short; padding"): - resp_short = ct.input_output_response(sys, T, [U[0], [0, 1]], [X0, 1]) + ct.input_output_response(sys, T, [U[0], [0, 1]], [X0, 1]) # Make sure that inconsistent settings don't work with pytest.raises(ValueError, match="inconsistent"): - resp_bad = ct.input_output_response( + ct.input_output_response( sys, T, (U[0, :], U[:2, :-1]), [X0, P0]) @pytest.mark.parametrize("nstates, ninputs, noutputs", [ diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 54a1fc76e..566b35a28 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -13,9 +13,10 @@ import inspect import warnings -import matplotlib.pyplot as plt import pytest +import numpy as np + import control import control.flatsys import control.tests.descfcn_test as descfcn_test @@ -173,6 +174,7 @@ def test_unrecognized_kwargs(function, nsssys, ntfsys, moreargs, kwargs, (control.phase_plane_plot, 1, ([-1, 1, -1, 1], 1), {}), (control.phaseplot.streamlines, 1, ([-1, 1, -1, 1], 1), {}), (control.phaseplot.vectorfield, 1, ([-1, 1, -1, 1], ), {}), + (control.phaseplot.streamplot, 1, ([-1, 1, -1, 1], ), {}), (control.phaseplot.equilpoints, 1, ([-1, 1, -1, 1], ), {}), (control.phaseplot.separatrices, 1, ([-1, 1, -1, 1], ), {}), (control.singular_values_plot, 1, (), {})] @@ -342,9 +344,6 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): 'PoleZeroList.plot': test_response_plot_kwargs, 'InterconnectedSystem.__init__': interconnect_test.test_interconnect_exceptions, - 'StateSpace.__init__': - interconnect_test.test_interconnect_exceptions, - 'StateSpace.sample': test_unrecognized_kwargs, 'NonlinearIOSystem.__init__': interconnect_test.test_interconnect_exceptions, 'StateSpace.__init__': test_unrecognized_kwargs, @@ -369,6 +368,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): optimal_test.test_oep_argument_errors, 'phaseplot.streamlines': test_matplotlib_kwargs, 'phaseplot.vectorfield': test_matplotlib_kwargs, + 'phaseplot.streamplot': test_matplotlib_kwargs, 'phaseplot.equilpoints': test_matplotlib_kwargs, 'phaseplot.separatrices': test_matplotlib_kwargs, } diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 9edf09013..dd95f3505 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -8,11 +8,7 @@ import control as ct from control import NonlinearIOSystem, c2d, common_timebase, isctime, \ isdtime, issiso, ss, tf, tf2ss -from control.exception import slycot_check from control.lti import LTI, bandwidth, damp, dcgain, evalfr, poles, zeros -from control.tests.conftest import slycotonly - -from .conftest import editsdefaults class TestLTI: @@ -26,10 +22,10 @@ def test_poles(self, fun, args): np.testing.assert_allclose(poles(sys), 42) with pytest.raises(AttributeError, match="no attribute 'pole'"): - pole_list = sys.pole() + sys.pole() with pytest.raises(AttributeError, match="no attribute 'pole'"): - pole_list = ct.pole(sys) + ct.pole(sys) @pytest.mark.parametrize("fun, args", [ [tf, (126, [-1, 42])], @@ -41,10 +37,10 @@ def test_zeros(self, fun, args): np.testing.assert_allclose(zeros(sys), 42) with pytest.raises(AttributeError, match="no attribute 'zero'"): - zero_list = sys.zero() + sys.zero() with pytest.raises(AttributeError, match="no attribute 'zero'"): - zero_list = ct.zero(sys) + ct.zero(sys) def test_issiso(self): assert issiso(1) @@ -61,7 +57,7 @@ def test_issiso(self): assert issiso(sys) assert issiso(sys, strict=True) - @slycotonly + @pytest.mark.slycot def test_issiso_mimo(self): # MIMO transfer function sys = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]], @@ -192,6 +188,10 @@ def test_isdtime(self, objfun, arg, dt, ref, strictref): assert isctime(obj) == ref assert isctime(obj, strict=True) == strictref + def p(*args): + # convenience for parametrize below + return pytest.param(*args, marks=pytest.mark.slycot) + @pytest.mark.usefixtures("editsdefaults") @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd]) @pytest.mark.parametrize("nstate, nout, ninp, omega, squeeze, shape", [ @@ -204,26 +204,26 @@ def test_isdtime(self, objfun, arg, dt, ref, strictref): [3, 1, 1, 0.1, False, (1, 1)], [3, 1, 1, [0.1], False, (1, 1, 1)], [3, 1, 1, [0.1, 1, 10], False, (1, 1, 3)], - [1, 2, 1, 0.1, None, (2, 1)], # SIMO - [1, 2, 1, [0.1], None, (2, 1, 1)], - [1, 2, 1, [0.1, 1, 10], None, (2, 1, 3)], - [2, 2, 1, 0.1, True, (2,)], - [2, 2, 1, [0.1], True, (2,)], - [3, 2, 1, 0.1, False, (2, 1)], - [3, 2, 1, [0.1], False, (2, 1, 1)], - [3, 2, 1, [0.1, 1, 10], False, (2, 1, 3)], - [1, 1, 2, [0.1, 1, 10], None, (1, 2, 3)], # MISO - [2, 1, 2, [0.1, 1, 10], True, (2, 3)], - [3, 1, 2, [0.1, 1, 10], False, (1, 2, 3)], - [1, 1, 2, 0.1, None, (1, 2)], - [1, 1, 2, 0.1, True, (2,)], - [1, 1, 2, 0.1, False, (1, 2)], - [1, 2, 2, [0.1, 1, 10], None, (2, 2, 3)], # MIMO - [2, 2, 2, [0.1, 1, 10], True, (2, 2, 3)], - [3, 2, 2, [0.1, 1, 10], False, (2, 2, 3)], - [1, 2, 2, 0.1, None, (2, 2)], - [2, 2, 2, 0.1, True, (2, 2)], - [3, 2, 2, 0.1, False, (2, 2)], + p(1, 2, 1, 0.1, None, (2, 1)), + p(1, 2, 1, [0.1], None, (2, 1, 1)), + p(1, 2, 1, [0.1, 1, 10], None, (2, 1, 3)), + p(2, 2, 1, 0.1, True, (2,)), + p(2, 2, 1, [0.1], True, (2,)), + p(3, 2, 1, 0.1, False, (2, 1)), + p(3, 2, 1, [0.1], False, (2, 1, 1)), + p(3, 2, 1, [0.1, 1, 10], False, (2, 1, 3)), + p(1, 1, 2, [0.1, 1, 10], None, (1, 2, 3)), # MISO + p(2, 1, 2, [0.1, 1, 10], True, (2, 3)), + p(3, 1, 2, [0.1, 1, 10], False, (1, 2, 3)), + p(1, 1, 2, 0.1, None, (1, 2)), + p(1, 1, 2, 0.1, True, (2,)), + p(1, 1, 2, 0.1, False, (1, 2)), + p(1, 2, 2, [0.1, 1, 10], None, (2, 2, 3)), # MIMO + p(2, 2, 2, [0.1, 1, 10], True, (2, 2, 3)), + p(3, 2, 2, [0.1, 1, 10], False, (2, 2, 3)), + p(1, 2, 2, 0.1, None, (2, 2)), + p(2, 2, 2, 0.1, True, (2, 2)), + p(3, 2, 2, 0.1, False, (2, 2)), ]) @pytest.mark.parametrize("omega_type", ["numpy", "native"]) def test_squeeze(self, fcn, nstate, nout, ninp, omega, squeeze, shape, @@ -232,9 +232,6 @@ def test_squeeze(self, fcn, nstate, nout, ninp, omega, squeeze, shape, # Create the system to be tested if fcn == ct.frd: sys = fcn(ct.rss(nstate, nout, ninp), [1e-2, 1e-1, 1, 1e1, 1e2]) - elif fcn == ct.tf and (nout > 1 or ninp > 1) and not slycot_check(): - pytest.skip("Conversion of MIMO systems to transfer functions " - "requires slycot.") else: sys = fcn(ct.rss(nstate, nout, ninp)) @@ -295,7 +292,7 @@ def test_squeeze_exceptions(self, fcn): sys = fcn(ct.rss(2, 1, 1)) with pytest.raises(ValueError, match="unknown squeeze value"): - resp = sys.frequency_response([1], squeeze='siso') + sys.frequency_response([1], squeeze='siso') with pytest.raises(ValueError, match="unknown squeeze value"): sys([1j], squeeze='siso') with pytest.raises(ValueError, match="unknown squeeze value"): diff --git a/control/tests/margin_test.py b/control/tests/margin_test.py index 43cd68ae3..c8be4ee6c 100644 --- a/control/tests/margin_test.py +++ b/control/tests/margin_test.py @@ -14,7 +14,7 @@ from control import ControlMIMONotImplemented, FrequencyResponseData, \ StateSpace, TransferFunction, margin, phase_crossover_frequencies, \ - stability_margins + stability_margins, disk_margins, tf, ss s = TransferFunction.s @@ -372,3 +372,112 @@ def test_stability_margins_discrete(cnum, cden, dt, else: out = stability_margins(tf) assert_allclose(out, ref, rtol=rtol) + +def test_siso_disk_margin(): + # Frequencies of interest + omega = np.logspace(-1, 2, 1001) + + # Loop transfer function + L = tf(25, [1, 10, 10, 10]) + + # Balanced (S - T) disk-based stability margins + DM, DGM, DPM = disk_margins(L, omega, skew=0.0) + assert_allclose([DM], [0.46], atol=0.1) # disk margin of 0.46 + assert_allclose([DGM], [4.05], atol=0.1) # disk-based gain margin of 4.05 dB + assert_allclose([DPM], [25.8], atol=0.1) # disk-based phase margin of 25.8 deg + + # For SISO systems, the S-based (S) disk margin should match the third output + # of existing library "stability_margins", i.e., minimum distance from the + # Nyquist plot to -1. + _, _, SM = stability_margins(L)[:3] + DM = disk_margins(L, omega, skew=1.0)[0] + assert_allclose([DM], [SM], atol=0.01) + +@pytest.mark.slycot +def test_mimo_disk_margin(): + # Frequencies of interest + omega = np.logspace(-1, 3, 1001) + + # Loop transfer gain + P = ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], [-10, 1]], 0) # plant + K = ss([], [], [], [[1, -2], [0, 1]]) # controller + Lo = P * K # loop transfer function, broken at plant output + Li = K * P # loop transfer function, broken at plant input + + # Balanced (S - T) disk-based stability margins at plant output + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) + assert_allclose([DMo], [0.3754], atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMo], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMo], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg + + # Balanced (S - T) disk-based stability margins at plant input + DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0) + assert_allclose([DMi], [0.3754], atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMi], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMi], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg + + +@pytest.mark.noslycot +def test_mimo_disk_margin_exception(): + # Slycot not installed. Should throw exception. + # Frequencies of interest + omega = np.logspace(-1, 3, 1001) + + # Loop transfer gain + P = ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], [-10, 1]], 0) # plant + K = ss([], [], [], [[1, -2], [0, 1]]) # controller + Lo = P * K # loop transfer function, broken at plant output + with pytest.raises(ControlMIMONotImplemented,\ + match="Need slycot to compute MIMO disk_margins"): + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) + +def test_siso_disk_margin_return_all(): + # Frequencies of interest + omega = np.logspace(-1, 2, 1001) + + # Loop transfer function + L = tf(25, [1, 10, 10, 10]) + + # Balanced (S - T) disk-based stability margins + DM, DGM, DPM = disk_margins(L, omega, skew=0.0, returnall=True) + assert_allclose([omega[np.argmin(DM)]], [1.94],\ + atol=0.01) # sensitivity peak at 1.94 rad/s + assert_allclose([min(DM)], [0.46], atol=0.1) # disk margin of 0.46 + assert_allclose([DGM[np.argmin(DM)]], [4.05],\ + atol=0.1) # disk-based gain margin of 4.05 dB + assert_allclose([DPM[np.argmin(DM)]], [25.8],\ + atol=0.1) # disk-based phase margin of 25.8 deg + + +@pytest.mark.slycot +def test_mimo_disk_margin_return_all(): + # Frequencies of interest + omega = np.logspace(-1, 3, 1001) + + # Loop transfer gain + P = ss([[0, 10], [-10, 0]], np.eye(2),\ + [[1, 10], [-10, 1]], 0) # plant + K = ss([], [], [], [[1, -2], [0, 1]]) # controller + Lo = P * K # loop transfer function, broken at plant output + Li = K * P # loop transfer function, broken at plant input + + # Balanced (S - T) disk-based stability margins at plant output + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True) + assert_allclose([omega[np.argmin(DMo)]], [omega[0]],\ + atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) + assert_allclose([min(DMo)], [0.3754], atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMo[np.argmin(DMo)]], [3.3],\ + atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMo[np.argmin(DMo)]], [21.26],\ + atol=0.1) # disk-based phase margin of 21.26 deg + + # Balanced (S - T) disk-based stability margins at plant input + DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0, returnall=True) + assert_allclose([omega[np.argmin(DMi)]], [omega[0]],\ + atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) + assert_allclose([min(DMi)], [0.3754],\ + atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMi[np.argmin(DMi)]], [3.3],\ + atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMi[np.argmin(DMi)]], [21.26],\ + atol=0.1) # disk-based phase margin of 21.26 deg diff --git a/control/tests/mateqn_test.py b/control/tests/mateqn_test.py index 0ae5a7db2..77bf553bf 100644 --- a/control/tests/mateqn_test.py +++ b/control/tests/mateqn_test.py @@ -39,56 +39,58 @@ import pytest from scipy.linalg import eigvals, solve -import control as ct from control.mateqn import lyap, dlyap, care, dare -from control.exception import ControlArgument, ControlDimension, slycot_check -from control.tests.conftest import slycotonly +from control.exception import ControlArgument, ControlDimension class TestMatrixEquations: """These are tests for the matrix equation solvers in mateqn.py""" - def test_lyap(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_lyap(self, method): A = array([[-1, 1], [-1, 0]]) Q = array([[1, 0], [0, 1]]) - X = lyap(A, Q) + X = lyap(A, Q, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X + X @ A.T + Q, zeros((2,2))) A = array([[1, 2], [-3, -4]]) Q = array([[3, 1], [1, 1]]) - X = lyap(A,Q) + X = lyap(A,Q, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X + X @ A.T + Q, zeros((2,2))) # Compare methods - if slycot_check(): + if method == 'slycot': X_scipy = lyap(A, Q, method='scipy') - X_slycot = lyap(A, Q, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) + assert_array_almost_equal(X_scipy, X) - def test_lyap_sylvester(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_lyap_sylvester(self, method): A = 5 B = array([[4, 3], [4, 3]]) C = array([2, 1]) - X = lyap(A, B, C) + X = lyap(A, B, C, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A * X + X @ B + C, zeros((1,2))) A = array([[2, 1], [1, 2]]) B = array([[1, 2], [0.5, 0.1]]) C = array([[1, 0], [0, 1]]) - X = lyap(A, B, C) + X = lyap(A, B, C, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X + X @ B + C, zeros((2,2))) # Compare methods - if slycot_check(): + if method=='slycot': X_scipy = lyap(A, B, C, method='scipy') - X_slycot = lyap(A, B, C, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) + assert_array_almost_equal(X_scipy, X) - @slycotonly + @pytest.mark.slycot def test_lyap_g(self): A = array([[-1, 2], [-3, -4]]) Q = array([[3, 1], [1, 1]]) @@ -102,20 +104,28 @@ def test_lyap_g(self): with pytest.raises(ControlArgument, match="'scipy' not valid"): X = lyap(A, Q, None, E, method='scipy') - def test_dlyap(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dlyap(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[1,0],[0,1]]) - X = dlyap(A,Q) + X = dlyap(A,Q,method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X @ A.T - X + Q, zeros((2,2))) A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[3, 1],[1, 1]]) - X = dlyap(A,Q) + X = dlyap(A,Q,method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X @ A.T - X + Q, zeros((2,2))) - @slycotonly + # Compare methods + if method=='slycot': + X_scipy = dlyap(A,Q, method='scipy') + assert_array_almost_equal(X_scipy, X) + + @pytest.mark.slycot def test_dlyap_g(self): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[3, 1],[1, 1]]) @@ -129,7 +139,7 @@ def test_dlyap_g(self): with pytest.raises(ControlArgument, match="'scipy' not valid"): X = dlyap(A, Q, None, E, method='scipy') - @slycotonly + @pytest.mark.slycot def test_dlyap_sylvester(self): A = 5 B = array([[4, 3], [4, 3]]) @@ -149,12 +159,15 @@ def test_dlyap_sylvester(self): with pytest.raises(ControlArgument, match="'scipy' not valid"): X = dlyap(A, B, C, method='scipy') - def test_care(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_care(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) B = array([[1, 0],[0, 4]]) - X, L, G = care(A, B, Q) + X, L, G = care(A, B, Q, method=method) # print("The solution obtained is", X) M = A.T @ X + X @ A - X @ B @ B.T @ X + Q assert_array_almost_equal(M, @@ -162,14 +175,16 @@ def test_care(self): assert_array_almost_equal(B.T @ X, G) # Compare methods - if slycot_check(): + if method == 'slycot': X_scipy, L_scipy, G_scipy = care(A, B, Q, method='scipy') - X_slycot, L_slycot, G_slycot = care(A, B, Q, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) - assert_array_almost_equal(np.sort(L_scipy), np.sort(L_slycot)) - assert_array_almost_equal(G_scipy, G_slycot) - - def test_care_g(self): + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(np.sort(L_scipy), np.sort(L)) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_care_g(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) B = array([[1, 0],[0, 4]]) @@ -177,7 +192,7 @@ def test_care_g(self): S = array([[0, 0],[0, 0]]) E = array([[2, 1],[1, 2]]) - X,L,G = care(A,B,Q,R,S,E) + X,L,G = care(A,B,Q,R,S,E,method=method) # print("The solution obtained is", X) Gref = solve(R, B.T @ X @ E + S.T) assert_array_almost_equal(Gref, G) @@ -187,16 +202,17 @@ def test_care_g(self): zeros((2,2))) # Compare methods - if slycot_check(): + if method=='slycot': X_scipy, L_scipy, G_scipy = care( A, B, Q, R, S, E, method='scipy') - X_slycot, L_slycot, G_slycot = care( - A, B, Q, R, S, E, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) - assert_array_almost_equal(np.sort(L_scipy), np.sort(L_slycot)) - assert_array_almost_equal(G_scipy, G_slycot) - - def test_care_g2(self): + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(np.sort(L_scipy), np.sort(L)) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_care_g2(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) B = array([[1],[0]]) @@ -204,7 +220,7 @@ def test_care_g2(self): S = array([[1],[0]]) E = array([[2, 1],[1, 2]]) - X,L,G = care(A,B,Q,R,S,E) + X,L,G = care(A,B,Q,R,S,E,method=method) # print("The solution obtained is", X) Gref = 1/R * (B.T @ X @ E + S.T) assert_array_almost_equal( @@ -214,22 +230,23 @@ def test_care_g2(self): assert_array_almost_equal(Gref , G) # Compare methods - if slycot_check(): + if method=='slycot': X_scipy, L_scipy, G_scipy = care( A, B, Q, R, S, E, method='scipy') - X_slycot, L_slycot, G_slycot = care( - A, B, Q, R, S, E, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) - assert_array_almost_equal(L_scipy, L_slycot) - assert_array_almost_equal(G_scipy, G_slycot) - - def test_dare(self): + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(L_scipy, L) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dare(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[2, 1],[1, 0]]) B = array([[2, 1],[0, 1]]) R = array([[1, 0],[0, 1]]) - X, L, G = dare(A, B, Q, R) + X, L, G = dare(A, B, Q, R, method=method) # print("The solution obtained is", X) Gref = solve(B.T @ X @ B + R, B.T @ X @ A) assert_array_almost_equal(Gref, G) @@ -244,7 +261,7 @@ def test_dare(self): B = array([[1],[0]]) R = 2 - X, L, G = dare(A, B, Q, R) + X, L, G = dare(A, B, Q, R, method=method) # print("The solution obtained is", X) AtXA = A.T @ X @ A AtXB = A.T @ X @ B @@ -257,6 +274,7 @@ def test_dare(self): lam = eigvals(A - B @ G) assert_array_less(abs(lam), 1.0) + @pytest.mark.slycot def test_dare_compare(self): A = np.array([[-0.6, 0], [-0.1, -0.4]]) Q = np.array([[2, 1], [1, 0]]) @@ -268,15 +286,16 @@ def test_dare_compare(self): # Solve via scipy X_scipy, L_scipy, G_scipy = dare(A, B, Q, R, method='scipy') - # Solve via slycot - if ct.slycot_check(): - X_slicot, L_slicot, G_slicot = dare( - A, B, Q, R, S, E, method='scipy') - np.testing.assert_almost_equal(X_scipy, X_slicot) - np.testing.assert_almost_equal(L_scipy, L_slicot) - np.testing.assert_almost_equal(G_scipy, G_slicot) + X_slicot, L_slicot, G_slicot = dare( + A, B, Q, R, S, E, method='scipy') + np.testing.assert_almost_equal(X_scipy, X_slicot) + np.testing.assert_almost_equal(L_scipy, L_slicot) + np.testing.assert_almost_equal(G_scipy, G_slicot) - def test_dare_g(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dare_g(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[2, 1],[1, 3]]) B = array([[1, 5],[2, 4]]) @@ -284,7 +303,7 @@ def test_dare_g(self): S = array([[1, 0],[2, 0]]) E = array([[2, 1],[1, 2]]) - X, L, G = dare(A, B, Q, R, S, E) + X, L, G = dare(A, B, Q, R, S, E, method=method) # print("The solution obtained is", X) Gref = solve(B.T @ X @ B + R, B.T @ X @ A + S.T) assert_array_almost_equal(Gref, G) @@ -294,8 +313,18 @@ def test_dare_g(self): # check for stable closed loop lam = eigvals(A - B @ G, E) assert_array_less(abs(lam), 1.0) - - def test_dare_g2(self): + # Compare methods + if method=='slycot': + X_scipy, L_scipy, G_scipy = dare( + A, B, Q, R, S, E, method='scipy') + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(L_scipy, L) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dare_g2(self, method): A = array([[-0.6, 0], [-0.1, -0.4]]) Q = array([[2, 1], [1, 3]]) B = array([[1], [2]]) @@ -303,7 +332,7 @@ def test_dare_g2(self): S = array([[1], [2]]) E = array([[2, 1], [1, 2]]) - X, L, G = dare(A, B, Q, R, S, E) + X, L, G = dare(A, B, Q, R, S, E, method=method) # print("The solution obtained is", X) AtXA = A.T @ X @ A AtXB = A.T @ X @ B @@ -317,6 +346,13 @@ def test_dare_g2(self): lam = eigvals(A - B @ G, E) assert_array_less(abs(lam), 1.0) + if method=='slycot': + X_scipy, L_scipy, G_scipy = dare( + A, B, Q, R, S, E, method='scipy') + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(L_scipy, L) + assert_array_almost_equal(G_scipy, G) + def test_raise(self): """ Test exception raise for invalid inputs """ diff --git a/control/tests/matlab2_test.py b/control/tests/matlab2_test.py index 4d135e33e..f8b0d2b40 100644 --- a/control/tests/matlab2_test.py +++ b/control/tests/matlab2_test.py @@ -16,7 +16,6 @@ from control.matlab import ss, step, impulse, initial, lsim, dcgain, ss2tf from control.timeresp import _check_convert_array -from control.tests.conftest import slycotonly class TestControlMatlab: diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index b7e0d25d2..d1a71bce3 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -30,7 +30,6 @@ from control.exception import ControlArgument from control.frdata import FRD -from control.tests.conftest import slycotonly # for running these through Matlab or Octave ''' @@ -130,33 +129,33 @@ def mimo(self): def testParallel(self, siso): """Call parallel()""" - sys1 = parallel(siso.ss1, siso.ss2) - sys1 = parallel(siso.ss1, siso.tf2) - sys1 = parallel(siso.tf1, siso.ss2) - sys1 = parallel(1, siso.ss2) - sys1 = parallel(1, siso.tf2) - sys1 = parallel(siso.ss1, 1) - sys1 = parallel(siso.tf1, 1) + _sys1 = parallel(siso.ss1, siso.ss2) + _sys1 = parallel(siso.ss1, siso.tf2) + _sys1 = parallel(siso.tf1, siso.ss2) + _sys1 = parallel(1, siso.ss2) + _sys1 = parallel(1, siso.tf2) + _sys1 = parallel(siso.ss1, 1) + _sys1 = parallel(siso.tf1, 1) def testSeries(self, siso): """Call series()""" - sys1 = series(siso.ss1, siso.ss2) - sys1 = series(siso.ss1, siso.tf2) - sys1 = series(siso.tf1, siso.ss2) - sys1 = series(1, siso.ss2) - sys1 = series(1, siso.tf2) - sys1 = series(siso.ss1, 1) - sys1 = series(siso.tf1, 1) + _sys1 = series(siso.ss1, siso.ss2) + _sys1 = series(siso.ss1, siso.tf2) + _sys1 = series(siso.tf1, siso.ss2) + _sys1 = series(1, siso.ss2) + _sys1 = series(1, siso.tf2) + _sys1 = series(siso.ss1, 1) + _sys1 = series(siso.tf1, 1) def testFeedback(self, siso): """Call feedback()""" - sys1 = feedback(siso.ss1, siso.ss2) - sys1 = feedback(siso.ss1, siso.tf2) - sys1 = feedback(siso.tf1, siso.ss2) - sys1 = feedback(1, siso.ss2) - sys1 = feedback(1, siso.tf2) - sys1 = feedback(siso.ss1, 1) - sys1 = feedback(siso.tf1, 1) + _sys1 = feedback(siso.ss1, siso.ss2) + _sys1 = feedback(siso.ss1, siso.tf2) + _sys1 = feedback(siso.tf1, siso.ss2) + _sys1 = feedback(1, siso.ss2) + _sys1 = feedback(1, siso.tf2) + _sys1 = feedback(siso.ss1, 1) + _sys1 = feedback(siso.tf1, 1) def testPoleZero(self, siso): """Call pole() and zero()""" @@ -487,21 +486,21 @@ def testEvalfr_mimo(self, mimo): ref = np.array([[44.8 - 21.4j, 0.], [0., 44.8 - 21.4j]]) np.testing.assert_array_almost_equal(fr, ref) - @slycotonly + @pytest.mark.slycot def testHsvd(self, siso): """Call hsvd()""" hsvd(siso.ss1) hsvd(siso.ss2) hsvd(siso.ss3) - @slycotonly + @pytest.mark.slycot def testBalred(self, siso): """Call balred()""" balred(siso.ss1, 1) balred(siso.ss2, 2) balred(siso.ss3, [2, 2]) - @slycotonly + @pytest.mark.slycot def testModred(self, siso): """Call modred()""" modred(siso.ss1, [1]) @@ -509,7 +508,7 @@ def testModred(self, siso): modred(siso.ss1, [1], 'matchdc') modred(siso.ss1, [1], 'truncate') - @slycotonly + @pytest.mark.slycot def testPlace_varga(self, siso): """Call place_varga()""" place_varga(siso.ss1.A, siso.ss1.B, [-2, -2]) @@ -552,7 +551,7 @@ def testObsv(self, siso): obsv(siso.ss1.A, siso.ss1.C) obsv(siso.ss2.A, siso.ss2.C) - @slycotonly + @pytest.mark.slycot def testGram(self, siso): """Call gram()""" gram(siso.ss1, 'c') @@ -582,10 +581,11 @@ def testOpers(self, siso): # siso.tf1 / siso.ss2 def testUnwrap(self): - """Call unwrap()""" + # control.matlab.unwrap phase = np.array(range(1, 100)) / 10. wrapped = phase % (2 * np.pi) unwrapped = unwrap(wrapped) + np.testing.assert_array_almost_equal(phase, unwrapped) def testSISOssdata(self, siso): """Call ssdata() @@ -695,7 +695,7 @@ def testFRD(self): frd2 = frd(frd1.frdata[0, 0, :], omega) assert isinstance(frd2, FRD) - @slycotonly + @pytest.mark.slycot def testMinreal(self, verbose=False): """Test a minreal model reduction""" # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] diff --git a/control/tests/minreal_test.py b/control/tests/minreal_test.py index 10c56d4ca..e8223184c 100644 --- a/control/tests/minreal_test.py +++ b/control/tests/minreal_test.py @@ -11,7 +11,6 @@ from control.statesp import StateSpace from control.xferfcn import TransferFunction from itertools import permutations -from control.tests.conftest import slycotonly @pytest.fixture @@ -19,7 +18,7 @@ def fixedseed(scope="class"): np.random.seed(5) -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures("fixedseed") class TestMinreal: """Tests for the StateSpace class.""" diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 7dcda6296..c2773231b 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -3,7 +3,6 @@ RMM, 30 Mar 2011 (based on TestModelSimp from v0.4a) """ -import math import warnings import numpy as np @@ -15,13 +14,12 @@ from control.exception import ControlArgument, ControlDimension from control.modelsimp import balred, eigensys_realization, hsvd, markov, \ modred -from control.tests.conftest import slycotonly class TestModelsimp: """Test model reduction functions""" - @slycotonly + @pytest.mark.slycot def testHSVD(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -124,6 +122,7 @@ def testMarkovSignature(self): inp = np.array([1, 2]) outp = np.array([2, 4]) mrk = markov(outp, inp, 1, transpose=False) + np.testing.assert_almost_equal(mrk, 2.) # Test mimo example # Mechanical Vibrations: Theory and Application, SI Edition, 1st ed. @@ -390,7 +389,7 @@ def testModredTruncate(self): np.testing.assert_array_almost_equal(rsys.D, Drtrue) - @slycotonly + @pytest.mark.slycot def testBalredTruncate(self): # controlable canonical realization computed in matlab for the transfer # function: @@ -431,7 +430,7 @@ def testBalredTruncate(self): np.testing.assert_array_almost_equal(Cr, Crtrue, decimal=4) np.testing.assert_array_almost_equal(Dr, Drtrue, decimal=4) - @slycotonly + @pytest.mark.slycot def testBalredMatchDC(self): # controlable canonical realization computed in matlab for the transfer # function: diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 34feb5b35..8c44f5980 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -8,7 +8,6 @@ created for that purpose. """ -import re from copy import copy import warnings @@ -80,21 +79,26 @@ def test_named_ss(): } +def p(*args): + # convenience for parametrize below + return pytest.param(*args, marks=pytest.mark.slycot) + + @pytest.mark.parametrize("fun, args, kwargs", [ - [ct.rss, (4, 1, 1), {}], - [ct.rss, (3, 2, 1), {}], - [ct.drss, (4, 1, 1), {}], - [ct.drss, (3, 2, 1), {}], + p(ct.rss, (4, 1, 1), {}), + p(ct.rss, (3, 2, 1), {}), + p(ct.drss, (4, 1, 1), {}), + p(ct.drss, (3, 2, 1), {}), [ct.FRD, ([1, 2, 3,], [1, 2, 3]), {}], [ct.NonlinearIOSystem, (lambda t, x, u, params: -x, None), {'inputs': 2, 'outputs':2, 'states':2}], - [ct.ss, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}], - [ct.ss, ([], [], [], 3), {}], # static system - [ct.StateSpace, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}], - [ct.tf, ([1, 2], [3, 4, 5]), {}], - [ct.tf, (2, 3), {}], # static system - [ct.TransferFunction, ([1, 2], [3, 4, 5]), {}], + p(ct.ss, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}), + p(ct.ss, ([], [], [], 3), {}), # static system + p(ct.StateSpace, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}), + p(ct.tf, ([1, 2], [3, 4, 5]), {}), + p(ct.tf, (2, 3), {}), # static system + p(ct.TransferFunction, ([1, 2], [3, 4, 5]), {}), ]) def test_io_naming(fun, args, kwargs): # Reset the ID counter to get uniform generic names @@ -165,8 +169,8 @@ def test_io_naming(fun, args, kwargs): # # Convert the system to state space and make sure labels transfer # - if ct.slycot_check() and not isinstance( - sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)): + if not isinstance(sys_r, + (ct.FrequencyResponseData, ct.NonlinearIOSystem)): sys_ss = ct.ss(sys_r) assert sys_ss != sys_r assert sys_ss.input_labels == input_labels @@ -185,9 +189,8 @@ def test_io_naming(fun, args, kwargs): # # Convert the system to a transfer function and make sure labels transfer # - if not isinstance( - sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)) and \ - ct.slycot_check(): + if not isinstance(sys_r, + (ct.FrequencyResponseData, ct.NonlinearIOSystem)): sys_tf = ct.tf(sys_r) assert sys_tf != sys_r assert sys_tf.input_labels == input_labels @@ -203,9 +206,8 @@ def test_io_naming(fun, args, kwargs): # # Convert the system to a StateSpace and make sure labels transfer # - if not isinstance( - sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)) and \ - ct.slycot_check(): + if not isinstance(sys_r, + (ct.FrequencyResponseData, ct.NonlinearIOSystem)): sys_lio = ct.ss(sys_r) assert sys_lio != sys_r assert sys_lio.input_labels == input_labels @@ -285,7 +287,7 @@ def test_duplicate_sysname(): # strip out matrix warnings warnings.filterwarnings("ignore", "the matrix subclass", category=PendingDeprecationWarning) - res = sys * sys + sys * sys # Generate a warning if the system is named sys = ct.rss(4, 1, 1) @@ -293,7 +295,7 @@ def test_duplicate_sysname(): sys.updfcn, sys.outfcn, inputs=sys.ninputs, outputs=sys.noutputs, states=sys.nstates, name='sys') with pytest.warns(UserWarning, match="duplicate object found"): - res = sys * sys + sys * sys # Finding signals @@ -332,10 +334,10 @@ def test_find_signals(): # Invalid signal names def test_invalid_signal_names(): with pytest.raises(ValueError, match="invalid signal name"): - sys = ct.rss(4, inputs="input.signal", outputs=1) + ct.rss(4, inputs="input.signal", outputs=1) with pytest.raises(ValueError, match="invalid system name"): - sys = ct.rss(4, inputs=1, outputs=1, name="system.subsys") + ct.rss(4, inputs=1, outputs=1, name="system.subsys") # Negative system spect @@ -367,8 +369,6 @@ def test_negative_system_spec(): # Named signal representations def test_named_signal_repr(): - from numpy import array - from ..iosys import NamedSignal sys = ct.rss( states=2, inputs=['u1', 'u2'], outputs=['y1', 'y2'], state_prefix='xi') @@ -376,6 +376,9 @@ def test_named_signal_repr(): for signal in ['inputs', 'outputs', 'states']: sig_orig = getattr(resp, signal) - sig_eval = eval(repr(sig_orig)) + sig_eval = eval(repr(sig_orig), + None, + {'array': np.array, + 'NamedSignal': ct.NamedSignal}) assert sig_eval.signal_labels == sig_orig.signal_labels assert sig_eval.trace_labels == sig_orig.trace_labels diff --git a/control/tests/nlsys_test.py b/control/tests/nlsys_test.py index 4b1a235c0..b14a619e0 100644 --- a/control/tests/nlsys_test.py +++ b/control/tests/nlsys_test.py @@ -98,7 +98,7 @@ def test_nlsys_impulse(): # Impulse_response (not implemented) with pytest.raises(ValueError, match="system must be LTI"): - resp_nl = ct.impulse_response(sys_nl, timepts) + ct.impulse_response(sys_nl, timepts) # Test nonlinear systems that are missing inputs or outputs diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 3b27ee27c..243a291d2 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -291,7 +291,7 @@ def test_nyquist_indent_default(indentsys): def test_nyquist_indent_dont(indentsys): # first value of default omega vector was 0.1, replaced by 0. for contour - # indent_radius is larger than 0.1 -> no extra quater circle around origin + # indent_radius is larger than 0.1 -> no extra quarter circle around origin with pytest.warns() as record: count, contour = ct.nyquist_response( indentsys, omega=[0, 0.2, 0.3, 0.4], indent_radius=.1007, @@ -406,15 +406,16 @@ def test_linestyle_checks(): # Set the line styles cplt = ct.nyquist_plot( sys, primary_style=[':', ':'], mirror_style=[':', ':']) - assert all([line.get_linestyle() == ':' for line in cplt.lines[0]]) + assert all([lines[0].get_linestyle() == ':' for lines in cplt.lines[0, :]]) # Set the line colors cplt = ct.nyquist_plot(sys, color='g') - assert all([line.get_color() == 'g' for line in cplt.lines[0]]) + assert all([line.get_color() == 'g' for line in cplt.lines[0, 0]]) # Turn off the mirror image cplt = ct.nyquist_plot(sys, mirror_style=False) - assert cplt.lines[0][2:] == [None, None] + assert cplt.lines[0, 2] == [None] + assert cplt.lines[0, 3] == [None] with pytest.raises(ValueError, match="invalid 'primary_style'"): ct.nyquist_plot(sys, primary_style=False) @@ -428,6 +429,7 @@ def test_linestyle_checks(): ct.nyquist_plot(sys, primary_style=':', mirror_style='-.') @pytest.mark.usefixtures("editsdefaults") +@pytest.mark.xfail(reason="updated code avoids warning") def test_nyquist_legacy(): ct.use_legacy_defaults('0.9.1') @@ -436,7 +438,7 @@ def test_nyquist_legacy(): sys = (0.02 * s**3 - 0.1 * s) / (s**4 + s**3 + s**2 + 0.25 * s + 0.04) with pytest.warns(UserWarning, match="indented contour may miss"): - response = ct.nyquist_plot(sys) + ct.nyquist_plot(sys) def test_discrete_nyquist(): # TODO: add tests to make sure plots make sense @@ -512,7 +514,7 @@ def test_nyquist_frd(): # Computing Nyquist response w/ different frequencies OK if given as a list nyqresp = ct.nyquist_response([sys1, sys2]) - cplt = nyqresp.plot() + nyqresp.plot() warnings.resetwarnings() @@ -522,10 +524,46 @@ def test_no_indent_pole(): sys = ((1 + 5/s)/(1 + 0.5/s))**2 # Double-Lag-Compensator with pytest.raises(RuntimeError, match="evaluate at a pole"): - resp = ct.nyquist_response( + ct.nyquist_response( sys, warn_encirclements=False, indent_direction='none') +def test_nyquist_rescale(): + sys = 2 * ct.tf([1], [1, 1]) * ct.tf([1], [1, 0])**2 + sys.name = 'How example' + + # Default case + resp = ct.nyquist_response(sys, indent_direction='left') + cplt = resp.plot(label='default [0.15]') + assert len(cplt.lines[0, 0]) == 2 + assert all([len(cplt.lines[0, i]) == 1 for i in range(1, 4)]) + + # Sharper corner + cplt = ct.nyquist_plot( + sys*4, indent_direction='left', + max_curve_magnitude=17, blend_fraction=0.05, label='fraction=0.05') + assert len(cplt.lines[0, 0]) == 2 + assert all([len(cplt.lines[0, i]) == 1 for i in range(1, 4)]) + + # More gradual corner + cplt = ct.nyquist_plot( + sys*0.25, indent_direction='left', + max_curve_magnitude=13, blend_fraction=0.25, label='fraction=0.25') + assert len(cplt.lines[0, 0]) == 2 + assert all([len(cplt.lines[0, i]) == 1 for i in range(1, 4)]) + + # No corner + cplt = ct.nyquist_plot( + sys*12, indent_direction='left', + max_curve_magnitude=19, blend_fraction=0, label='fraction=0') + assert len(cplt.lines[0, 0]) == 2 + assert all([len(cplt.lines[0, i]) == 1 for i in range(1, 4)]) + + # Bad value + with pytest.raises(ValueError, match="blend_fraction must be between"): + ct.nyquist_plot(sys, indent_direction='left', blend_fraction=1.2) + + if __name__ == "__main__": # # Interactive mode: generate plots for manual viewing @@ -566,8 +604,8 @@ def test_no_indent_pole(): sys = 3 * (s+6)**2 / (s * (s**2 + 1e-4 * s + 1)) plt.figure() ct.nyquist_plot(sys) - ct.nyquist_plot(sys, max_curve_magnitude=15) - ct.nyquist_plot(sys, indent_radius=1e-6, max_curve_magnitude=25) + ct.nyquist_plot(sys, max_curve_magnitude=10) + ct.nyquist_plot(sys, indent_radius=1e-6, max_curve_magnitude=20) print("Unusual Nyquist plot") sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1]) @@ -595,3 +633,29 @@ def test_no_indent_pole(): plt.figure() cplt = ct.nyquist_plot([sys, sys1, sys2]) cplt.set_plot_title("Mixed FRD, tf data") + + plt.figure() + print("Jon How example") + test_nyquist_rescale() + + # + # Save the figures in a PDF file for later comparisons + # + import subprocess + from matplotlib.backends.backend_pdf import PdfPages + from datetime import date + + # Create the file to store figures + try: + git_info = subprocess.check_output( + ['git', 'describe'], text=True).strip() + except subprocess.CalledProcessError: + git_info = 'UNKNOWN-REPO-INFO' + pdf = PdfPages( + f'nyquist_gallery-{git_info}-{date.today().isoformat()}.pdf') + + # Go through each figure and save it + for fignum in plt.get_fignums(): + pdf.savefig(plt.figure(fignum)) + + pdf.close() diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 4ea436515..fb3f4e716 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -12,7 +12,6 @@ import control as ct import control.optimal as opt import control.flatsys as flat -from control.tests.conftest import slycotonly from numpy.lib import NumpyVersion @@ -81,7 +80,7 @@ def test_finite_horizon_simple(method): sys, time, x0, cost, constraints, squeeze=True, trajectory_method=method, terminal_cost=cost) # include to match MPT3 formulation - t, u_openloop = res.time, res.inputs + _t, u_openloop = res.time, res.inputs np.testing.assert_almost_equal( u_openloop, [-1, -1, 0.1393, 0.3361, -5.204e-16], decimal=4) @@ -103,7 +102,7 @@ def test_finite_horizon_simple(method): # optimal control problem with terminal cost set to LQR "cost to go" # gives the same answer as LQR. # -@slycotonly +@pytest.mark.slycot def test_discrete_lqr(): # oscillator model defined in 2D # Source: https://www.mpt3.org/UI/RegulationProblem @@ -186,7 +185,6 @@ def test_mpc_iosystem_aircraft(): # compute the steady state values for a particular value of the input ud = np.array([0.8, -0.3]) xd = np.linalg.inv(np.eye(5) - A) @ B @ ud - yd = C @ xd # provide constraints on the system signals constraints = [opt.input_range_constraint(sys, [-5, -6], [5, 6])] @@ -264,7 +262,7 @@ def test_mpc_iosystem_continuous(): # Continuous time MPC controller not implemented with pytest.raises(NotImplementedError): - ctrl = opt.create_mpc_iosystem(sys, T, cost) + opt.create_mpc_iosystem(sys, T, cost) # Test various constraint combinations; need to use a somewhat convoluted @@ -315,7 +313,7 @@ def test_constraint_specification(constraint_list): # Compute optimal control and compare against MPT3 solution x0 = [4, 0] res = optctrl.compute_trajectory(x0, squeeze=True) - t, u_openloop = res.time, res.inputs + _t, u_openloop = res.time, res.inputs np.testing.assert_almost_equal( u_openloop, [-1, -1, 0.1393, 0.3361, -5.204e-16], decimal=3) @@ -352,7 +350,7 @@ def test_terminal_constraints(sys_args): # Find a path to the origin x0 = np.array([4, 3]) res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) - t, u1, x1 = res.time, res.inputs, res.states + _t, u1, x1 = res.time, res.inputs, res.states # Bug prior to SciPy 1.6 will result in incorrect results if NumpyVersion(sp.__version__) < '1.6.0': @@ -401,7 +399,7 @@ def test_terminal_constraints(sys_args): # Find a path to the origin res = optctrl.compute_trajectory( x0, squeeze=True, return_x=True, initial_guess=u1) - t, u2, x2 = res.time, res.inputs, res.states + _t, u2, x2 = res.time, res.inputs, res.states # Not all configurations are able to converge (?) if res.success: @@ -416,7 +414,7 @@ def test_terminal_constraints(sys_args): optctrl = opt.OptimalControlProblem( sys, time, cost, constraints, terminal_constraints=final_point) res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) - t, u3, x3 = res.time, res.inputs, res.states + _t, u3, x3 = res.time, res.inputs, res.states # Check the answers only if we converged if res.success: @@ -448,7 +446,7 @@ def test_optimal_logging(capsys): # Solve it, with logging turned on (with warning due to mixed constraints) with pytest.warns(sp.optimize.OptimizeWarning, match="Equality and inequality .* same element"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, input_constraint, terminal_cost=cost, terminal_constraints=state_constraint, log=True) @@ -513,21 +511,21 @@ def test_ocp_argument_errors(): # Trajectory constraints not in the right form with pytest.raises(TypeError, match="constraints must be a list"): - res = opt.solve_optimal_trajectory(sys, time, x0, cost, np.eye(2)) + opt.solve_optimal_trajectory(sys, time, x0, cost, np.eye(2)) # Terminal constraints not in the right form with pytest.raises(TypeError, match="constraints must be a list"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, constraints, terminal_constraints=np.eye(2)) # Initial guess in the wrong shape with pytest.raises(ValueError, match="initial guess is the wrong shape"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, constraints, initial_guess=np.zeros((4,1,1))) # Unrecognized arguments with pytest.raises(TypeError, match="unrecognized keyword"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, constraints, terminal_constraint=None) with pytest.raises(TypeError, match="unrecognized keyword"): @@ -541,21 +539,21 @@ def test_ocp_argument_errors(): # Unrecognized trajectory constraint type constraints = [(None, np.eye(3), [0, 0, 0], [0, 0, 0])] with pytest.raises(TypeError, match="unknown trajectory constraint type"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, trajectory_constraints=constraints) # Unrecognized terminal constraint type with pytest.raises(TypeError, match="unknown terminal constraint type"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, terminal_constraints=constraints) # Discrete time system checks: solve_ivp keywords not allowed sys = ct.rss(2, 1, 1, dt=True) with pytest.raises(TypeError, match="solve_ivp method, kwargs not allowed"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, solve_ivp_method='LSODA') with pytest.raises(TypeError, match="solve_ivp method, kwargs not allowed"): - res = opt.solve_optimal_trajectory( + opt.solve_optimal_trajectory( sys, time, x0, cost, solve_ivp_kwargs={'eps': 0.1}) @@ -629,7 +627,7 @@ def test_equality_constraints(): # Find a path to the origin x0 = np.array([4, 3]) res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) - t, u1, x1 = res.time, res.inputs, res.states + _t, u1, x1 = res.time, res.inputs, res.states # Bug prior to SciPy 1.6 will result in incorrect results if NumpyVersion(sp.__version__) < '1.6.0': @@ -649,7 +647,7 @@ def final_point_eval(x, u): # Find a path to the origin x0 = np.array([4, 3]) res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) - t, u2, x2 = res.time, res.inputs, res.states + _t, u2, x2 = res.time, res.inputs, res.states np.testing.assert_almost_equal(x2[:,-1], 0, decimal=4) np.testing.assert_almost_equal(u1, u2) np.testing.assert_almost_equal(x1, x2) @@ -732,8 +730,6 @@ def vehicle_output(t, x, u, params): initial_guess[0, :] = (xf[0] - x0[0]) / Tf # Steering = rate required to turn to proper slope in first segment - straight_seg_length = timepts[-2] - timepts[1] - curved_seg_length = (Tf - straight_seg_length)/2 approximate_angle = math.atan2(xf[1] - x0[1], xf[0] - x0[0]) initial_guess[1, 0] = approximate_angle / (timepts[1] - timepts[0]) initial_guess[1, -1] = -approximate_angle / (timepts[-1] - timepts[-2]) @@ -794,7 +790,7 @@ def test_oep_argument_errors(): # Unrecognized arguments with pytest.raises(TypeError, match="unrecognized keyword"): - res = opt.solve_optimal_estimate(sys, timepts, Y, U, cost, unknown=True) + opt.solve_optimal_estimate(sys, timepts, Y, U, cost, unknown=True) with pytest.raises(TypeError, match="unrecognized keyword"): oep = opt.OptimalEstimationProblem(sys, timepts, cost, unknown=True) @@ -807,4 +803,4 @@ def test_oep_argument_errors(): # Incorrect number of signals with pytest.raises(ValueError, match="incorrect length"): oep = opt.OptimalEstimationProblem(sys, timepts, cost) - mhe = oep.create_mhe_iosystem(estimate_labels=['x1', 'x2', 'x3']) + oep.create_mhe_iosystem(estimate_labels=['x1', 'x2', 'x3']) diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 4d7c8e6eb..22b73e0da 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -5,10 +5,9 @@ import pytest import numpy from control import ss, passivity, tf, sample_system, parallel, feedback -from control.tests.conftest import cvxoptonly from control.exception import ControlArgument, ControlDimension -pytestmark = cvxoptonly +pytestmark = pytest.mark.cvxopt def test_ispassive_ctime(): diff --git a/control/tests/phaseplot_test.py b/control/tests/phaseplot_test.py index 6ea6411dc..ac5249948 100644 --- a/control/tests/phaseplot_test.py +++ b/control/tests/phaseplot_test.py @@ -12,6 +12,7 @@ import warnings from math import pi +import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pytest @@ -19,7 +20,6 @@ import control as ct import control.phaseplot as pp from control import phase_plot -from control.tests.conftest import mplcleanup # Legacy tests @@ -123,11 +123,11 @@ def test_helper_functions(func, args, kwargs): sys = ct.nlsys( lambda t, x, u, params: [x[0] - 3*x[1], -3*x[0] + x[1]], states=2, inputs=0) - out = func(sys, [-1, 1, -1, 1], *args, **kwargs) + _out = func(sys, [-1, 1, -1, 1], *args, **kwargs) # Test with function rhsfcn = lambda t, x: sys.dynamics(t, x, 0, {}) - out = func(rhsfcn, [-1, 1, -1, 1], *args, **kwargs) + _out = func(rhsfcn, [-1, 1, -1, 1], *args, **kwargs) @pytest.mark.usefixtures('mplcleanup') @@ -138,29 +138,39 @@ def invpend_ode(t, x, m=0, l=0, b=0, g=0): # Use callable form, with parameters (if not correct, will get /0 error) ct.phase_plane_plot( - invpend_ode, [-5, 5, -2, 2], params={'args': (1, 1, 0.2, 1)}) + invpend_ode, [-5, 5, -2, 2], params={'args': (1, 1, 0.2, 1)}, + plot_streamlines=True) # Linear I/O system ct.phase_plane_plot( - ct.ss([[0, 1], [-1, -1]], [[0], [1]], [[1, 0]], 0)) + ct.ss([[0, 1], [-1, -1]], [[0], [1]], [[1, 0]], 0), + plot_streamlines=True) @pytest.mark.usefixtures('mplcleanup') def test_phaseplane_errors(): with pytest.raises(ValueError, match="invalid grid specification"): - ct.phase_plane_plot(ct.rss(2, 1, 1), gridspec='bad') + ct.phase_plane_plot(ct.rss(2, 1, 1), gridspec='bad', + plot_streamlines=True) with pytest.raises(ValueError, match="unknown grid type"): - ct.phase_plane_plot(ct.rss(2, 1, 1), gridtype='bad') + ct.phase_plane_plot(ct.rss(2, 1, 1), gridtype='bad', + plot_streamlines=True) with pytest.raises(ValueError, match="system must be planar"): - ct.phase_plane_plot(ct.rss(3, 1, 1)) + ct.phase_plane_plot(ct.rss(3, 1, 1), + plot_streamlines=True) with pytest.raises(ValueError, match="params must be dict with key"): def invpend_ode(t, x, m=0, l=0, b=0, g=0): return (x[1], -b/m*x[1] + (g*l/m) * np.sin(x[0])) ct.phase_plane_plot( - invpend_ode, [-5, 5, 2, 2], params={'stuff': (1, 1, 0.2, 1)}) + invpend_ode, [-5, 5, 2, 2], params={'stuff': (1, 1, 0.2, 1)}, + plot_streamlines=True) + + with pytest.raises(ValueError, match="gridtype must be 'meshgrid' when using streamplot"): + ct.phase_plane_plot(ct.rss(2, 1, 1), plot_streamlines=False, + plot_streamplot=True, gridtype='boxgrid') # Warning messages for invalid solutions: nonlinear spring mass system sys = ct.nlsys( @@ -171,14 +181,85 @@ def invpend_ode(t, x, m=0, l=0, b=0, g=0): UserWarning, match=r"initial_state=\[.*\], solve_ivp failed"): ct.phase_plane_plot( sys, [-12, 12, -10, 10], 15, gridspec=[2, 9], - plot_separatrices=False) + plot_separatrices=False, plot_streamlines=True) # Turn warnings off with warnings.catch_warnings(): warnings.simplefilter("error") ct.phase_plane_plot( sys, [-12, 12, -10, 10], 15, gridspec=[2, 9], - plot_separatrices=False, suppress_warnings=True) + plot_streamlines=True, plot_separatrices=False, + suppress_warnings=True) + +@pytest.mark.usefixtures('mplcleanup') +def test_phase_plot_zorder(): + # some of these tests are a bit akward since the streamlines and separatrices + # are stored in the same list, so we separate them by color + key_color = "tab:blue" # must not be 'k', 'r', 'b' since they are used by separatrices + + def get_zorders(cplt): + max_zorder = lambda items: max([line.get_zorder() for line in items]) + assert isinstance(cplt.lines[0], list) + streamline_lines = [line for line in cplt.lines[0] if line.get_color() == key_color] + separatrice_lines = [line for line in cplt.lines[0] if line.get_color() != key_color] + streamlines = max_zorder(streamline_lines) if streamline_lines else None + separatrices = max_zorder(separatrice_lines) if separatrice_lines else None + assert cplt.lines[1] == None or isinstance(cplt.lines[1], mpl.quiver.Quiver) + quiver = cplt.lines[1].get_zorder() if cplt.lines[1] else None + assert cplt.lines[2] == None or isinstance(cplt.lines[2], list) + equilpoints = max_zorder(cplt.lines[2]) if cplt.lines[2] else None + assert cplt.lines[3] == None or isinstance(cplt.lines[3], mpl.streamplot.StreamplotSet) + streamplot = max(cplt.lines[3].lines.get_zorder(), cplt.lines[3].arrows.get_zorder()) if cplt.lines[3] else None + return streamlines, quiver, streamplot, separatrices, equilpoints + + def assert_orders(streamlines, quiver, streamplot, separatrices, equilpoints): + print(streamlines, quiver, streamplot, separatrices, equilpoints) + if streamlines is not None: + assert streamlines < separatrices < equilpoints + if quiver is not None: + assert quiver < separatrices < equilpoints + if streamplot is not None: + assert streamplot < separatrices < equilpoints + + def sys(t, x): + return np.array([4*x[1], -np.sin(4*x[0])]) + + # ensure correct zordering for all three flow types + res_streamlines = ct.phase_plane_plot(sys, plot_streamlines=dict(color=key_color)) + assert_orders(*get_zorders(res_streamlines)) + res_vectorfield = ct.phase_plane_plot(sys, plot_vectorfield=True) + assert_orders(*get_zorders(res_vectorfield)) + res_streamplot = ct.phase_plane_plot(sys, plot_streamplot=True) + assert_orders(*get_zorders(res_streamplot)) + + # ensure that zorder can still be overwritten + res_reversed = ct.phase_plane_plot(sys, plot_streamlines=dict(color=key_color, zorder=50), plot_vectorfield=dict(zorder=40), + plot_streamplot=dict(zorder=30), plot_separatrices=dict(zorder=20), plot_equilpoints=dict(zorder=10)) + streamlines, quiver, streamplot, separatrices, equilpoints = get_zorders(res_reversed) + assert streamlines > quiver > streamplot > separatrices > equilpoints + + +@pytest.mark.usefixtures('mplcleanup') +def test_stream_plot_magnitude(): + def sys(t, x): + return np.array([4*x[1], -np.sin(4*x[0])]) + + # plt context with linewidth + with plt.rc_context({'lines.linewidth': 4}): + res = ct.phase_plane_plot(sys, plot_streamplot=dict(vary_linewidth=True)) + linewidths = res.lines[3].lines.get_linewidths() + # linewidths are scaled to be between 0.25 and 2 times default linewidth + # but the extremes may not exist if there is no line at that point + assert min(linewidths) < 2 and max(linewidths) > 7 + + # make sure changing the colormap works + res = ct.phase_plane_plot(sys, plot_streamplot=dict(vary_color=True, cmap='viridis')) + assert res.lines[3].lines.get_cmap().name == 'viridis' + res = ct.phase_plane_plot(sys, plot_streamplot=dict(vary_color=True, cmap='turbo')) + assert res.lines[3].lines.get_cmap().name == 'turbo' + + # make sure changing the norm at least doesn't throw an error + ct.phase_plane_plot(sys, plot_streamplot=dict(vary_color=True, norm=mpl.colors.LogNorm())) @pytest.mark.usefixtures('mplcleanup') @@ -190,7 +271,7 @@ def test_basic_phase_plots(savefigs=False): plt.figure() axis_limits = [-1, 1, -1, 1] T = 8 - ct.phase_plane_plot(sys, axis_limits, T) + ct.phase_plane_plot(sys, axis_limits, T, plot_streamlines=True) if savefigs: plt.savefig('phaseplot-dampedosc-default.png') @@ -203,7 +284,7 @@ def invpend_update(t, x, u, params): ct.phase_plane_plot( invpend, [-2*pi, 2*pi, -2, 2], 5, gridtype='meshgrid', gridspec=[5, 8], arrows=3, - plot_separatrices={'gridspec': [12, 9]}, + plot_separatrices={'gridspec': [12, 9]}, plot_streamlines=True, params={'m': 1, 'l': 1, 'b': 0.2, 'g': 1}) plt.xlabel(r"$\theta$ [rad]") plt.ylabel(r"$\dot\theta$ [rad/sec]") @@ -218,7 +299,8 @@ def oscillator_update(t, x, u, params): oscillator_update, states=2, inputs=0, name='nonlinear oscillator') plt.figure() - ct.phase_plane_plot(oscillator, [-1.5, 1.5, -1.5, 1.5], 0.9) + ct.phase_plane_plot(oscillator, [-1.5, 1.5, -1.5, 1.5], 0.9, + plot_streamlines=True) pp.streamlines( oscillator, np.array([[0, 0]]), 1.5, gridtype='circlegrid', gridspec=[0.5, 6], dir='both') @@ -228,6 +310,18 @@ def oscillator_update(t, x, u, params): if savefigs: plt.savefig('phaseplot-oscillator-helpers.png') + plt.figure() + ct.phase_plane_plot( + invpend, [-2*pi, 2*pi, -2, 2], + plot_streamplot=dict(vary_color=True, vary_density=True), + gridspec=[60, 20], params={'m': 1, 'l': 1, 'b': 0.2, 'g': 1} + ) + plt.xlabel(r"$\theta$ [rad]") + plt.ylabel(r"$\dot\theta$ [rad/sec]") + + if savefigs: + plt.savefig('phaseplot-invpend-streamplot.png') + if __name__ == "__main__": # diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py index 438732b84..64bbdee3e 100644 --- a/control/tests/pzmap_test.py +++ b/control/tests/pzmap_test.py @@ -111,10 +111,10 @@ def test_pzmap_raises(): sys1 = ct.rss(2, 1, 1) sys2 = sys1.sample(0.1) with pytest.raises(ValueError, match="incompatible time bases"): - pzdata = ct.pole_zero_plot([sys1, sys2], grid=True) + ct.pole_zero_plot([sys1, sys2], grid=True) with pytest.warns(UserWarning, match="axis already exists"): - fig, ax = plt.figure(), plt.axes() + _fig, ax = plt.figure(), plt.axes() ct.pole_zero_plot(sys1, ax=ax, grid='empty') diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 2e74f8649..4d3a08206 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -134,7 +134,7 @@ def test_root_locus_zoom(self): ax_rlocus.set_xlim((-10.813628105112421, 14.760795435937652)) ax_rlocus.set_ylim((-35.61713798641108, 33.879716621220311)) plt.get_current_fig_manager().toolbar.mode = 'zoom rect' - _RLClickDispatcher(event, system, fig, ax_rlocus, '-') + _RLClickDispatcher(event, system, fig, ax_rlocus, '-') # noqa: F821 zoom_x = ax_rlocus.lines[-2].get_data()[0][0:5] zoom_y = ax_rlocus.lines[-2].get_data()[1][0:5] @@ -161,7 +161,6 @@ def test_rlocus_default_wn(self): # that will take a long time to do the calculation (minutes). # import scipy as sp - import signal # Define a system that exhibits this behavior sys = ct.tf(*sp.signal.zpk2tf( diff --git a/control/tests/robust_test.py b/control/tests/robust_test.py index fc9c9570d..8434ea6cd 100644 --- a/control/tests/robust_test.py +++ b/control/tests/robust_test.py @@ -5,12 +5,11 @@ from control import append, minreal, ss, tf from control.robust import augw, h2syn, hinfsyn, mixsyn -from control.tests.conftest import slycotonly class TestHinf: - @slycotonly + @pytest.mark.slycot def testHinfsyn(self): """Test hinfsyn""" p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) @@ -32,7 +31,7 @@ def testHinfsyn(self): class TestH2: - @slycotonly + @pytest.mark.slycot def testH2syn(self): """Test h2syn""" p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) @@ -71,7 +70,7 @@ def siso_almost_equal(self, g, h): "sys 2:\n" "{}".format(maxnum, g, h)) - @slycotonly + @pytest.mark.slycot def testSisoW1(self): """SISO plant with S weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -88,7 +87,7 @@ def testSisoW1(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @slycotonly + @pytest.mark.slycot def testSisoW2(self): """SISO plant with KS weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -105,7 +104,7 @@ def testSisoW2(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @slycotonly + @pytest.mark.slycot def testSisoW3(self): """SISO plant with T weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -122,7 +121,7 @@ def testSisoW3(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @slycotonly + @pytest.mark.slycot def testSisoW123(self): """SISO plant with all weights""" g = ss([-1.], [1.], [1.], [1.]) @@ -149,7 +148,7 @@ def testSisoW123(self): # u->v should be -g self.siso_almost_equal(-g, p[3, 1]) - @slycotonly + @pytest.mark.slycot def testMimoW1(self): """MIMO plant with S weighting""" g = ss([[-1., -2], [-3, -4]], @@ -181,7 +180,7 @@ def testMimoW1(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @slycotonly + @pytest.mark.slycot def testMimoW2(self): """MIMO plant with KS weighting""" g = ss([[-1., -2], [-3, -4]], @@ -213,7 +212,7 @@ def testMimoW2(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @slycotonly + @pytest.mark.slycot def testMimoW3(self): """MIMO plant with T weighting""" g = ss([[-1., -2], [-3, -4]], @@ -245,7 +244,7 @@ def testMimoW3(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @slycotonly + @pytest.mark.slycot def testMimoW123(self): """MIMO plant with all weights""" g = ss([[-1., -2], [-3, -4]], @@ -307,7 +306,7 @@ def testMimoW123(self): self.siso_almost_equal(-g[1, 0], p[7, 2]) self.siso_almost_equal(-g[1, 1], p[7, 3]) - @slycotonly + @pytest.mark.slycot def testErrors(self): """Error cases handled""" from control import augw, ss @@ -330,7 +329,7 @@ class TestMixsyn: """Test control.robust.mixsyn""" # it's a relatively simple wrapper; compare results with augw, hinfsyn - @slycotonly + @pytest.mark.slycot def testSiso(self): """mixsyn with SISO system""" # Skogestad+Postlethwaite, Multivariable Feedback Control, 1st Ed., Example 2.11 diff --git a/control/tests/slycot_convert_test.py b/control/tests/slycot_convert_test.py index 25beeb908..2739a4cf1 100644 --- a/control/tests/slycot_convert_test.py +++ b/control/tests/slycot_convert_test.py @@ -7,7 +7,6 @@ import pytest from control import bode, rss, ss, tf -from control.tests.conftest import slycotonly numTests = 5 maxStates = 10 @@ -21,7 +20,7 @@ def fixedseed(): np.random.seed(0) -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures("fixedseed") class TestSlycot: """Test Slycot system conversion diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index ebf531546..d0230fb18 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -12,11 +12,10 @@ import control as ct from control import poles, rss, ss, tf from control.exception import ControlDimension, ControlSlycot, \ - ControlArgument, slycot_check + ControlArgument from control.mateqn import care, dare from control.statefbk import (ctrb, obsv, place, place_varga, lqr, dlqr, gram, place_acker) -from control.tests.conftest import slycotonly @pytest.fixture @@ -128,7 +127,7 @@ def testCtrbObsvDuality(self): Wo = np.transpose(obsv(A, C)) np.testing.assert_array_almost_equal(Wc,Wo) - @slycotonly + @pytest.mark.slycot def testGramWc(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -144,7 +143,7 @@ def testGramWc(self): Wc = gram(sysd, 'c') np.testing.assert_array_almost_equal(Wc, Wctrue) - @slycotonly + @pytest.mark.slycot def testGramWc2(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -161,7 +160,7 @@ def testGramWc2(self): Wc = gram(sysd, 'c') np.testing.assert_array_almost_equal(Wc, Wctrue) - @slycotonly + @pytest.mark.slycot def testGramRc(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -177,7 +176,7 @@ def testGramRc(self): Rc = gram(sysd, 'cf') np.testing.assert_array_almost_equal(Rc, Rctrue) - @slycotonly + @pytest.mark.slycot def testGramWo(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -193,7 +192,7 @@ def testGramWo(self): Wo = gram(sysd, 'o') np.testing.assert_array_almost_equal(Wo, Wotrue) - @slycotonly + @pytest.mark.slycot def testGramWo2(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -209,7 +208,7 @@ def testGramWo2(self): Wo = gram(sysd, 'o') np.testing.assert_array_almost_equal(Wo, Wotrue) - @slycotonly + @pytest.mark.slycot def testGramRo(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -318,7 +317,7 @@ def testPlace(self): with pytest.raises(ValueError): place(A, B, P_repeated) - @slycotonly + @pytest.mark.slycot def testPlace_varga_continuous(self): """ Check that we can place eigenvalues for dtime=False @@ -345,7 +344,7 @@ def testPlace_varga_continuous(self): self.checkPlaced(P, P_placed) - @slycotonly + @pytest.mark.slycot def testPlace_varga_continuous_partial_eigs(self): """ Check that we are able to use the alpha parameter to only place @@ -365,7 +364,7 @@ def testPlace_varga_continuous_partial_eigs(self): # No guarantee of the ordering, so sort them self.checkPlaced(P_expected, P_placed) - @slycotonly + @pytest.mark.slycot def testPlace_varga_discrete(self): """ Check that we can place poles using dtime=True (discrete time) @@ -379,7 +378,7 @@ def testPlace_varga_discrete(self): # No guarantee of the ordering, so sort them self.checkPlaced(P, P_placed) - @slycotonly + @pytest.mark.slycot def testPlace_varga_discrete_partial_eigs(self): """" Check that we can only assign a single eigenvalue in the discrete @@ -412,27 +411,30 @@ def check_DLQR(self, K, S, poles, Q, R): np.testing.assert_array_almost_equal(K, K_expected) np.testing.assert_array_almost_equal(poles, poles_expected) - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) + @pytest.mark.parametrize("method", + [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_LQR_integrator(self, method): - if method == 'slycot' and not slycot_check(): - return A, B, Q, R = (np.array([[X]]) for X in [0., 1., 10., 2.]) K, S, poles = lqr(A, B, Q, R, method=method) self.check_LQR(K, S, poles, Q, R) - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) + @pytest.mark.parametrize("method", + [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_LQR_3args(self, method): - if method == 'slycot' and not slycot_check(): - return sys = ss(0., 1., 1., 0.) Q, R = (np.array([[X]]) for X in [10., 2.]) K, S, poles = lqr(sys, Q, R, method=method) self.check_LQR(K, S, poles, Q, R) - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) + @pytest.mark.parametrize("method", + [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_DLQR_3args(self, method): - if method == 'slycot' and not slycot_check(): - return dsys = ss(0., 1., 1., 0., .1) Q, R = (np.array([[X]]) for X in [10., 2.]) K, S, poles = dlqr(dsys, Q, R, method=method) @@ -449,12 +451,12 @@ def test_lqr_badmethod(self, cdlqr): with pytest.raises(ControlArgument, match="Unknown method"): K, S, poles = cdlqr(A, B, Q, R, method='nosuchmethod') + @pytest.mark.noslycot @pytest.mark.parametrize("cdlqr", [lqr, dlqr]) def test_lqr_slycot_not_installed(self, cdlqr): A, B, Q, R = 0, 1, 10, 2 - if not slycot_check(): - with pytest.raises(ControlSlycot, match="Can't find slycot"): - K, S, poles = cdlqr(A, B, Q, R, method='slycot') + with pytest.raises(ControlSlycot, match="Can't find slycot"): + K, S, poles = cdlqr(A, B, Q, R, method='slycot') @pytest.mark.xfail(reason="warning not implemented") def testLQR_warning(self): @@ -538,7 +540,13 @@ def testDLQR_warning(self): with pytest.warns(UserWarning): (K, S, E) = dlqr(A, B, Q, R, N) - def test_care(self): + @pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) + @pytest.mark.parametrize("method", + [pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) + def test_care(self, have_slycot, method): """Test stabilizing and anti-stabilizing feedback, continuous""" A = np.diag([1, -1]) B = np.identity(2) @@ -547,19 +555,19 @@ def test_care(self): S = np.zeros((2, 2)) E = np.identity(2) - X, L, G = care(A, B, Q, R, S, E, stabilizing=True) + X, L, G = care(A, B, Q, R, S, E, stabilizing=True, method=method) assert np.all(np.real(L) < 0) - if slycot_check(): - X, L, G = care(A, B, Q, R, S, E, stabilizing=False) + if have_slycot and method=='slycot': + X, L, G = care(A, B, Q, R, S, E, stabilizing=False, method=method) assert np.all(np.real(L) > 0) else: with pytest.raises(ControlArgument, match="'scipy' not valid"): - X, L, G = care(A, B, Q, R, S, E, stabilizing=False) + X, L, G = care(A, B, Q, R, S, E, stabilizing=False, method=method) @pytest.mark.parametrize( "stabilizing", - [True, pytest.param(False, marks=slycotonly)]) + [True, pytest.param(False, marks=pytest.mark.slycot)]) def test_dare(self, stabilizing): """Test stabilizing and anti-stabilizing feedback, discrete""" A = np.diag([0.5, 2]) @@ -782,7 +790,10 @@ def test_statefbk_iosys_unused(self): np.testing.assert_allclose(clsys0_lin.A, clsys2_lin.A) - def test_lqr_integral_continuous(self): + @pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) + def test_lqr_integral_continuous(self, have_slycot): # Generate a continuous-time system for testing sys = ct.rss(4, 4, 2, strictly_proper=True) sys.C = np.eye(4) # reset output to be full state @@ -844,7 +855,7 @@ def test_lqr_integral_continuous(self): assert all(np.real(clsys.poles()) < 0) # Make sure controller infinite zero frequency gain - if slycot_check(): + if have_slycot: ctrl_tf = tf(ctrl) assert abs(ctrl_tf(1e-9)[0][0]) > 1e6 assert abs(ctrl_tf(1e-9)[1][1]) > 1e6 @@ -860,7 +871,7 @@ def test_lqr_integral_discrete(self): K, _, _ = ct.lqr( sys, np.eye(sys.nstates + nintegrators), np.eye(sys.ninputs), integral_action=C_int) - Kp, Ki = K[:, :sys.nstates], K[:, sys.nstates:] + Kp, _Ki = K[:, :sys.nstates], K[:, sys.nstates:] # Create an I/O system for the controller ctrl, clsys = ct.create_statefbk_iosystem( @@ -1237,20 +1248,10 @@ def test_create_statefbk_errors(): def test_create_statefbk_params(unicycle): - # Speeds and angles at which to compute the gains - speeds = [1, 5, 10] - angles = np.linspace(0, pi/2, 4) - points = list(itertools.product(speeds, angles)) - - # Gains for each speed (using LQR controller) Q = np.identity(unicycle.nstates) R = np.identity(unicycle.ninputs) gain, _, _ = ct.lqr(unicycle.linearize([0, 0, 0], [5, 0]), Q, R) - # - # Schedule on desired speed and angle - # - # Create a linear controller ctrl, clsys = ct.create_statefbk_iosystem(unicycle, gain) assert [k for k in ctrl.params.keys()] == [] @@ -1271,3 +1272,23 @@ def test_create_statefbk_params(unicycle): assert [k for k in clsys.params.keys()] == ['K', 'a', 'b'] assert clsys.params['a'] == 2 assert clsys.params['b'] == 1 + + +@pytest.mark.parametrize('ny, nu', [(1, 1), (2, 2), (2, 1)]) +@pytest.mark.parametrize( + 'method', [ + place, place_acker, + pytest.param(place_varga, marks=pytest.mark.slycot)]) +def test_place_variants(ny, nu, method): + sys = ct.rss(states=2, inputs=nu, outputs=ny) + desired_poles = -np.arange(1, sys.nstates + 1, 1) + + if method == place_acker and sys.ninputs != 1: + with pytest.raises(np.linalg.LinAlgError, match="must be square"): + K = method(sys.A, sys.B, desired_poles) + else: + K = method(sys.A, sys.B, desired_poles) + + placed_poles = np.linalg.eigvals(sys.A - sys.B @ K) + np.testing.assert_array_almost_equal( + np.sort(desired_poles), np.sort(placed_poles)) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 2f3aa512f..9b3c677fe 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -8,7 +8,6 @@ """ import operator -import platform import numpy as np import pytest @@ -23,10 +22,7 @@ from control.statesp import StateSpace, _convert_to_statespace, \ _rss_generate, _statesp_defaults, drss, linfnorm, rss, ss, tf2ss from control.xferfcn import TransferFunction, ss2tf - -from .conftest import assert_tf_close_coeff, editsdefaults, \ - ignore_future_warning, slycotonly - +from .conftest import assert_tf_close_coeff class TestStateSpace: """Tests for the StateSpace class.""" @@ -140,11 +136,12 @@ def test_constructor(self, sys322ABCD, dt, argfun): ((np.ones((3, 3)), np.ones((3, 2)), np.ones((2, 3)), np.ones((2, 3))), ValueError, r"Incompatible dimensions of D matrix; expected \(2, 2\)"), + (([1j], 2, 3, 0), TypeError, "real number, not 'complex'"), ]) def test_constructor_invalid(self, args, exc, errmsg): """Test invalid input to StateSpace() constructor""" - with pytest.raises(exc, match=errmsg) as w: + with pytest.raises(exc, match=errmsg): StateSpace(*args) with pytest.raises(exc, match=errmsg): ss(*args) @@ -232,7 +229,7 @@ def test_zero_empty(self): sys = _convert_to_statespace(TransferFunction([1], [1, 2, 1])) np.testing.assert_array_equal(sys.zeros(), np.array([])) - @slycotonly + @pytest.mark.slycot def test_zero_siso(self, sys222): """Evaluate the zeros of a SISO system.""" # extract only first input / first output system of sys222. This system is denoted sys111 @@ -262,7 +259,7 @@ def test_zero_mimo_sys222_square(self, sys222): true_z = np.sort([-10.568501, 3.368501]) np.testing.assert_array_almost_equal(z, true_z) - @slycotonly + @pytest.mark.slycot def test_zero_mimo_sys623_non_square(self, sys623): """Evaluate the zeros of a non square MIMO system.""" @@ -409,7 +406,7 @@ def test_add_sub_mimo_siso(self): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize( "left, right, expected", [ @@ -484,7 +481,7 @@ def test_mul_mimo_siso(self, left, right, expected): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize( "left, right, expected", [ @@ -559,7 +556,7 @@ def test_rmul_mimo_siso(self, left, right, expected): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize("power", [0, 1, 3, -3]) @pytest.mark.parametrize("sysname", ["sys222", "sys322"]) def test_pow(self, request, sysname, power): @@ -578,7 +575,7 @@ def test_pow(self, request, sysname, power): np.testing.assert_allclose(expected.C, result.C) np.testing.assert_allclose(expected.D, result.D) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize("order", ["left", "right"]) @pytest.mark.parametrize("sysname", ["sys121", "sys222", "sys322"]) def test_pow_inv(self, request, sysname, order): @@ -602,7 +599,7 @@ def test_pow_inv(self, request, sysname, order): # Check that the output is the same as the input np.testing.assert_allclose(R.outputs, U) - @slycotonly + @pytest.mark.slycot def test_truediv(self, sys222, sys322): """Test state space truediv""" for sys in [sys222, sys322]: @@ -621,7 +618,7 @@ def test_truediv(self, sys222, sys322): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot def test_rtruediv(self, sys222, sys322): """Test state space rtruediv""" for sys in [sys222, sys322]: @@ -722,7 +719,7 @@ def test_freq_resp(self): mag, phase, omega = sys.freqresp(true_omega) np.testing.assert_almost_equal(mag, true_mag) - @slycotonly + @pytest.mark.slycot def test_minreal(self): """Test a minreal model reduction.""" # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] @@ -1444,6 +1441,7 @@ def test_html_repr(gmats, ref, dt, dtref, repr_type, num_format, editsdefaults): dt_html = dtref.format(dt=dt, fmt=defaults['statesp.latex_num_format']) ref_html = ref[refkey].format(dt=dt_html) assert g._repr_html_() == ref_html + assert g._repr_html_() == g._repr_markdown_() @pytest.mark.parametrize( @@ -1516,7 +1514,7 @@ def dt_siso(self, request): name, systype, sysargs, dt, refgpeak, reffpeak = request.param return ct.c2d(systype(*sysargs), dt), refgpeak, reffpeak - @slycotonly + @pytest.mark.slycot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_ct_siso(self, ct_siso): sys, refgpeak, reffpeak = ct_siso @@ -1524,7 +1522,7 @@ def test_linfnorm_ct_siso(self, ct_siso): np.testing.assert_allclose(gpeak, refgpeak) np.testing.assert_allclose(fpeak, reffpeak) - @slycotonly + @pytest.mark.slycot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_dt_siso(self, dt_siso): sys, refgpeak, reffpeak = dt_siso @@ -1533,7 +1531,7 @@ def test_linfnorm_dt_siso(self, dt_siso): np.testing.assert_allclose(gpeak, refgpeak) np.testing.assert_allclose(fpeak, reffpeak) - @slycotonly + @pytest.mark.slycot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_ct_mimo(self, ct_siso): siso, refgpeak, reffpeak = ct_siso @@ -1604,10 +1602,13 @@ def test_tf2ss_unstable(method): np.testing.assert_allclose(tf_poles, ss_poles, rtol=1e-4) -def test_tf2ss_mimo(): +@pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) +def test_tf2ss_mimo(have_slycot): sys_tf = ct.tf([[[1], [1, 1, 1]]], [[[1, 1, 1], [1, 2, 1]]]) - if ct.slycot_check(): + if have_slycot: sys_ss = ct.ss(sys_tf) np.testing.assert_allclose( np.sort(sys_tf.poles()), np.sort(sys_ss.poles())) diff --git a/control/tests/stochsys_test.py b/control/tests/stochsys_test.py index 4c0d9665d..20e799643 100644 --- a/control/tests/stochsys_test.py +++ b/control/tests/stochsys_test.py @@ -6,7 +6,7 @@ import control as ct import control.optimal as opt -from control import lqe, dlqe, rss, drss, tf, ss, ControlArgument, slycot_check +from control import lqe, dlqe, rss, tf, ControlArgument from math import log, pi # Utility function to check LQE answer @@ -27,11 +27,10 @@ def check_DLQE(L, P, poles, G, QN, RN): np.testing.assert_almost_equal(L, L_expected) np.testing.assert_almost_equal(poles, poles_expected) -@pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) +@pytest.mark.parametrize("method", [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_LQE(method): - if method == 'slycot' and not slycot_check(): - return - A, G, C, QN, RN = (np.array([[X]]) for X in [0., .1, 1., 10., 2.]) L, P, poles = lqe(A, G, C, QN, RN, method=method) check_LQE(L, P, poles, G, QN, RN) @@ -78,11 +77,10 @@ def test_lqe_call_format(cdlqe): with pytest.raises(ct.ControlArgument, match="LTI system must be"): L, P, E = cdlqe(sys_tf, Q, R) -@pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) +@pytest.mark.parametrize("method", [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_DLQE(method): - if method == 'slycot' and not slycot_check(): - return - A, G, C, QN, RN = (np.array([[X]]) for X in [0., .1, 1., 10., 2.]) L, P, poles = dlqe(A, G, C, QN, RN, method=method) check_DLQE(L, P, poles, G, QN, RN) @@ -225,26 +223,25 @@ def test_estimator_iosys_ctime(sys_args): def test_estimator_errors(): sys = ct.drss(4, 2, 2, strictly_proper=True) - P0 = np.eye(sys.nstates) QN = np.eye(sys.ninputs) RN = np.eye(sys.noutputs) with pytest.raises(TypeError, match="unrecognized keyword"): - estim = ct.create_estimator_iosystem(sys, QN, RN, unknown=True) + ct.create_estimator_iosystem(sys, QN, RN, unknown=True) with pytest.raises(ct.ControlArgument, match=".* system must be a linear"): sys_tf = ct.tf([1], [1, 1], dt=True) - estim = ct.create_estimator_iosystem(sys_tf, QN, RN) + ct.create_estimator_iosystem(sys_tf, QN, RN) with pytest.raises(ValueError, match="output must be full state"): C = np.eye(2, 4) - estim = ct.create_estimator_iosystem(sys, QN, RN, C=C) + ct.create_estimator_iosystem(sys, QN, RN, C=C) with pytest.raises(ValueError, match="output is the wrong size"): sys_fs = ct.drss(4, 4, 2, strictly_proper=True) sys_fs.C = np.eye(4) C = np.eye(1, 4) - estim = ct.create_estimator_iosystem(sys_fs, QN, RN, C=C) + ct.create_estimator_iosystem(sys_fs, QN, RN, C=C) def test_white_noise(): @@ -430,7 +427,6 @@ def test_mhe(): V = np.array( [0 if i % 2 == 1 else 1 if i % 4 == 0 else -1 for i, t in enumerate(timepts)]).reshape(1, -1) * 0.1 - W = np.sin(timepts / dt) * 1e-3 # Create a moving horizon estimator traj_cost = opt.gaussian_likelihood_cost(sys, Rv, Rw) @@ -482,7 +478,6 @@ def test_indices(ctrl_indices, dist_indices): sysm = ct.ss(sys.A, sys.B[:, ctrl_idx], sys.C, sys.D[:, ctrl_idx]) # Set the simulation time based on the slowest system pole - from math import log T = 10 # Generate a system response with no disturbances diff --git a/control/tests/timeplot_test.py b/control/tests/timeplot_test.py index 9525c7e02..ea0a290c9 100644 --- a/control/tests/timeplot_test.py +++ b/control/tests/timeplot_test.py @@ -7,8 +7,6 @@ import pytest import control as ct -from control.tests.conftest import mplcleanup, slycotonly - # Detailed test of (almost) all functionality # @@ -238,7 +236,7 @@ def test_axes_setup(): sys_3x1 = ct.rss(4, 3, 1) -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures('mplcleanup') def test_legend_map(): sys_mimo = ct.tf2ss( @@ -312,15 +310,15 @@ def test_combine_time_responses(): with pytest.raises(ValueError, match="must have the same number"): resp = ct.step_response(ct.rss(4, 2, 3), timepts) - combresp = ct.combine_time_responses([resp1, resp]) + ct.combine_time_responses([resp1, resp]) with pytest.raises(ValueError, match="trace labels does not match"): - combresp = ct.combine_time_responses( + ct.combine_time_responses( [resp1, resp2], trace_labels=["T1", "T2", "T3"]) with pytest.raises(ValueError, match="must have the same time"): resp = ct.step_response(ct.rss(4, 2, 3), timepts/2) - combresp6 = ct.combine_time_responses([resp1, resp]) + ct.combine_time_responses([resp1, resp]) @pytest.mark.parametrize("resp_fcn", [ @@ -372,7 +370,7 @@ def test_list_responses(resp_fcn): assert cplt.lines[row, col][1].get_color() == 'tab:orange' -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures('mplcleanup') def test_linestyles(): # Check to make sure we can change line styles @@ -415,13 +413,10 @@ def test_timeplot_trace_labels(resp_fcn): # Figure out the expected shape of the system match resp_fcn: case ct.step_response | ct.impulse_response: - shape = (2, 2) kwargs = {} case ct.initial_response: - shape = (2, 1) kwargs = {} case ct.forced_response | ct.input_output_response: - shape = (4, 1) # outputs and inputs both plotted T = np.linspace(0, 10) U = [np.sin(T), np.cos(T)] kwargs = {'T': T, 'U': U} diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index aa4987209..16ee01a3d 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -5,12 +5,10 @@ import numpy as np import pytest -import scipy as sp import control as ct from control import StateSpace, TransferFunction, c2d, isctime, ss2tf, tf2ss -from control.exception import pandas_check, slycot_check -from control.tests.conftest import slycotonly +from control.exception import pandas_check from control.timeresp import _default_time_vector, _ideal_tfinal_and_dt, \ forced_response, impulse_response, initial_response, step_info, \ step_response @@ -455,7 +453,7 @@ def test_step_info(self, tsystem, systype, time_2d, yfinal): @pytest.mark.parametrize( "tsystem", ['mimo_ss_step_matlab', - pytest.param('mimo_tf_step_info', marks=slycotonly)], + pytest.param('mimo_tf_step_info', marks=pytest.mark.slycot)], indirect=["tsystem"]) def test_step_info_mimo(self, tsystem, systype, yfinal): """Test step info for MIMO systems.""" @@ -800,7 +798,7 @@ def test_lsim_double_integrator(self, u, x0, xtrue): np.testing.assert_array_almost_equal(yout, ytrue, decimal=6) - @slycotonly + @pytest.mark.slycot def test_step_robustness(self): "Test robustness os step_response against denomiantors: gh-240" # Create 2 input, 2 output system @@ -903,9 +901,9 @@ def test_default_timevector_functions_d(self, fun, dt): "siso_dtf2", "siso_ss2_dtnone", # undetermined timebase "mimo_ss2", # MIMO - pytest.param("mimo_tf2", marks=slycotonly), + pytest.param("mimo_tf2", marks=pytest.mark.slycot), "mimo_dss1", - pytest.param("mimo_dtf1", marks=slycotonly), + pytest.param("mimo_dtf1", marks=pytest.mark.slycot), ], indirect=True) @pytest.mark.parametrize("fun", [step_response, @@ -1034,30 +1032,41 @@ def test_time_series_data_convention_2D(self, tsystem): assert y.ndim == 1 # SISO returns "scalar" output assert t.shape == y.shape # Allows direct plotting of output + def p(*args): + # convenience for parametrize below + return pytest.param(*args, marks=pytest.mark.slycot) + @pytest.mark.usefixtures("editsdefaults") - @pytest.mark.parametrize("fcn", [ct.ss, ct.tf]) - @pytest.mark.parametrize("nstate, nout, ninp, squeeze, shape1, shape2", [ - # state out in squeeze in/out out-only - [1, 1, 1, None, (8,), (8,)], - [2, 1, 1, True, (8,), (8,)], - [3, 1, 1, False, (1, 1, 8), (1, 8)], - [3, 2, 1, None, (2, 1, 8), (2, 8)], - [4, 2, 1, True, (2, 8), (2, 8)], - [5, 2, 1, False, (2, 1, 8), (2, 8)], - [3, 1, 2, None, (1, 2, 8), (1, 8)], - [4, 1, 2, True, (2, 8), (8,)], - [5, 1, 2, False, (1, 2, 8), (1, 8)], - [4, 2, 2, None, (2, 2, 8), (2, 8)], - [5, 2, 2, True, (2, 2, 8), (2, 8)], - [6, 2, 2, False, (2, 2, 8), (2, 8)], + @pytest.mark.parametrize("fcn, nstate, nout, ninp, squeeze, shape1, shape2", [ + # fcn, state out in squeeze in/out out-only + [ct.ss, 1, 1, 1, None, (8,), (8,)], + [ct.ss, 2, 1, 1, True, (8,), (8,)], + [ct.ss, 3, 1, 1, False, (1, 1, 8), (1, 8)], + [ct.ss, 3, 2, 1, None, (2, 1, 8), (2, 8)], + [ct.ss, 4, 2, 1, True, (2, 8), (2, 8)], + [ct.ss, 5, 2, 1, False, (2, 1, 8), (2, 8)], + [ct.ss, 3, 1, 2, None, (1, 2, 8), (1, 8)], + [ct.ss, 4, 1, 2, True, (2, 8), (8,)], + [ct.ss, 5, 1, 2, False, (1, 2, 8), (1, 8)], + [ct.ss, 4, 2, 2, None, (2, 2, 8), (2, 8)], + [ct.ss, 5, 2, 2, True, (2, 2, 8), (2, 8)], + [ct.ss, 6, 2, 2, False, (2, 2, 8), (2, 8)], + [ct.tf, 1, 1, 1, None, (8,), (8,)], + [ct.tf, 2, 1, 1, True, (8,), (8,)], + [ct.tf, 3, 1, 1, False, (1, 1, 8), (1, 8)], + p(ct.tf, 3, 2, 1, None, (2, 1, 8), (2, 8)), + p(ct.tf, 4, 2, 1, True, (2, 8), (2, 8)), + p(ct.tf, 5, 2, 1, False, (2, 1, 8), (2, 8)), + p(ct.tf, 3, 1, 2, None, (1, 2, 8), (1, 8)), + p(ct.tf, 4, 1, 2, True, (2, 8), (8,)), + p(ct.tf, 5, 1, 2, False, (1, 2, 8), (1, 8)), + p(ct.tf, 4, 2, 2, None, (2, 2, 8), (2, 8)), + p(ct.tf, 5, 2, 2, True, (2, 2, 8), (2, 8)), + p(ct.tf, 6, 2, 2, False, (2, 2, 8), (2, 8)), ]) def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): # Define the system - if fcn == ct.tf and (nout > 1 or ninp > 1) and not slycot_check(): - pytest.skip("Conversion of MIMO systems to transfer functions " - "requires slycot.") - else: - sys = fcn(ct.rss(nstate, nout, ninp, strictly_proper=True)) + sys = fcn(ct.rss(nstate, nout, ninp, strictly_proper=True)) # Generate the time and input vectors tvec = np.linspace(0, 1, 8) @@ -1198,7 +1207,6 @@ def test_squeeze_0_8_4(self, nstate, nout, ninp, squeeze, shape): # Generate system, time, and input vectors sys = ct.rss(nstate, nout, ninp, strictly_proper=True) tvec = np.linspace(0, 1, 8) - uvec =np.ones((sys.ninputs, 1)) @ np.reshape(np.sin(tvec), (1, 8)) _, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze) assert yvec.shape == shape @@ -1238,7 +1246,7 @@ def test_response_transpose( assert x.shape == (T.size, sys.nstates) -@pytest.mark.skipif(not pandas_check(), reason="pandas not installed") +@pytest.mark.pandas def test_to_pandas(): # Create a SISO time response sys = ct.rss(2, 1, 1) @@ -1323,7 +1331,7 @@ def test_no_pandas(): # Convert to pandas with pytest.raises(ImportError, match="pandas"): - df = resp.to_pandas() + resp.to_pandas() # https://github.com/python-control/python-control/issues/1014 @@ -1429,40 +1437,40 @@ def test_timeresp_aliases(): # Aliases resp_short = ct.input_output_response(sys, timepts, 1, X0=[1, 1]) - np.testing.assert_allclose(resp_long.states, resp_posn.states) + np.testing.assert_allclose(resp_long.states, resp_short.states) # Legacy with pytest.warns(PendingDeprecationWarning, match="legacy"): resp_legacy = ct.input_output_response(sys, timepts, 1, x0=[1, 1]) - np.testing.assert_allclose(resp_long.states, resp_posn.states) + np.testing.assert_allclose(resp_long.states, resp_legacy.states) # Check for multiple values: full keyword and alias with pytest.raises(TypeError, match="multiple"): - resp_multiple = ct.input_output_response( + ct.input_output_response( sys, timepts, 1, initial_state=[1, 2], X0=[1, 1]) # Check for multiple values: positional and keyword with pytest.raises(TypeError, match="multiple"): - resp_multiple = ct.input_output_response( + ct.input_output_response( sys, timepts, 1, [1, 2], initial_state=[1, 1]) # Check for multiple values: positional and alias with pytest.raises(TypeError, match="multiple"): - resp_multiple = ct.input_output_response( + ct.input_output_response( sys, timepts, 1, [1, 2], X0=[1, 1]) # Make sure that LTI functions check for keywords with pytest.raises(TypeError, match="unrecognized keyword"): - resp = ct.forced_response(sys, timepts, 1, unknown=True) + ct.forced_response(sys, timepts, 1, unknown=True) with pytest.raises(TypeError, match="unrecognized keyword"): - resp = ct.impulse_response(sys, timepts, unknown=True) + ct.impulse_response(sys, timepts, unknown=True) with pytest.raises(TypeError, match="unrecognized keyword"): - resp = ct.initial_response(sys, timepts, [1, 2], unknown=True) + ct.initial_response(sys, timepts, [1, 2], unknown=True) with pytest.raises(TypeError, match="unrecognized keyword"): - resp = ct.step_response(sys, timepts, unknown=True) + ct.step_response(sys, timepts, unknown=True) with pytest.raises(TypeError, match="unrecognized keyword"): - info = ct.step_info(sys, timepts, unknown=True) + ct.step_info(sys, timepts, unknown=True) diff --git a/control/tests/trdata_test.py b/control/tests/trdata_test.py index 7d0c20e7a..b84369d72 100644 --- a/control/tests/trdata_test.py +++ b/control/tests/trdata_test.py @@ -214,7 +214,7 @@ def test_response_copy(): # Unknown keyword with pytest.raises(TypeError, match="unrecognized keywords"): - response_bad_kw = response_mimo(input=0) + response_mimo(input=0) def test_trdata_labels(): diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 87c852395..a9be040ab 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -14,7 +14,7 @@ isdtime, reset_defaults, rss, sample_system, set_defaults, ss, ss2tf, tf, tf2ss, zpk) from control.statesp import _convert_to_statespace -from control.tests.conftest import assert_tf_close_coeff, slycotonly +from control.tests.conftest import assert_tf_close_coeff from control.xferfcn import _convert_to_transfer_function @@ -49,6 +49,10 @@ def test_constructor_bad_input_type(self): [[4, 5], [6, 7]]], [[[6, 7], [4, 5]], [[2, 3]]]) + + with pytest.raises(TypeError, match="unsupported data type"): + ct.tf([1j], [1, 2, 3]) + # good input TransferFunction([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], @@ -993,7 +997,7 @@ def test_minreal_4(self): np.testing.assert_allclose(hm.num[0][0], hr.num[0][0]) np.testing.assert_allclose(hr.dt, hm.dt) - @slycotonly + @pytest.mark.slycot def test_state_space_conversion_mimo(self): """Test conversion of a single input, two-output state-space system against the same TF""" diff --git a/control/timeresp.py b/control/timeresp.py index bd549589a..3c49d213e 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -551,10 +551,10 @@ def outputs(self): def states(self): """Time response state vector. - Time evolution of the state vector, indexed indexed by either the - state and time (if only a single trace is given) or the state, trace, - and time (for multiple traces). See `TimeResponseData.squeeze` - for a description of how this can be modified using the `squeeze` + Time evolution of the state vector, indexed by either the state and + time (if only a single trace is given) or the state, trace, and + time (for multiple traces). See `TimeResponseData.squeeze` for a + description of how this can be modified using the `squeeze` keyword. Input and output signal names can be used to index the data in @@ -616,9 +616,9 @@ def inputs(self): def _legacy_states(self): """Time response state vector (legacy version). - Time evolution of the state vector, indexed indexed by either the - state and time (if only a single trace is given) or the state, - trace, and time (for multiple traces). + Time evolution of the state vector, indexed by either the state and + time (if only a single trace is given) or the state, trace, and + time (for multiple traces). The `legacy_states` property is not affected by the `squeeze` keyword and hence it will always have these dimensions. diff --git a/control/xferfcn.py b/control/xferfcn.py index 16d7c5054..8e51534d7 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1350,7 +1350,7 @@ def _c2d_matched(sysC, Ts, **kwargs): zpoles[idx] = z pregainden[idx] = 1 - z zgain = np.multiply.reduce(pregainnum) / np.multiply.reduce(pregainden) - gain = sysC.dcgain() / zgain + gain = sysC.dcgain() / zgain.real sysDnum, sysDden = zpk2tf(zzeros, zpoles, gain) return TransferFunction(sysDnum, sysDden, Ts, **kwargs) @@ -1954,6 +1954,7 @@ def _clean_part(data, name=""): """ valid_types = (int, float, complex, np.number) + unsupported_types = (complex, np.complexfloating) valid_collection = (list, tuple, ndarray) if isinstance(data, np.ndarray) and data.ndim == 2 and \ @@ -1998,8 +1999,11 @@ def _clean_part(data, name=""): for i in range(out.shape[0]): for j in range(out.shape[1]): for k in range(len(out[i, j])): - if isinstance(out[i, j][k], (int, np.int32, np.int64)): + if isinstance(out[i, j][k], (int, np.integer)): out[i, j][k] = float(out[i, j][k]) + elif isinstance(out[i, j][k], unsupported_types): + raise TypeError( + f"unsupported data type: {type(out[i, j][k])}") return out diff --git a/doc/Makefile b/doc/Makefile index 493fd7da5..4029dd70f 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,7 +1,7 @@ # Makefile for python-control Sphinx documentation # RMM, 15 Jan 2025 -FIGS = figures/classes.pdf +FIGS = figures/classes.svg RST_FIGS = figures/flatsys-steering-compare.png \ figures/iosys-predprey-open.png \ figures/timeplot-servomech-combined.png \ diff --git a/doc/_templates/extended-class-template.rst b/doc/_templates/extended-class-template.rst new file mode 100644 index 000000000..6e1e4ccd7 --- /dev/null +++ b/doc/_templates/extended-class-template.rst @@ -0,0 +1,9 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :no-members: + :show-inheritance: + :no-inherited-members: + :no-special-members: diff --git a/doc/classes.rst b/doc/classes.rst index 0ab508a3a..000761724 100644 --- a/doc/classes.rst +++ b/doc/classes.rst @@ -14,10 +14,10 @@ systems (both linear time-invariant and nonlinear). They are usually created from factory functions such as :func:`tf` and :func:`ss`, so the user should normally not need to instantiate these directly. -The following figure illustrates the relationship between the classes. +The following figure illustrates the relationship between the classes: -.. image:: figures/classes.pdf - :width: 800 +.. figure:: figures/classes.svg + :width: 640 :align: center .. autosummary:: @@ -34,6 +34,17 @@ The following figure illustrates the relationship between the classes. InterconnectedSystem LinearICSystem +The time response of an input/output system is represented using a +special :class:`NamedSignal` class that allows the individual signal +elements to be access using signal names in place of integer offsets: + +.. autosummary:: + :toctree: generated/ + :template: extended-class-template.rst + :nosignatures: + + NamedSignal + Response and Plotting Classes ============================= @@ -95,5 +106,5 @@ operations: optimal.OptimalEstimationProblem optimal.OptimalEstimationResult -More informaton on the functions used to create these classes can be +More information on the functions used to create these classes can be found in the :ref:`nonlinear-systems` chapter. diff --git a/doc/develop.rst b/doc/develop.rst index c9b6738a8..3ab4f8a94 100644 --- a/doc/develop.rst +++ b/doc/develop.rst @@ -110,8 +110,8 @@ Filenames * Source files are lower case, usually less than 10 characters (and 8 or less is better). -* Unit tests (in `control/tests/`) are of the form `module_test.py` or - `module_function.py`. +* Unit tests (in `control/tests/`) are of the form `module_test.py`, + `module_functionality_test.py`, or `functionality_test.py`. Class names diff --git a/doc/examples.rst b/doc/examples.rst index 2937fecab..b8d71807a 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -39,6 +39,7 @@ other sources. examples/mrac_siso_lyapunov examples/markov examples/era_msd + examples/disk_margins Jupyter Notebooks ================= diff --git a/doc/examples/disk_margins.py b/doc/examples/disk_margins.py new file mode 120000 index 000000000..a1dbcb7b1 --- /dev/null +++ b/doc/examples/disk_margins.py @@ -0,0 +1 @@ +../../examples/disk_margins.py \ No newline at end of file diff --git a/doc/examples/disk_margins.rst b/doc/examples/disk_margins.rst new file mode 100644 index 000000000..e7938f4ac --- /dev/null +++ b/doc/examples/disk_margins.rst @@ -0,0 +1,19 @@ +Disk margin example +------------------------------------------ + +This example demonstrates the use of the `disk_margins` routine +to compute robust stability margins for a feedback system, i.e., +variation in gain and phase one or more loops. The SISO examples +are drawn from the published paper and the MIMO example is the +"spinning satellite" example from the MathWorks documentation. + +Code +.... +.. literalinclude:: disk_margins.py + :language: python + :linenos: + +Notes +..... +1. The environment variable `PYCONTROL_TEST_EXAMPLES` is used for +testing to turn off plotting of the outputs. diff --git a/doc/figures/Makefile b/doc/figures/Makefile index 1ca54b372..26bdf22c2 100644 --- a/doc/figures/Makefile +++ b/doc/figures/Makefile @@ -2,7 +2,7 @@ # RMM, 26 Dec 2024 # List of figures that need to be created (first figure generated is OK) -FIGS = classes.pdf +FIGS = classes.svg # Location of the control package SRCDIR = ../.. @@ -12,5 +12,5 @@ all: $(FIGS) clean: /bin/rm -f $(FIGS) -classes.pdf: classes.fig - fig2dev -Lpdf $< $@ +classes.svg: classes.fig + fig2dev -Lsvg $< $@ diff --git a/doc/figures/classes.fig b/doc/figures/classes.fig index 4e63b8bff..17c112cc7 100644 --- a/doc/figures/classes.fig +++ b/doc/figures/classes.fig @@ -1,4 +1,5 @@ -#FIG 3.2 Produced by xfig version 3.2.8b +#FIG 3.2 Produced by xfig version 3.2.9 +#encoding: UTF-8 Landscape Center Inches @@ -37,12 +38,13 @@ Single 2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 1 0 1.00 60.00 90.00 6525 1950 7050 2550 -4 1 1 50 -1 16 12 0.0000 4 210 2115 4050 3675 InterconnectedSystem\001 -4 1 1 50 -1 16 12 0.0000 4 165 1605 7950 3675 TransferFunction\001 -4 1 1 50 -1 0 12 0.0000 4 150 345 7050 2775 LTI\001 -4 1 1 50 -1 16 12 0.0000 4 210 1830 5175 2775 NonlinearIOSystem\001 -4 1 1 50 -1 16 12 0.0000 4 210 1095 6150 3675 StateSpace\001 -4 1 1 50 -1 16 12 0.0000 4 210 1500 5175 4575 LinearICSystem\001 -4 2 1 50 -1 16 12 0.0000 4 210 1035 3375 3225 FlatSystem\001 -4 0 1 50 -1 16 12 0.0000 4 165 420 8400 3225 FRD\001 -4 1 1 50 -1 16 12 0.0000 4 210 1770 6300 1875 InputOutputSystem\001 +4 1 1 50 -1 16 12 0.0000 4 191 1958 4050 3675 InterconnectedSystem\001 +4 1 1 50 -1 16 12 0.0000 4 151 1496 7950 3675 TransferFunction\001 +4 1 1 50 -1 0 12 0.0000 4 133 305 7050 2775 LTI\001 +4 1 1 50 -1 16 12 0.0000 4 191 1705 5175 2775 NonlinearIOSystem\001 +4 1 1 50 -1 16 12 0.0000 4 190 1016 6150 3675 StateSpace\001 +4 1 1 50 -1 16 12 0.0000 4 191 1394 5175 4575 LinearICSystem\001 +4 2 1 50 -1 16 12 0.0000 4 191 970 3375 3225 FlatSystem\001 +4 0 1 50 -1 16 12 0.0000 4 145 384 8400 3225 FRD\001 +4 1 1 50 -1 16 12 0.0000 4 191 1681 6300 1875 InputOutputSystem\001 +4 1 7 50 -1 16 12 0.0000 4 22 21 5175 4800 .\001 diff --git a/doc/figures/classes.pdf b/doc/figures/classes.pdf deleted file mode 100644 index 2c51b0193..000000000 Binary files a/doc/figures/classes.pdf and /dev/null differ diff --git a/doc/figures/classes.svg b/doc/figures/classes.svg new file mode 100644 index 000000000..98fedb596 --- /dev/null +++ b/doc/figures/classes.svg @@ -0,0 +1,151 @@ + + + + + + + +. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +InterconnectedSystem + +TransferFunction + +LTI + +NonlinearIOSystem + +StateSpace + +LinearICSystem + +FlatSystem + +FRD + +InputOutputSystem + + + + + + + + + + + diff --git a/doc/figures/freqplot-nyquist-custom.png b/doc/figures/freqplot-nyquist-custom.png index 5cd2c19d0..29e7e476d 100644 Binary files a/doc/figures/freqplot-nyquist-custom.png and b/doc/figures/freqplot-nyquist-custom.png differ diff --git a/doc/figures/freqplot-nyquist-default.png b/doc/figures/freqplot-nyquist-default.png index c511509fa..968987d2c 100644 Binary files a/doc/figures/freqplot-nyquist-default.png and b/doc/figures/freqplot-nyquist-default.png differ diff --git a/doc/figures/phaseplot-dampedosc-default.png b/doc/figures/phaseplot-dampedosc-default.png index 69a28254f..3841fce83 100644 Binary files a/doc/figures/phaseplot-dampedosc-default.png and b/doc/figures/phaseplot-dampedosc-default.png differ diff --git a/doc/figures/phaseplot-invpend-meshgrid.png b/doc/figures/phaseplot-invpend-meshgrid.png index 118c364be..0d73f967c 100644 Binary files a/doc/figures/phaseplot-invpend-meshgrid.png and b/doc/figures/phaseplot-invpend-meshgrid.png differ diff --git a/doc/figures/phaseplot-oscillator-helpers.png b/doc/figures/phaseplot-oscillator-helpers.png index 829d94d74..ab1bb62a3 100644 Binary files a/doc/figures/phaseplot-oscillator-helpers.png and b/doc/figures/phaseplot-oscillator-helpers.png differ diff --git a/doc/functions.rst b/doc/functions.rst index db9a5a08c..8432f7fcf 100644 --- a/doc/functions.rst +++ b/doc/functions.rst @@ -103,6 +103,7 @@ Phase plane plots phaseplot.separatrices phaseplot.streamlines phaseplot.vectorfield + phaseplot.streamplot Frequency Response @@ -141,6 +142,7 @@ Frequency domain analysis: bandwidth dcgain + disk_margins linfnorm margin stability_margins diff --git a/doc/intro.rst b/doc/intro.rst index e1e5fb8e6..0054bb668 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -126,7 +126,7 @@ some things to keep in mind: * Vectors and matrices used as arguments to functions can be written using lists, with commas required between elements and column - vectors implemented as nested list . So [1 2 3] must be written as + vectors implemented as nested lists. So [1 2 3] must be written as [1, 2, 3] and matrices are written using 2D nested lists, e.g., [[1, 2], [3, 4]]. * Functions that in MATLAB would return variable numbers of values @@ -150,12 +150,12 @@ This documentation has a number of notional conventions and functionality: Manual, which contains documentation for all functions, classes, configurable default parameters, and other detailed information. -* Class, functions, and methods with additional documentation appear +* Classes, functions, and methods with additional documentation appear in a bold, code font that link to the Reference Manual. Example: `ss`. * Links to other sections appear in blue. Example: :ref:`nonlinear-systems`. -* Parameters appear in a (non-bode) code font, as do code fragments. +* Parameters appear in a (non-bold) code font, as do code fragments. Example: `omega`. * Example code is contained in code blocks that can be copied using diff --git a/doc/linear.rst b/doc/linear.rst index b7b8f7137..200260303 100644 --- a/doc/linear.rst +++ b/doc/linear.rst @@ -39,7 +39,7 @@ of linear time-invariant (LTI) systems: y &= C x + D u where :math:`u` is the input, :math:`y` is the output, and :math:`x` -is the state. +is the state. All vectors and matrices must be real-valued. To create a state space system, use the :func:`ss` function: @@ -94,7 +94,8 @@ transfer functions {b_0 s^n + b_1 s^{n-1} + \cdots + b_n}, where :math:`n` is greater than or equal to :math:`m` for a proper -transfer function. Improper transfer functions are also allowed. +transfer function. Improper transfer functions are also allowed. All +coefficients must be real-valued. To create a transfer function, use the :func:`tf` function:: @@ -530,6 +531,8 @@ equilibrium values (thereby keeping the input/output gain unchanged at zero frequency ["DC"]). +.. _displaying-lti-system-information: + Displaying LTI System Information ================================= diff --git a/doc/phaseplot.rst b/doc/phaseplot.rst index 324b3baf4..8c014415f 100644 --- a/doc/phaseplot.rst +++ b/doc/phaseplot.rst @@ -12,7 +12,7 @@ functionality is supported by a set of mapping functions that are part of the `phaseplot` module. The default method for generating a phase plane plot is to provide a -2D dynamical system along with a range of coordinates and time limit: +2D dynamical system along with a range of coordinates in phase space: .. testsetup:: phaseplot @@ -27,8 +27,7 @@ The default method for generating a phase plane plot is to provide a sys_update, states=['position', 'velocity'], inputs=0, name='damped oscillator') axis_limits = [-1, 1, -1, 1] - T = 8 - ct.phase_plane_plot(sys, axis_limits, T) + ct.phase_plane_plot(sys, axis_limits) .. testcode:: phaseplot :hide: @@ -39,12 +38,12 @@ The default method for generating a phase plane plot is to provide a .. image:: figures/phaseplot-dampedosc-default.png :align: center -By default, the plot includes streamlines generated from starting -points on limits of the plot, with arrows showing the flow of the -system, as well as any equilibrium points for the system. A variety +By default the plot includes streamlines infered from function values +on a grid, equilibrium points and separatrices if they exist. A variety of options are available to modify the information that is plotted, -including plotting a grid of vectors instead of streamlines and -turning on and off various features of the plot. +including plotting a grid of vectors instead of streamlines, plotting +streamlines from arbitrary starting points and turning on and off +various features of the plot. To illustrate some of these possibilities, consider a phase plane plot for an inverted pendulum system, which is created using a mesh grid: @@ -62,9 +61,7 @@ an inverted pendulum system, which is created using a mesh grid: invpend = ct.nlsys(invpend_update, states=2, inputs=1, name='invpend') ct.phase_plane_plot( - invpend, [-2 * np.pi, 2 * np.pi, -2, 2], 5, - gridtype='meshgrid', gridspec=[5, 8], arrows=3, - plot_equilpoints={'gridspec': [12, 9]}, + invpend, [-2 * np.pi, 2 * np.pi, -2, 2], params={'m': 1, 'l': 1, 'b': 0.2, 'g': 1}) plt.xlabel(r"$\theta$ [rad]") plt.ylabel(r"$\dot\theta$ [rad/sec]") @@ -79,16 +76,17 @@ an inverted pendulum system, which is created using a mesh grid: This figure shows several features of more complex phase plane plots: multiple equilibrium points are shown, with saddle points showing -separatrices, and streamlines generated along a 5x8 mesh of initial -conditions. At each mesh point, a streamline is created that goes 5 time -units forward and backward in time. A separate grid specification is used -to find equilibrium points and separatrices (since the course grid spacing -of 5x8 does not find all possible equilibrium points). Together, the -multiple features in the phase plane plot give a good global picture of the +separatrices, and streamlines generated generated from a rectangular +25x25 grid (default) of function evaluations. Together, the multiple +features in the phase plane plot give a good global picture of the topological structure of solutions of the dynamical system. -Phase plots can be built up by hand using a variety of helper functions that -are part of the :mod:`phaseplot` (pp) module: +Phase plots can be built up by hand using a variety of helper +functions that are part of the :mod:`phaseplot` (pp) module. For more +precise control, the streamlines can also generated by integrating the +system forwards or backwards in time from a set of initial +conditions. The initial conditions can be chosen on a rectangular +grid, rectangual boundary, circle or from an arbitrary set of points. .. testcode:: phaseplot :hide: @@ -105,7 +103,8 @@ are part of the :mod:`phaseplot` (pp) module: oscillator = ct.nlsys( oscillator_update, states=2, inputs=0, name='nonlinear oscillator') - ct.phase_plane_plot(oscillator, [-1.5, 1.5, -1.5, 1.5], 0.9) + ct.phase_plane_plot(oscillator, [-1.5, 1.5, -1.5, 1.5], 0.9, + plot_streamlines=True) pp.streamlines( oscillator, np.array([[0, 0]]), 1.5, gridtype='circlegrid', gridspec=[0.5, 6], dir='both') @@ -128,6 +127,7 @@ The following helper functions are available: phaseplot.equilpoints phaseplot.separatrices phaseplot.streamlines + phaseplot.streamplot phaseplot.vectorfield The :func:`phase_plane_plot` function calls these helper functions diff --git a/doc/releases.rst b/doc/releases.rst index 88a76775a..7bc5c0f46 100644 --- a/doc/releases.rst +++ b/doc/releases.rst @@ -27,6 +27,7 @@ the ability to index systems and signal using signal labels. .. toctree:: :maxdepth: 1 + releases/0.10.2-notes releases/0.10.1-notes releases/0.10.0-notes diff --git a/doc/releases/0.10.1-notes.rst b/doc/releases/0.10.1-notes.rst index dd0939021..8b99100f2 100644 --- a/doc/releases/0.10.1-notes.rst +++ b/doc/releases/0.10.1-notes.rst @@ -2,8 +2,8 @@ .. _version-0.10.1: -Version 0.10.1 Release Notes (current) --------------------------------------- +Version 0.10.1 Release Notes +---------------------------- * Released: 17 Aug 2024 * `GitHub release page diff --git a/doc/releases/0.10.2-notes.rst b/doc/releases/0.10.2-notes.rst new file mode 100644 index 000000000..175fdaff2 --- /dev/null +++ b/doc/releases/0.10.2-notes.rst @@ -0,0 +1,240 @@ +.. currentmodule:: control + +.. _version-0.10.2: + +Version 0.10.2 Release Notes (current) +-------------------------------------- + +* Released: date of release +* `GitHub release page + `_ + +This release includes numerous bug fixes and improvements, with major +changes such as a substantial reorganization of the documentation into +a User Guide and Reference Manual, more consistent and complete +docstrings, and support for referencing signals and subsystems by name +as well as by index. Phase plane plots now use matplotlib’s +`streamplot` for better visuals. New functions include `combine_tf` +and `split_tf` for MIMO/SISO conversion and `disk_margins` for +stability analysis. Additional improvements include consistent keyword +usage, expanded LTI system methods for plotting and responses, better +error messages, and legacy aliases to maintain backward compatibility. + +This version of `python-control` requires Python 3.10 or higher, NumPy +1.23 or higher (2.x recommended), and SciPy 1.8 or higher. + + +New classes, functions, and methods +................................... + +The following new classes, functions, and methods have been added in +this release: + +* `find_operating_point`: this function replaces (with a legacy alias) + the `find_eqpt` function and now returns an `OperatingPoint` object + containing the information about the operating point. + +* `combine_tf` and `split_tf`: these two new functions allow you to + create an MIMO transfer function from SISO transfer functions and + vice versa. + +* `create_statefbk_iosystem` now allows the creation of state feedback + controllers using a "reference gain" pattern (:math:`u = k_\text{f}\, + r - K x`) in addition to the default "trajectory generation" pattern + (:math:`u = u_\text{d} - K(x - x_\text{d})`). + +* `disk_margins`: compute disk-based stability margins for SISO and + MIMO systems. + +* `model_reduction`: allow specific states, inputs, or outputs to be + either eliminated or retained. + +* `place_acker`: renamed version of `acker` (which is still accessible + via an alias). + + +Bug fixes +......... + +The following bugs have been fixed in this release: + +* `phase_plane_plot`: fixed a bug in which the return value was + returning a sublist of lines rather than just a list of lines in + `cplt.lines`. + +* Processing of the timebase parameter (`dt`) for I/O systems is now + handled uniformly across all I/O system factory functions. This + affected the `zpk` function, which was defaulting to a discrete time + system to have timebase None instead of 0. + +* Multiplying (*), adding (+), or subtracting (-) a constant from any + (MIMO) LTI object now acts element-wise (same as ndarray's). This + fixes a bug where multiplying a MIMO LTI system by a constant was + multiplying by a matrix filled with the constant rather than a + diagonal matrix (scaled identity). + +* Fixed a bug where specifying an FRD system with fewer than 4 + frequency points was generating an error because the default + settings try to set up a smooth (interpolating) response and the + default degree of the fit was 3. + +* Fixed some bugs where computing poles and zeros of transfer + functions could generate spurious error messages about unsafe + casting of complex numbers to real numbers. + +* `TimeResponseData.to_pandas`: multi-trace data (e.g., the output + from a MIMO step response) was not being processed correctly. A new + column 'trace' is now generated for multi-trace responses. + +* Fixed a bug where where some arguments to `nyquist_plot` were not + being processed correctly and generated errors about unrecognized + keywords. + +* Updated `ctrb` and `obsv` to handle 1D `B` or `C` matrix correctly. + +* `bode_plot`: Fixed missing plot title when `display_margin` keyword + was used. + +* `singular_values_plot`: color cycling was not working correctly when + a list of systems or responses was provided. + +* `nyquist_plot`: The `lines` parameter of the `ControlPlot` object + now matches the documentation. A 2D array is returned with the + first index corresponding to the response (system) index and the + second index corresponding to the segment type (primary, mirror x + unscaled, scaled). + +* Fix some internal bugs that cropped up when using NumPy 2.3.1 but + were latent prior to that. + + +Improvements +............ + +The following additional improvements and changes in functionality +were implemented in this release: + +* User documentation is now divided into a User Guide that provides a + description of the main functionality of the python-control package, + along with a Reference Manual describing classes, functions, and + parameters in more detail. + +* Signal responses and I/O subsystem specifications can now use signal + names in addition to indices to get the desired inputs, outputs, and + states (e.g., `response.outputs['y0', 'y1']`). This is implemented + via a new `NamedSignal` object, which generalizes `numpy.ndarray`. + +* `find_operating_point` (legacy `find_eqpt`): accepts new parameters + `root_method` and `root_kwargs` to set the root finding algorithm + that is used. + +* `root_locus_map` now correctly handles the degenerate case of being + passed a single gain. + +* The `PoleZeroData` object now takes a `sort_loci` parameter when it + is created, with a default value of True. This is useful if you + create a `PoleZeroData` object by hand (e.g., for producing stability + diagrams). + +* Factory functions for I/O system creation are now consistent in + terms of copying signal/system names, overriding system/signal + names, and converting between classes. + +* The `tf` factory function to allow a 2D list of SISO transfer + functions to be given as a means of creating a MIMO transfer + function (use the new `combine_tf` function). + +* The `nlsys` factory function can now create a `NonlinearIOSystem` + representation of a `StateSpace` system (passed as the first + argument to `nlsys`). + +* LTI systems now have member functions for computing the various time + responses and generating frequency domain plots. See `LTI.to_ss`, + `LTI.to_tf`, `LTI.bode_plot`, `LTI.nyquist_plot`, `LTI.nichols_plot` + and `LTI.forced_response`, `LTI.impulse_response`, + `LTI.initial_response`, `LTI.step_response`. + +* String representations of I/O systems (accessed via `repr`, `print`, + and `str`) have been updated to create a more consistent form and + provide more useful information. See + :ref:`displaying-lti-system-information` for more information. + +* Binary operations between MIMO and SISO functions are now supported, + with the SISO system being converted to a MIMO system as if it were + a scalar. + +* `nyquist_response`: generates an error if you force the system to + evaluate the dynamics at a pole. + +* `phase_crossover_frequencies`: turned off spurious warning messages. + +* `ss2tf`: added new `method=scipy` capability, allowing `ss2tf` to + work on MIMO systems even if Slycot is not present. + +* `flatsys.solve_flat_optimal` (legacy `flatsys.solve_flat_ocp`): + allows scalar time vector. + +* Improved checking of matrix shapes and better error messages in + state space factory functions and other operations where matrices + are passed as arguments. + +* `FrequencyResponseData`: use `~FrequencyResponseData.complex` to + access the (squeeze processed) complex frequency response (instead + of the legacy `response` property) and + `~FrequencyResponseData.frdata ` to access + the 3D frequency response data array (instead of the legacy `fresp` + attribute). + +* Time response and optimization function keywords have been + regularized to allow consistent use of keywords across related + functions: + + - Parameters specifying the inputs, outputs, and states are referred + to as `inputs`, `outputs`, and `states` consistently throughout the + functions. + + - Variables associated with inputs, outputs, states and time use + those words plus an appropriate modifier: `initial_state`, + `final_output`, `input_indices`, etc. + + - Aliases are used both to maintain backward compatibility and to + allow shorthand descriptions: e.g., `U`, `Y`, `X0`. Short form + aliases are documented in docstrings by listing the parameter as + ``long_form (or sf) : type``. + + - Existing legacy keywords are allowed and generate a + `PendingDeprecationWarning`. Specifying a parameter value in two + different ways (e.g., via long form and an alias) generates a + `TypeError`. + +* `phase_plane_plot`: makes use of the matplotlib + `~matplotlib.pyplot.streamplot` function to provide better default + phase plane diagrams. + +* `root_locus_plot`: added by the ability to recompute the root locus + when zooming in on portions of the root locus diagram. + +* `nyquist_plot`: updated the rescaling algorithm to use a more + gradual change in the magnitude of the Nyquist curve. The + `blend_fraction` parameter can be used to start the rescaling prior + to reaching `max_curve_magnitude`, giving less confusing plots. Some + default parameter values have been adjusted to improve Nyquist + plots. + + +Deprecations +............ + +The following functions have been newly deprecated in this release and +generate a warning message when used: + +* `FrequencyResponseData.response`: use + `FrequencyResponseData.complex` to return the complex value of the + frequency response. + +* `FrequencyResponseData.fresp`: use `FrequencyResponseData.frdata + ` to access the raw 3D frequency response + data array. + +The listed items are slated to be removed in future releases (usually +the next major or minor version update). diff --git a/doc/requirements.txt b/doc/requirements.txt index 5fdf9113d..ded0c7087 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -7,4 +7,4 @@ sphinx-copybutton numpydoc ipykernel nbsphinx -docutils==0.16 # pin until sphinx_rtd_theme is compatible with 0.17 or later +docutils diff --git a/doc/response.rst b/doc/response.rst index 0058a500d..8ccdccba8 100644 --- a/doc/response.rst +++ b/doc/response.rst @@ -547,7 +547,7 @@ the computation of the Nyquist curve and the way the data are plotted: sys = ct.tf([1, 0.2], [1, 0, 1]) * ct.tf([1], [1, 0]) nyqresp = ct.nyquist_response(sys) nyqresp.plot( - max_curve_magnitude=6, max_curve_offset=1, + max_curve_magnitude=6, max_curve_offset=1, blend_fraction=0.05, arrows=[0, 0.15, 0.3, 0.6, 0.7, 0.925], title='Custom Nyquist plot') print("Encirclements =", nyqresp.count) diff --git a/doc/test_sphinxdocs.py b/doc/test_sphinxdocs.py index 1a49f357c..80024c23d 100644 --- a/doc/test_sphinxdocs.py +++ b/doc/test_sphinxdocs.py @@ -48,7 +48,6 @@ # Functons that we can skip object_skiplist = [ - control.NamedSignal, # np.ndarray members cause errors control.FrequencyResponseList, # Use FrequencyResponseData control.TimeResponseList, # Use TimeResponseData control.common_timebase, # mainly internal use diff --git a/examples/bdalg-matlab.py b/examples/bdalg-matlab.py index 8911d6579..eaafaa59a 100644 --- a/examples/bdalg-matlab.py +++ b/examples/bdalg-matlab.py @@ -1,7 +1,7 @@ -# bdalg-matlab.py - demonstrate some MATLAB commands for block diagram altebra +# bdalg-matlab.py - demonstrate some MATLAB commands for block diagram algebra # RMM, 29 May 09 -from control.matlab import * # MATLAB-like functions +from control.matlab import ss, ss2tf, tf, tf2ss # MATLAB-like functions # System matrices A1 = [[0, 1.], [-4, -1]] diff --git a/examples/cds110-L8a_maglev-limits.ipynb b/examples/cds110-L8a_maglev-limits.ipynb index 5a7473ade..8f6d07c3a 100644 --- a/examples/cds110-L8a_maglev-limits.ipynb +++ b/examples/cds110-L8a_maglev-limits.ipynb @@ -253,7 +253,7 @@ "omega = np.linspace(0, 1e6, 100000)\n", "for name, sys in zip(['C1', 'C2', 'C3'], [magS1, magS2, magS3]):\n", " freqresp = ct.frequency_response(sys, omega)\n", - " bodeint = np.trapz(np.log(freqresp.magnitude), omega)\n", + " bodeint = np.trapezoid(np.log(freqresp.magnitude), omega)\n", " print(\"Bode integral for\", name, \"=\", bodeint)\n", "\n", "print(\"pi * sum[ Re(pk) ]\", pi * np.sum(magP.poles()[magP.poles().real > 0]))" diff --git a/examples/cds110-L8b_pvtol-complete-limits.ipynb b/examples/cds110-L8b_pvtol-complete-limits.ipynb index 0b482c865..b0ae80d76 100644 --- a/examples/cds110-L8b_pvtol-complete-limits.ipynb +++ b/examples/cds110-L8b_pvtol-complete-limits.ipynb @@ -830,13 +830,13 @@ "\n", "# Gain margin for Lx\n", "neg1overgm_x = -0.67 # vary this manually to find intersection with curve\n", - "color = cplt.lines[0][0].get_color()\n", + "color = cplt.lines[0, 0][0].get_color()\n", "plt.plot(neg1overgm_x, 0, color=color, marker='o', fillstyle='none')\n", "gm_x = -1/neg1overgm_x\n", "\n", "# Gain margin for Ly\n", "neg1overgm_y = -0.32 # vary this manually to find intersection with curve\n", - "color = cplt.lines[1][0].get_color()\n", + "color = cplt.lines[1, 0][0].get_color()\n", "plt.plot(neg1overgm_y, 0, color=color, marker='o', fillstyle='none')\n", "gm_y = -1/neg1overgm_y\n", "\n", @@ -885,13 +885,13 @@ "# Phase margin of Lx:\n", "th_pm_x = 0.14*np.pi\n", "th_plt_x = np.pi + th_pm_x\n", - "color = cplt.lines[0][0].get_color()\n", + "color = cplt.lines[0, 0][0].get_color()\n", "plt.plot(np.cos(th_plt_x), np.sin(th_plt_x), color=color, marker='o')\n", "\n", "# Phase margin of Ly\n", "th_pm_y = 0.19*np.pi\n", "th_plt_y = np.pi + th_pm_y\n", - "color = cplt.lines[1][0].get_color()\n", + "color = cplt.lines[1, 0][0].get_color()\n", "plt.plot(np.cos(th_plt_y), np.sin(th_plt_y), color=color, marker='o')\n", "\n", "print('Margins obtained visually:')\n", @@ -936,12 +936,12 @@ "\n", "# Stability margin:\n", "sm_x = 0.3 # vary this manually to find min which intersects\n", - "color = cplt.lines[0][0].get_color()\n", + "color = cplt.lines[0, 0][0].get_color()\n", "sm_circle = plt.Circle((-1, 0), sm_x, color=color, fill=False, ls=':')\n", "cplt.axes[0, 0].add_patch(sm_circle)\n", "\n", "sm_y = 0.5 # vary this manually to find min which intersects\n", - "color = cplt.lines[1][0].get_color()\n", + "color = cplt.lines[1, 0][0].get_color()\n", "sm_circle = plt.Circle((-1, 0), sm_y, color=color, fill=False, ls=':')\n", "cplt.axes[0, 0].add_patch(sm_circle)\n", "\n", @@ -1023,8 +1023,22 @@ } ], "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.1" } }, "nbformat": 4, diff --git a/examples/cds112-L6_stochastic-linsys.ipynb b/examples/cds112-L6_stochastic-linsys.ipynb index 3efc158cb..78dc926cc 100644 --- a/examples/cds112-L6_stochastic-linsys.ipynb +++ b/examples/cds112-L6_stochastic-linsys.ipynb @@ -92,7 +92,7 @@ "source": [ "# Calculate the sample properties and make sure they match\n", "print(\"mean(V) [0.0] = \", np.mean(V))\n", - "print(\"cov(V) * dt [%0.3g] = \" % Q, np.round(np.cov(V), decimals=3) * dt)" + "print(\"cov(V) * dt [%0.3g] = \" % Q.item(), np.round(np.cov(V), decimals=3) * dt)" ] }, { diff --git a/examples/check-controllability-and-observability.py b/examples/check-controllability-and-observability.py index 67ecdf26c..a8fc5c6ad 100644 --- a/examples/check-controllability-and-observability.py +++ b/examples/check-controllability-and-observability.py @@ -4,8 +4,8 @@ RMM, 6 Sep 2010 """ -import numpy as np # Load the scipy functions -from control.matlab import * # Load the controls systems library +import numpy as np # Load the numpy functions +from control.matlab import ss, ctrb, obsv # Load the controls systems library # Parameters defining the system diff --git a/examples/cruise-control.py b/examples/cruise-control.py index 5bb263830..77768aa86 100644 --- a/examples/cruise-control.py +++ b/examples/cruise-control.py @@ -247,7 +247,6 @@ def pi_update(t, x, u, params={}): # Assign variables for inputs and states (for readability) v = u[0] # current velocity vref = u[1] # reference velocity - z = x[0] # integrated error # Compute the nominal controller output (needed for anti-windup) u_a = pi_output(t, x, u, params) @@ -394,7 +393,7 @@ def sf_output(t, z, u, params={}): ud = params.get('ud', 0) # Get the system state and reference input - x, y, r = u[0], u[1], u[2] + x, r = u[0], u[2] return ud - K * (x - xd) - ki * z + kf * (r - yd) @@ -440,13 +439,13 @@ def sf_output(t, z, u, params={}): 4./180. * pi for t in T] t, y = ct.input_output_response( cruise_sf, T, [vref, gear, theta_hill], [X0[0], 0], - params={'K': K, 'kf': kf, 'ki': 0.0, 'kf': kf, 'xd': xd, 'ud': ud, 'yd': yd}) + params={'K': K, 'kf': kf, 'ki': 0.0, 'xd': xd, 'ud': ud, 'yd': yd}) subplots = cruise_plot(cruise_sf, t, y, label='Proportional', linetype='b--') # Response of the system with state feedback + integral action t, y = ct.input_output_response( cruise_sf, T, [vref, gear, theta_hill], [X0[0], 0], - params={'K': K, 'kf': kf, 'ki': 0.1, 'kf': kf, 'xd': xd, 'ud': ud, 'yd': yd}) + params={'K': K, 'kf': kf, 'ki': 0.1, 'xd': xd, 'ud': ud, 'yd': yd}) cruise_plot(cruise_sf, t, y, label='PI control', t_hill=8, linetype='b-', subplots=subplots, legend=True) diff --git a/examples/cruise.ipynb b/examples/cruise.ipynb index 16935b15e..08a1583ac 100644 --- a/examples/cruise.ipynb +++ b/examples/cruise.ipynb @@ -420,8 +420,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "system: a = (0.010124405669387215-0j) , b = (1.3203061238159202+0j)\n", - "pzcancel: kp = 0.5 , ki = (0.005062202834693608+0j) , 1/(kp b) = (1.5148002148317266+0j)\n", + "system: a = 0.010124405669387215 , b = 1.3203061238159202\n", + "pzcancel: kp = 0.5 , ki = 0.005062202834693608 , 1/(kp b) = 1.5148002148317266\n", "sfb_int: K = 0.5 , ki = 0.1\n" ] }, @@ -442,7 +442,7 @@ "\n", "# Construction a controller that cancels the pole\n", "kp = 0.5\n", - "a = -P.poles()[0]\n", + "a = -P.poles()[0].real\n", "b = np.real(P(0)) * a\n", "ki = a * kp\n", "control_pz = ct.TransferFunction(\n", diff --git a/examples/disk_margins.py b/examples/disk_margins.py new file mode 100644 index 000000000..1b9934156 --- /dev/null +++ b/examples/disk_margins.py @@ -0,0 +1,548 @@ +"""disk_margins.py + +Demonstrate disk-based stability margin calculations. + +References: +[1] Blight, James D., R. Lane Dailey, and Dagfinn Gangsaas. “Practical + Control Law Design for Aircraft Using Multivariable Techniques.” + International Journal of Control 59, no. 1 (January 1994): 93-137. + https://doi.org/10.1080/00207179408923071. + +[2] Seiler, Peter, Andrew Packard, and Pascal Gahinet. “An Introduction + to Disk Margins [Lecture Notes].” IEEE Control Systems Magazine 40, + no. 5 (October 2020): 78-95. + +[3] P. Benner, V. Mehrmann, V. Sima, S. Van Huffel, and A. Varga, "SLICOT + - A Subroutine Library in Systems and Control Theory", Applied and + Computational Control, Signals, and Circuits (Birkhauser), Vol. 1, Ch. + 10, pp. 505-546, 1999. + +[4] S. Van Huffel, V. Sima, A. Varga, S. Hammarling, and F. Delebecque, + "Development of High Performance Numerical Software for Control", IEEE + Control Systems Magazine, Vol. 24, Nr. 1, Feb., pp. 60-76, 2004. + +[5] Deodhare, G., & Patel, V. (1998, August). A "Modern" Look at Gain + and Phase Margins: An H-Infinity/mu Approach. In Guidance, Navigation, + and Control Conference and Exhibit (p. 4134). +""" + +import os +import control +import matplotlib.pyplot as plt +import numpy as np + +def plot_allowable_region(alpha_max, skew, ax=None): + """Plot region of allowable gain/phase variation, given worst-case disk margin. + + Parameters + ---------- + alpha_max : float (scalar or list) + worst-case disk margin(s) across all frequencies. May be a scalar or list. + skew : float (scalar or list) + skew parameter(s) for disk margin calculation. + skew=0 uses the "balanced" sensitivity function 0.5*(S - T) + skew=1 uses the sensitivity function S + skew=-1 uses the complementary sensitivity function T + ax : axes to plot bounding curve(s) onto + + Returns + ------- + DM : ndarray + 1D array of frequency-dependent disk margins. DM is the same + size as "omega" parameter. + GM : ndarray + 1D array of frequency-dependent disk-based gain margins, in dB. + GM is the same size as "omega" parameter. + PM : ndarray + 1D array of frequency-dependent disk-based phase margins, in deg. + PM is the same size as "omega" parameter. + """ + + # Create axis if needed + if ax is None: + ax = plt.gca() + + # Allow scalar or vector arguments (to overlay plots) + if np.isscalar(alpha_max): + alpha_max = np.asarray([alpha_max]) + else: + alpha_max = np.asarray(alpha_max) + + if np.isscalar(skew): + skew=np.asarray([skew]) + else: + skew=np.asarray(skew) + + # Add a plot for each (alpha, skew) pair present + theta = np.linspace(0, np.pi, 500) + legend_list = [] + for ii in range(0, skew.shape[0]): + legend_str = "$\\sigma$ = %.1f, $\\alpha_{max}$ = %.2f" %(\ + skew[ii], alpha_max[ii]) + legend_list.append(legend_str) + + # Complex bounding curve of stable gain/phase variations + f = (2 + alpha_max[ii] * (1 - skew[ii]) * np.exp(1j * theta))\ + /(2 - alpha_max[ii] * (1 + skew[ii]) * np.exp(1j * theta)) + + # Allowable combined gain/phase variations + gamma_dB = control.ctrlutil.mag2db(np.abs(f)) # gain margin (dB) + phi_deg = np.rad2deg(np.angle(f)) # phase margin (deg) + + # Plot the allowable combined gain/phase variations + out = ax.plot(gamma_dB, phi_deg, alpha=0.25, label='_nolegend_') + ax.fill_between(ax.lines[ii].get_xydata()[:,0],\ + ax.lines[ii].get_xydata()[:,1], alpha=0.25) + + plt.ylabel('Phase Variation (deg)') + plt.xlabel('Gain Variation (dB)') + plt.title('Range of Gain and Phase Variations') + plt.legend(legend_list) + plt.grid() + plt.tight_layout() + + return out + +def test_siso1(): + # + # Disk-based stability margins for example + # SISO loop transfer function(s) + # + + # Frequencies of interest + omega = np.logspace(-1, 2, 1001) + + # Loop transfer gain + L = control.tf(25, [1, 10, 10, 10]) + + print("------------- Python control built-in (S) -------------") + GM_, PM_, SM_ = control.stability_margins(L)[:3] # python-control default (S-based...?) + print(f"SM_ = {SM_}") + print(f"GM_ = {GM_} dB") + print(f"PM_ = {PM_} deg\n") + + print("------------- Sensitivity function (S) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=1.0, returnall=True) # S-based (S) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(1) + plt.subplot(3, 3, 1) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('S-Based Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(1) + plt.subplot(3, 3, 4) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(1) + plt.subplot(3, 3, 7) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + print("------------- Complementary sensitivity function (T) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=-1.0, returnall=True) # T-based (T) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(1) + plt.subplot(3, 3, 2) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('T_Based Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(1) + plt.subplot(3, 3, 5) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(1) + plt.subplot(3, 3, 8) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + print("------------- Balanced sensitivity function (S - T) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=0.0, returnall=True) # balanced (S - T) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(1) + plt.subplot(3, 3, 3) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('Balanced Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(1) + plt.subplot(3, 3, 6) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(1) + plt.subplot(3, 3, 9) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + # Disk margin plot of admissible gain/phase variations for which + DM_plot = [] + DM_plot.append(control.disk_margins(L, omega, skew=-2.0)[0]) + DM_plot.append(control.disk_margins(L, omega, skew=0.0)[0]) + DM_plot.append(control.disk_margins(L, omega, skew=2.0)[0]) + plt.figure(10); plt.clf() + plot_allowable_region(DM_plot, skew=[-2.0, 0.0, 2.0]) + + return + +def test_siso2(): + # + # Disk-based stability margins for example + # SISO loop transfer function(s) + # + + # Frequencies of interest + omega = np.logspace(-1, 2, 1001) + + # Laplace variable + s = control.tf('s') + + # Loop transfer gain + L = (6.25 * (s + 3) * (s + 5)) / (s * (s + 1)**2 * (s**2 + 0.18 * s + 100)) + + print("------------- Python control built-in (S) -------------") + GM_, PM_, SM_ = control.stability_margins(L)[:3] # python-control default (S-based...?) + print(f"SM_ = {SM_}") + print(f"GM_ = {GM_} dB") + print(f"PM_ = {PM_} deg\n") + + print("------------- Sensitivity function (S) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=1.0, returnall=True) # S-based (S) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(2) + plt.subplot(3, 3, 1) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('S-Based Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(2) + plt.subplot(3, 3, 4) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(2) + plt.subplot(3, 3, 7) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + print("------------- Complementary sensitivity function (T) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=-1.0, returnall=True) # T-based (T) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(2) + plt.subplot(3, 3, 2) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('T-Based Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(2) + plt.subplot(3, 3, 5) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(2) + plt.subplot(3, 3, 8) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + print("------------- Balanced sensitivity function (S - T) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=0.0, returnall=True) # balanced (S - T) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(2) + plt.subplot(3, 3, 3) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('Balanced Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(2) + plt.subplot(3, 3, 6) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(2) + plt.subplot(3, 3, 9) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + # Disk margin plot of admissible gain/phase variations for which + # the feedback loop still remains stable, for each skew parameter + DM_plot = [] + DM_plot.append(control.disk_margins(L, omega, skew=-1.0)[0]) # T-based (T) + DM_plot.append(control.disk_margins(L, omega, skew=0.0)[0]) # balanced (S - T) + DM_plot.append(control.disk_margins(L, omega, skew=1.0)[0]) # S-based (S) + plt.figure(20) + plot_allowable_region(DM_plot, skew=[-1.0, 0.0, 1.0]) + + return + +def test_mimo(): + # + # Disk-based stability margins for example + # MIMO loop transfer function(s) + # + + # Frequencies of interest + omega = np.logspace(-1, 3, 1001) + + # Loop transfer gain + P = control.ss([[0, 10],[-10, 0]], np.eye(2), [[1, 10], [-10, 1]], 0) # plant + K = control.ss([], [], [], [[1, -2], [0, 1]]) # controller + L = P * K # loop gain + + print("------------- Sensitivity function (S) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=1.0, returnall=True) # S-based (S) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(3) + plt.subplot(3, 3, 1) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('S-Based Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(3) + plt.subplot(3, 3, 4) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(3) + plt.subplot(3, 3, 7) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + print("------------- Complementary sensitivity function (T) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=-1.0, returnall=True) # T-based (T) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(3) + plt.subplot(3, 3, 2) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('T-Based Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(3) + plt.subplot(3, 3, 5) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(3) + plt.subplot(3, 3, 8) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + print("------------- Balanced sensitivity function (S - T) -------------") + DM, GM, PM = control.disk_margins(L, omega, skew=0.0, returnall=True) # balanced (S - T) + print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})") + print(f"GM = {GM[np.argmin(DM)]} dB") + print(f"PM = {PM[np.argmin(DM)]} deg") + print(f"min(GM) = {min(GM)} dB") + print(f"min(PM) = {min(PM)} deg\n") + + plt.figure(3) + plt.subplot(3, 3, 3) + plt.semilogx(omega, DM, label='$\\alpha$') + plt.ylabel('Disk Margin (abs)') + plt.legend() + plt.title('Balanced Margins') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 2]) + + plt.figure(3) + plt.subplot(3, 3, 6) + plt.semilogx(omega, GM, label='$\\gamma_{m}$') + plt.ylabel('Gain Margin (dB)') + plt.legend() + #plt.title('Gain-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 40]) + + plt.figure(3) + plt.subplot(3, 3, 9) + plt.semilogx(omega, PM, label='$\\phi_{m}$') + plt.ylabel('Phase Margin (deg)') + plt.legend() + #plt.title('Phase-Only Margin') + plt.grid() + plt.xlim([omega[0], omega[-1]]) + plt.ylim([0, 90]) + plt.xlabel('Frequency (rad/s)') + + # Disk margin plot of admissible gain/phase variations for which + # the feedback loop still remains stable, for each skew parameter + DM_plot = [] + DM_plot.append(control.disk_margins(L, omega, skew=-1.0)[0]) # T-based (T) + DM_plot.append(control.disk_margins(L, omega, skew=0.0)[0]) # balanced (S - T) + DM_plot.append(control.disk_margins(L, omega, skew=1.0)[0]) # S-based (S) + plt.figure(30) + plot_allowable_region(DM_plot, skew=[-1.0, 0.0, 1.0]) + + return + +if __name__ == '__main__': + #test_siso1() + #test_siso2() + test_mimo() + if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: + #plt.tight_layout() + plt.show() diff --git a/examples/kincar.py b/examples/kincar.py index a12cdc774..ab026cba6 100644 --- a/examples/kincar.py +++ b/examples/kincar.py @@ -3,7 +3,6 @@ import numpy as np import matplotlib.pyplot as plt -import control as ct import control.flatsys as fs # diff --git a/examples/mrac_siso_mit.py b/examples/mrac_siso_mit.py index f8940e694..a821b65d0 100644 --- a/examples/mrac_siso_mit.py +++ b/examples/mrac_siso_mit.py @@ -46,7 +46,6 @@ def adaptive_controller_state(t, xc, uc, params): # Parameters gam = params["gam"] Am = params["Am"] - Bm = params["Bm"] signB = params["signB"] # Controller inputs diff --git a/examples/phase_plane_plots.py b/examples/phase_plane_plots.py index b3b2a01c3..44a47a29c 100644 --- a/examples/phase_plane_plots.py +++ b/examples/phase_plane_plots.py @@ -5,9 +5,8 @@ # using the phaseplot module. Most of these figures line up with examples # in FBS2e, with different display options shown as different subplots. -import time import warnings -from math import pi, sqrt +from math import pi import matplotlib.pyplot as plt import numpy as np @@ -15,6 +14,9 @@ import control as ct import control.phaseplot as pp +# Set default plotting parameters to match ControlPlot +plt.rcParams.update(ct.rcParams) + # # Example 1: Dampled oscillator systems # @@ -35,16 +37,18 @@ def damposc_update(t, x, u, params): ct.phase_plane_plot(damposc, [-1, 1, -1, 1], 8, ax=ax1) ax1.set_title("boxgrid [-1, 1, -1, 1], 8") -ct.phase_plane_plot(damposc, [-1, 1, -1, 1], ax=ax2, gridtype='meshgrid') -ax2.set_title("meshgrid [-1, 1, -1, 1]") +ct.phase_plane_plot(damposc, [-1, 1, -1, 1], ax=ax2, plot_streamlines=True, + gridtype='meshgrid') +ax2.set_title("streamlines, meshgrid [-1, 1, -1, 1]") ct.phase_plane_plot( - damposc, [-1, 1, -1, 1], 4, ax=ax3, gridtype='circlegrid', dir='both') -ax3.set_title("circlegrid [0, 0, 1], 4, both") + damposc, [-1, 1, -1, 1], 4, ax=ax3, plot_streamlines=True, + gridtype='circlegrid', dir='both') +ax3.set_title("streamlines, circlegrid [0, 0, 1], 4, both") ct.phase_plane_plot( damposc, [-1, 1, -1, 1], ax=ax4, gridtype='circlegrid', - dir='reverse', gridspec=[0.1, 12], timedata=5) + plot_streamlines=True, dir='reverse', gridspec=[0.1, 12], timedata=5) ax4.set_title("circlegrid [0, 0, 0.1], reverse") # @@ -67,17 +71,19 @@ def invpend_update(t, x, u, params): ax1.set_title("default, 5") ct.phase_plane_plot( - invpend, [-2*pi, 2*pi, -2, 2], gridtype='meshgrid', ax=ax2) -ax2.set_title("meshgrid") + invpend, [-2*pi, 2*pi, -2, 2], gridtype='meshgrid', ax=ax2, + plot_streamlines=True) +ax2.set_title("streamlines, meshgrid") ct.phase_plane_plot( invpend, [-2*pi, 2*pi, -2, 2], 1, gridtype='meshgrid', - gridspec=[12, 9], ax=ax3, arrows=1) -ax3.set_title("denser grid") + gridspec=[12, 9], ax=ax3, arrows=1, plot_streamlines=True) +ax3.set_title("streamlines, denser grid") ct.phase_plane_plot( invpend, [-2*pi, 2*pi, -2, 2], 4, gridspec=[6, 6], - plot_separatrices={'timedata': 20, 'arrows': 4}, ax=ax4) + plot_separatrices={'timedata': 20, 'arrows': 4}, ax=ax4, + plot_streamlines=True) ax4.set_title("custom") # @@ -102,21 +108,22 @@ def oscillator_update(t, x, u, params): try: ct.phase_plane_plot( oscillator, [-1.5, 1.5, -1.5, 1.5], 1, gridtype='meshgrid', - dir='forward', ax=ax2) + dir='forward', ax=ax2, plot_streamlines=True) except RuntimeError as inst: - axs[0,1].text(0, 0, "Runtime Error") + ax2.text(0, 0, "Runtime Error") warnings.warn(inst.__str__()) -ax2.set_title("meshgrid, forward, 0.5") +ax2.set_title("streamlines, meshgrid, forward, 0.5") ax2.set_aspect('equal') -ct.phase_plane_plot(oscillator, [-1.5, 1.5, -1.5, 1.5], ax=ax3) +ct.phase_plane_plot(oscillator, [-1.5, 1.5, -1.5, 1.5], ax=ax3, + plot_streamlines=True) pp.streamlines( oscillator, [-0.5, 0.5, -0.5, 0.5], dir='both', ax=ax3) -ax3.set_title("outer + inner") +ax3.set_title("streamlines, outer + inner") ax3.set_aspect('equal') ct.phase_plane_plot( - oscillator, [-1.5, 1.5, -1.5, 1.5], 0.9, ax=ax4) + oscillator, [-1.5, 1.5, -1.5, 1.5], 0.9, ax=ax4, plot_streamlines=True) pp.streamlines( oscillator, np.array([[0, 0]]), 1.5, gridtype='circlegrid', gridspec=[0.5, 6], dir='both', ax=ax4) @@ -141,8 +148,9 @@ def saddle_update(t, x, u, params): ax1.set_title("default") ct.phase_plane_plot( - saddle, [-1, 1, -1, 1], 0.5, gridtype='meshgrid', ax=ax2) -ax2.set_title("meshgrid") + saddle, [-1, 1, -1, 1], 0.5, plot_streamlines=True, gridtype='meshgrid', + ax=ax2) +ax2.set_title("streamlines, meshgrid") ct.phase_plane_plot( saddle, [-1, 1, -1, 1], gridspec=[16, 12], ax=ax3, @@ -150,9 +158,9 @@ def saddle_update(t, x, u, params): ax3.set_title("vectorfield") ct.phase_plane_plot( - saddle, [-1, 1, -1, 1], 0.3, + saddle, [-1, 1, -1, 1], 0.3, plot_streamlines=True, gridtype='meshgrid', gridspec=[5, 7], ax=ax4) -ax3.set_title("custom") +ax4.set_title("custom") # # Example 5: Internet congestion control @@ -172,6 +180,7 @@ def _congctrl_update(t, x, u, params): return np.append( c / x[M] - (rho * c) * (1 + (x[:-1]**2) / 2), N/M * np.sum(x[:-1]) * c / x[M] - c) + congctrl = ct.nlsys( _congctrl_update, states=2, inputs=0, params={'N': 60, 'rho': 2e-4, 'c': 10}) @@ -203,7 +212,7 @@ def _congctrl_update(t, x, u, params): ax3.set_title("vector field") ct.phase_plane_plot( - congctrl, [2, 6, 200, 300], 100, + congctrl, [2, 6, 200, 300], 100, plot_streamlines=True, params={'rho': 4e-4, 'c': 20}, ax=ax4, plot_vectorfield={'gridspec': [12, 9]}) ax4.set_title("vector field + streamlines") diff --git a/examples/plot_gallery.py b/examples/plot_gallery.py index 5d7163952..d7876d78f 100644 --- a/examples/plot_gallery.py +++ b/examples/plot_gallery.py @@ -102,7 +102,6 @@ def invpend_update(t, x, u, params): invpend = ct.nlsys(invpend_update, states=2, inputs=1, name='invpend') ct.phase_plane_plot( invpend, [-2*pi, 2*pi, -2, 2], 5, - gridtype='meshgrid', gridspec=[5, 8], arrows=3, plot_separatrices={'gridspec': [12, 9]}, params={'m': 1, 'l': 1, 'b': 0.2, 'g': 1}) diff --git a/examples/pvtol-nested-ss.py b/examples/pvtol-nested-ss.py index f53ac70f1..e8542a828 100644 --- a/examples/pvtol-nested-ss.py +++ b/examples/pvtol-nested-ss.py @@ -10,7 +10,6 @@ import os import matplotlib.pyplot as plt # MATLAB plotting functions -from control.matlab import * # MATLAB-like functions import numpy as np import math import control as ct @@ -23,12 +22,12 @@ c = 0.05 # damping factor (estimated) # Transfer functions for dynamics -Pi = tf([r], [J, 0, 0]) # inner loop (roll) -Po = tf([1], [m, c, 0]) # outer loop (position) +Pi = ct.tf([r], [J, 0, 0]) # inner loop (roll) +Po = ct.tf([1], [m, c, 0]) # outer loop (position) # Use state space versions -Pi = tf2ss(Pi) -Po = tf2ss(Po) +Pi = ct.tf2ss(Pi) +Po = ct.tf2ss(Po) # # Inner loop control design @@ -40,10 +39,10 @@ # Design a simple lead controller for the system k, a, b = 200, 2, 50 -Ci = k*tf([1, a], [1, b]) # lead compensator +Ci = k*ct.tf([1, a], [1, b]) # lead compensator # Convert to statespace -Ci = tf2ss(Ci) +Ci = ct.tf2ss(Ci) # Compute the loop transfer function for the inner loop Li = Pi*Ci @@ -51,49 +50,49 @@ # Bode plot for the open loop process plt.figure(1) -bode(Pi) +ct.bode(Pi) # Bode plot for the loop transfer function, with margins plt.figure(2) -bode(Li) +ct.bode(Li) # Compute out the gain and phase margins #! Not implemented # (gm, pm, wcg, wcp) = margin(Li); # Compute the sensitivity and complementary sensitivity functions -Si = feedback(1, Li) +Si = ct.feedback(1, Li) Ti = Li*Si # Check to make sure that the specification is met plt.figure(3) -gangof4(Pi, Ci) +ct.gangof4(Pi, Ci) # Compute out the actual transfer function from u1 to v1 (see L8.2 notes) # Hi = Ci*(1-m*g*Pi)/(1+Ci*Pi); -Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1)) +Hi = ct.parallel(ct.feedback(Ci, Pi), -m*g*ct.feedback(Ci*Pi, 1)) plt.figure(4) plt.clf() -bode(Hi) +ct.bode(Hi) # Now design the lateral control system a, b, K = 0.02, 5, 2 -Co = -K*tf([1, 0.3], [1, 10]) # another lead compensator +Co = -K*ct.tf([1, 0.3], [1, 10]) # another lead compensator # Convert to statespace -Co = tf2ss(Co) +Co = ct.tf2ss(Co) # Compute the loop transfer function for the outer loop Lo = -m*g*Po*Co plt.figure(5) -bode(Lo, display_margins=True) # margin(Lo) +ct.bode(Lo, display_margins=True) # margin(Lo) # Finally compute the real outer-loop loop gain + responses L = Co*Hi*Po -S = feedback(1, L) -T = feedback(L, 1) +S = ct.feedback(1, L) +T = ct.feedback(L, 1) # Compute stability margins #! Not yet implemented @@ -101,7 +100,7 @@ plt.figure(6) plt.clf() -out = ct.bode(L, logspace(-4, 3), initial_phase=-math.pi/2) +out = ct.bode(L, np.logspace(-4, 3), initial_phase=-math.pi/2) axs = ct.get_plot_axes(out) # Add crossover line to magnitude plot @@ -111,7 +110,7 @@ # Nyquist plot for complete design # plt.figure(7) -nyquist(L) +ct.nyquist(L) # set up the color color = 'b' @@ -126,10 +125,10 @@ # 'EdgeColor', color, 'FaceColor', color); plt.figure(9) -Yvec, Tvec = step(T, linspace(1, 20)) +Yvec, Tvec = ct.step_response(T, np.linspace(1, 20)) plt.plot(Tvec.T, Yvec.T) -Yvec, Tvec = step(Co*S, linspace(1, 20)) +Yvec, Tvec = ct.step_response(Co*S, np.linspace(1, 20)) plt.plot(Tvec.T, Yvec.T) #TODO: PZmap for statespace systems has not yet been implemented. @@ -142,7 +141,7 @@ # Gang of Four plt.figure(11) plt.clf() -gangof4(Hi*Po, Co, linspace(-2, 3)) +ct.gangof4(Hi*Po, Co, np.linspace(-2, 3)) if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: plt.show() diff --git a/examples/pvtol.py b/examples/pvtol.py index 4f92f12fa..bc826a564 100644 --- a/examples/pvtol.py +++ b/examples/pvtol.py @@ -64,8 +64,6 @@ def _pvtol_flat_forward(states, inputs, params={}): F1, F2 = inputs # Use equations of motion for higher derivates - x1ddot = (F1 * cos(theta) - F2 * sin(theta)) / m - x2ddot = (F1 * sin(theta) + F2 * cos(theta) - m * g) / m thddot = (r * F1) / J # Flat output is a point above the vertical axis @@ -110,7 +108,6 @@ def _pvtol_flat_reverse(zflag, params={}): J = params.get('J', 0.0475) # inertia around pitch axis r = params.get('r', 0.25) # distance to center of force g = params.get('g', 9.8) # gravitational constant - c = params.get('c', 0.05) # damping factor (estimated) # Given the flat variables, solve for the state theta = np.arctan2(-zflag[0][2], zflag[1][2] + g) @@ -185,10 +182,6 @@ def _windy_update(t, x, u, params): def _noisy_update(t, x, u, params): # Get the inputs F1, F2, Dx, Dy = u[:4] - if u.shape[0] > 4: - Nx, Ny, Nth = u[4:] - else: - Nx, Ny, Nth = 0, 0, 0 # Get the system response from the original dynamics xdot, ydot, thetadot, xddot, yddot, thddot = \ @@ -196,7 +189,6 @@ def _noisy_update(t, x, u, params): # Get the parameter values we need m = params.get('m', 4.) # mass of aircraft - J = params.get('J', 0.0475) # inertia around pitch axis # Now add the disturbances xddot += Dx / m @@ -219,7 +211,6 @@ def _noisy_output(t, x, u, params): def pvtol_noisy_A(x, u, params={}): # Get the parameter values we need m = params.get('m', 4.) # mass of aircraft - J = params.get('J', 0.0475) # inertia around pitch axis c = params.get('c', 0.05) # damping factor (estimated) # Get the angle and compute sine and cosine diff --git a/examples/python-control_tutorial.ipynb b/examples/python-control_tutorial.ipynb index 4d718b050..6ac127758 100644 --- a/examples/python-control_tutorial.ipynb +++ b/examples/python-control_tutorial.ipynb @@ -94,7 +94,7 @@ "id": "qMVGK15gNQw2" }, "source": [ - "## Example 1: Open loop analysis of a coupled mass spring system\n", + "## Example 1: Open Loop Analysis of a Coupled Mass Spring System\n", "\n", "Consider the spring mass system below:\n", "\n", @@ -781,7 +781,7 @@ "id": "2f27f767-e012-45f9-8b76-cc040cfc89e2", "metadata": {}, "source": [ - "## Example 2: Trajectory tracking for a kinematic vehicle model\n", + "## Example 2: Trajectory Tracking for a Kinematic Vehicle Model\n", "\n", "This example illustrates the use of python-control to model, analyze, and design nonlinear control systems.\n", "\n", @@ -1213,7 +1213,7 @@ "id": "03b1fd75-579c-47da-805d-68f155957084", "metadata": {}, "source": [ - "## Computing environment" + "## Computing Environment" ] }, { diff --git a/examples/secord-matlab.py b/examples/secord-matlab.py index 6cef881c1..53fe69e6f 100644 --- a/examples/secord-matlab.py +++ b/examples/secord-matlab.py @@ -3,7 +3,8 @@ import os import matplotlib.pyplot as plt # MATLAB plotting functions -from control.matlab import * # MATLAB-like functions +import numpy as np +from control.matlab import ss, step, bode, nyquist, rlocus # MATLAB-like functions # Parameters defining the system m = 250.0 # system mass @@ -24,7 +25,7 @@ # Bode plot for the system plt.figure(2) -mag, phase, om = bode(sys, logspace(-2, 2), plot=True) +mag, phase, om = bode(sys, np.logspace(-2, 2), plot=True) plt.show(block=False) # Nyquist plot for the system @@ -32,7 +33,7 @@ nyquist(sys) plt.show(block=False) -# Root lcous plot for the system +# Root locus plot for the system rlocus(sys) if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: diff --git a/examples/sisotool_example.py b/examples/sisotool_example.py index 6453bec74..44d7c0443 100644 --- a/examples/sisotool_example.py +++ b/examples/sisotool_example.py @@ -10,24 +10,24 @@ #%% import matplotlib.pyplot as plt -from control.matlab import * +import control as ct # first example, aircraft attitude equation -s = tf([1,0],[1]) +s = ct.tf([1,0],[1]) Kq = -24 T2 = 1.4 damping = 2/(13**.5) omega = 13**.5 H = (Kq*(1+T2*s))/(s*(s**2+2*damping*omega*s+omega**2)) plt.close('all') -sisotool(-H) +ct.sisotool(-H) #%% # a simple RL, with multiple poles in the origin plt.close('all') H = (s+0.3)/(s**4 + 4*s**3 + 6.25*s**2) -sisotool(H) +ct.sisotool(H) #%% @@ -43,4 +43,4 @@ plt.close('all') H = (b0 + b1*s + b2*s**2) / (a0 + a1*s + a2*s**2 + a3*s**3) -sisotool(H) +ct.sisotool(H) diff --git a/examples/slycot-import-test.py b/examples/slycot-import-test.py index 2df9b5b23..9c92fd2dc 100644 --- a/examples/slycot-import-test.py +++ b/examples/slycot-import-test.py @@ -5,7 +5,7 @@ """ import numpy as np -from control.matlab import * +import control as ct from control.exception import slycot_check # Parameters defining the system @@ -17,12 +17,12 @@ A = np.array([[1, -1, 1.], [1, -k/m, -b/m], [1, 1, 1]]) B = np.array([[0], [1/m], [1]]) C = np.array([[1., 0, 1.]]) -sys = ss(A, B, C, 0) +sys = ct.ss(A, B, C, 0) # Python control may be used without slycot, for example for a pole placement. # Eigenvalue placement w = [-3, -2, -1] -K = place(A, B, w) +K = ct.place(A, B, w) print("[python-control (from scipy)] K = ", K) print("[python-control (from scipy)] eigs = ", np.linalg.eig(A - B*K)[0]) diff --git a/examples/stochresp.ipynb b/examples/stochresp.ipynb index dda6bb501..9fdba3fcd 100644 --- a/examples/stochresp.ipynb +++ b/examples/stochresp.ipynb @@ -19,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 1, "id": "902af902", "metadata": {}, "outputs": [], @@ -50,20 +50,18 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": 2, "id": "60192a8c", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -97,7 +95,7 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": 3, "id": "23319dc6", "metadata": {}, "outputs": [ @@ -113,7 +111,7 @@ "source": [ "# Calculate the sample properties and make sure they match\n", "print(\"mean(V) [0.0] = \", np.mean(V))\n", - "print(\"cov(V) * dt [%0.3g] = \" % Q, np.round(np.cov(V), decimals=3) * dt)" + "print(\"cov(V) * dt [%0.3g] = \" % Q.item(), np.round(np.cov(V), decimals=3) * dt)" ] }, { @@ -126,20 +124,18 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": 4, "id": "2bdaaccf", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkMAAAGwCAYAAACq12GxAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAlaBJREFUeJztnXeYG+W59u/RqG2v3nVbF3DB4IZtMGtCKAaDgQRCckInJJDgGJIYpwLnJMDhwzkngZgUWgqEhIA5CSWFEByaDQaDjRcMGGMb7HXZ9Xr7rnbV9f0hvTPvjGakUZ+Rnt91+bJWGmlnpdG899xPEyKRSAQEQRAEQRAliq3QO0AQBEEQBFFISAwRBEEQBFHSkBgiCIIgCKKkITFEEARBEERJQ2KIIAiCIIiShsQQQRAEQRAlDYkhgiAIgiBKGnuhd8DshMNhHDp0CFVVVRAEodC7QxAEQRCEASKRCIaGhjB+/HjYbIm9HxJDSTh06BBaWloKvRsEQRAEQaTB/v37MXHixITbkBhKQlVVFYDom1ldXV3gvSEIgiAIwgiDg4NoaWmR1vFEkBhKAguNVVdXkxgiCIIgCIthJMWFEqgJgiAIgihpSAwRBEEQBFHSkBgiCIIgCKKkITFEEARBEERJQ2KIIAiCIIiShsQQQRAEQRAlDYkhgiAIgiBKGhJDBEEQBEGUNCSGCIIgCIIoaUgMEQRBEARR0pAYIgiCIAiipCExRBAEQRBESUNiiCAIwkKEwxH4gqFC7wZBFBUkhgiCICyCLxjCaT99GQtuX4+2/f2F3h2CKBpIDBEEQViE/b2jaO8dgccfwqu7jhR6dwiiaLCcGLr33nsxdepUuN1uLFy4EBs3bjT0vNdeew12ux3z58/P7Q4SBEHkiP4Rv3S7a8hXwD0hiOLCUmJo3bp1WLVqFW655RZs27YNp5xyCpYvX4729vaEzxsYGMBVV12FpUuX5mlPCYIgsk+vhxNDgySGCCJbWEoM3X333bjmmmtw7bXXYtasWVi7di1aWlpw3333JXzeddddh8suuwytra152lOCIIjs0z8SkG4fHvIWcE8IoriwjBjy+/3YunUrli1bprh/2bJl2LRpk+7zHnroIezZswc/+tGPDP0en8+HwcFBxT+CIAgz0DtCzhBB5ALLiKHu7m6EQiE0Nzcr7m9ubkZnZ6fmc3bt2oUf/OAHePTRR2G32w39njVr1qCmpkb619LSkvG+EwRBZAM+TDYwGkiwJUEQqWAZMcQQBEHxcyQSibsPAEKhEC677DLcdtttmDFjhuHXv+mmmzAwMCD9279/f8b7TBAEkQ12HR6Sbg/7ggiEwgXcG4IoHozZJSagsbERoijGuUBdXV1xbhEADA0NYcuWLdi2bRtuuOEGAEA4HEYkEoHdbsfzzz+PM844I+55LpcLLpcrN38EQRBEBnzQoQzbD4wG0FhJ5yuCyBTLOENOpxMLFy7E+vXrFfevX78eS5Ysidu+uroa27dvR1tbm/RvxYoVmDlzJtra2rB48eJ87TpBEETGBEJhHFblCfEJ1QRBpI9lnCEAWL16Na688kosWrQIra2tePDBB9He3o4VK1YAiIa4Dh48iEceeQQ2mw2zZ89WPL+pqQlutzvufoIgCLMz4pdHcIyvcePQgBcDo/4EzyAIwiiWEkMXX3wxenp6cPvtt6OjowOzZ8/Gs88+i8mTJwMAOjo6kvYcIgiCsCKjMTEk2gQ0VrlwaMCLPg85QwSRDYRIJBIp9E6YmcHBQdTU1GBgYADV1dWF3h2CIEqUT7o9OP2nL6PKZce8llq8ursbay+ejwuPn1DoXSMIU5LK+m2ZnCGCIIhSZsQfBACUOUWUO0UAgCd2H0EQmUFiiCAS8NLOLvz0XzsRDpOBShQWFiYrc4qocEUzHEZ8oURPIQjCIJbKGSKIfPPlh94CAMwaV43z5o4r8N4QpcxoICaGHCLKYs4Qn1RNEET6kDNEEAboGBgt9C4QJc4I7wxJYojCZASRDUgMESVBz7AP33xsG17f02P4OXxtgd0W3+WcIHJJJBLBjevacMtT2wEA3pgzVO4UUe6MmvqUM0QQ2YHEEFESfP8v7+Kv7xzCpb9+w/BzvAF51IEo0leFyC/tvSN4attBPLq5Hb5gSHaGHHZUuGLOEOUMEURWoDM8URJs3NWd8nOGvHIPF/KFiHzD5wP5g2FFmKws5gxRzhBBZAcSQ0RJ4AumPtBy0CuHIFiIgiDyBX/M+oNhOUzmkHOGKExGENmBxBBRUqSS+8M7Q6N0BU7kGY9PFjq+YFjVZ8getw1BEOlDYogoKVh/lkT4Y1fkQ5wzNErOEJFneDGuDpO5HdFTdzqOJ0EQ8ZAYIooevmEi69yrx2u7uzH7R//CI6/vVeRjUG4GkW94Me4LhiV3stwhwu0QpfsJgsgcEkNE0cMvKmVJxNDqJ9rgD4Xxw2feRyAkLzRWC5N1DIzCF7TWPhNK+ONW7Qy57MwZos+YILIBiSGi6BkYNV4V5rLLYsnPXXVbKUz23sEBtK55EV99ZGuhd4XIgGEuH8gfkkvry5126Tj1BcgZIohsQGKIKHp4MRRMMmOszCGLIYUzZCEx9NtXPwEAbPjoSIH3hMgEvlLMFwhjNBD9udwpwkU5QwSRVUgMEUXP2+190u1AksXDzYXR/CFlabNVeO/gQKF3gcgCgaAs3H2hsGJQK4XJCCK7kBgiip5n2g5KtwNJnSH5K8ELIKssOqufaMOuruFC7waRBfwh+ZjzBbicIYcoh8mCYcXYGIIg0oPEEFHUeHxBvN3eL/0cDCV2eFj/FgDoG/FLt63gDPV5/Hjy7YPJNyQsAe8M+UNhKVTLh8kiESAQim73wo7D2Nvtyf+OEkQRQGKIKGru+McOxc9s4dAiEoko8oSODPmk2/4kIiqf7Ovx4Nx7NuLpbUrhM0wN+IoKdZhWq5oMiLqWOzoGcc3vt+C0n76MUBL3kyCIeEgMEUVNrycqaMRY5+lAAlHzg79sV8ww6x6WnSEzVe189//exQcdg1i1rk1xP/VCKi54MeQLhuQ+Q047nNzgYG8gjAN9o9LP27gcOYIgjEFiiChqRmMi5tvLZgBIXE22bst+xc9mdYZ2dA5q3k9zqooLRc5aQB7HUe4UIQiCIomad4N6PH4QBJEaJIaIooYNt6xyOwAAoXBE0ZE6Ed3DnBgyUc4Q34yPZ8QX7wxRcq114Y+5YV8Q7LBljUNlMSSX3QOJ3U+CILQhMUQUNUwMVbvlxOhA2NhiwTtDVujnMqLhDFH+iHXhRQ3fK4v1wnI55MaLo35522CCvDiCILQhMUQUNbIzJIsho4sFH1IzkzOkh1bOULImk4R54Y85VtnoEAU4YvlCfJiMF8JmCukShFUgMUQUNawcudLlkO7TEkPJQmdWEENaOUO0MFoXhTM0EnWG+A7pfJjMy3VIpzAZQaSOPfkmBGE9XthxGLu7hqXwQYVLu7M0I1kYzB+KNrcThGTTzXKLuk9SIBSWnAKtYbLJOm4T5sWn4QzxfbD4xou8K0ifOUGkDokhoii55vdbFD+XOUQ4RAGBUARBjZwhI7PHfMEw3I7EU+9zjUeVJO3xBVFb7lQ8dtniSVj31n6EwhEKk1kYXrT3x3KGyrlxMdJ8skBIcfzSZ04QqUNhMqIkKHOKsNuih7tWmEwr+ViNGUJOw6r95BstSqXXMeEHWCO8R2jDh7v6WZjMqR0m411BMxynBGE1SAwRJYHbzgkEjcXCa8AZMoOw8Ki6TPNOEQuVlLvsUuiM8kesC3+89UthMlkMMZcyWlrPh8nIGSKIVCExRBQdWgKgzClKAkHLGeJLk/UwQ3m9euQG/zNLoK7g/tZE40cIc8N/dizy5dZMoA4pc4ZIABNEypAYIooOLZfHZbfBLuqP5DAUJjOBGFI7Q/x+s6aLUWco+fgRwtxoHW+KnCG73GeIPy6M9tEiCEKGxBBRdKiToSti4wsShY68BoSOGcSQekYavwiOsKnmDpHCZEWAVjhXr7ReIYYoTEYQKUNiiCg61IKBjeKQwmQa1TaJhE5ztSv6usHCD0JVV8IN8zlDsQWxwkVhMqsTiUQ0j8kyrWqyYEgRLiUBTBCpQ2KIKDrUzhDrPm1PMLlevfDMnlANIHolzq7GzeAMqYWcR5EzJE81pzCZtdETscqcITmBmsQQUSjae0bichmtCIkhouhQ5wxJYihBArU/JD/nsa+ehB9fNBdLj2nCf51/LJyxcIQpxJBq3/mT0Cg31Zy1EaCF0ZrofW6aYbJAWFFVSG4gkS827enGp3/yElavayv0rmQMNV0kig6vKkxWXRYNkzkTuCVM6Cw9pgmtRzcAAH579QkAgD+9uQ8A4DOBsFDvezJniAa1WhM94a2VQD0aCClGsZAAJvLFfz39HgDg+Q8OF3hPMoecIaLoiA+TRcWQPUEeDVt8mAvEw1ftFBq1uFEkUHM5Q2IsJEjdiK2JnqBRhMliOUP9I35EuI+ZxBCRLzoHvIXehaxBYogoOnTDZJJA0J9NpiWGnDERZYbOvgGVuOnxRJvxBUNhyRmqdNkTdtsmzI9eTyutDtTsGGBQmIzIF2bovZYtSAwRRYeeGGJCRzNMFruPCR8eM+UMhWL7GdN10pUZm10lCEBNmUPqqaQl/AjzYyRniIXMDg8qr87JGSLyRTE5zySGiKJDLVrqY4NM5Wqy1MJkTq7Tb6FhJ5+W+nIAQAcTQ7FxDdVuB+yiTQqTUc6QNdFzIXkxNLmhAgCwr2dEsQ2JIaIQhC1+riExRBQdarFTVxETQ4mqyRLmDJnHGWJ/W0tdVAwdHvQiFI6g1xN1huorlMKPwmTWhB1rbofyeHRzYbIZzVWazyUxROQD9cWh1UNmJIaIokMdGmLOkDNBV2YjzpAZxFAo9reNrXEDiDpF/SN+9MWcodryaLK4yHKGLH61VqqwY7TS5VDczztDTPjGP5c+cyL3DMRC8wx14YrVIDFEFB1q0VJXwarJEpTWx+5zaeQM8WMPCg1b6NwOG6pjuVB9IwH0xZJomfCTS+sLv89E6rBjjeW7MXgxBADfPXtm3HPJGSLywaBKDGnNhLQSJIaIokN9ZczKke0J3BIjpfVmcIaY62W32aTwX9QZip6YamNiSEyQH0WYl0gkgk+6PdJCU+YQpWR5QNlnCNB2h0gMEfmgf6S4nCFqukgUHerFYGIsv4a5JcF0w2QmWGSYkLPbBNSWO7GvZyTqDMXCZPXMBaMEakty9/qP8IsXd0s/O+02OO02qZGoW+UM8eKo0mXHsC9IApjIC+owGTlDBGEymNg5feYY/Hv1qagpUw5q9WssFr5EpfUiG3tQ+C87S4i2izbUxfKD+jx+KUzGnCF7gqG0hHl57M12xc9O0SYdt4CyzxCgDJuxsCk5Q0Q+UDtDJIYIwmQwsXPUmEpMa6qU7rcbcobEuMdcJnKGQpwzVBcTPn1cArW6moxyhqyFWrs67TaAu0+dM1TulM19NnaGxBCRD+ISqP3WPu5IDBFFB1sMHCqXx5HALTHWZyi9L7svGMLX/7gVf3hjX1rP52F/m10UpMqxvpEAemPOUJ1UTUY5Q7lk88c9uOv5nVkXHuGI8vNy2m0Icfepw2S8U1QlOUP0mRO5p18lhqwuwilniCg6ZDEkKO5nP2slQjOL15WD0vrX9/Tgn+914p/vdeKc48ZiTJUrrdcB5DCZQ7RJzpBWAjUTfpQzlBsufvANANEWB5cvnpy111U3rnOIgkIgiTblMV3h4sVQzBkyQaI/Ufyoq8nM4JxnAjlDRNER4AQDj1xNpiGGgtoJqkDmzhB/sf/ugf60XoPBXC3RJkguUNeQDwf6ol2IJ9aVSY/z2xO5oV3V/TlTVMYQXHYRiSKd5Q4uTMacIQqNEnlALYas3uCVnCGi6NAPk+l3ZWbJ0eqOv0DmpfV8p9ZMexXJpfWC5AK9+GEXgOhcsgm1ZdLjgHZ+FJEZvHuj5SRmgvrIrHCJijCZGj5MxqomKUxG5INhX1Dxs9XDZJZzhu69915MnToVbrcbCxcuxMaNG3W3ffXVV3HyySejoaEBZWVlOOaYY/Czn/0sj3tLFAL9MBnrQB2/WLAwmTpBFcg8TMYLoEx7FTGnhw+TMcodIgQh+jfLg1ppYcw2g175itilcbxkgjpnqNxpTxjq5JsynnVsM4BoaFT9nN1dQ/jg0GAW95QoFQZGAohoCHKPXymGrB4ms5QztG7dOqxatQr33nsvTj75ZDzwwANYvnw5PvjgA0yaNClu+4qKCtxwww2YO3cuKioq8Oqrr+K6665DRUUFvva1rxXgLyDygZ4zZE8wjkOvjwsgl9an+2X3BeTnZTrslTk9ok1OoGbcedEc6TYbx0E5Q9mne9gv3c52V3K1GKpw2TGhtgwH+0c1t3c7RPz6qkUAgKljKqT7A6EwRJsY28cQzrx7AwDg/dvORoXLUqd9ooBs2tONy369GVecNAl3XDhH8diwL3ouswnRKkirh8ks5QzdfffduOaaa3Dttddi1qxZWLt2LVpaWnDfffdpbn/88cfj0ksvxXHHHYcpU6bgiiuuwNlnn53QTSKsj17OkBQm08wZShQmy2xqfVbDZNLfJqChUukMKdoI2PT/ViIzWOUeAHhUoYJMiEQicc5hhVPE986ZiQWTavGvVZ/WfN5ZxzbjrGObFT2yeEfwk26PdPvIkC9r+0sUPz/9104AwB/faI97jB37zKGmMFme8Pv92Lp1K5YtW6a4f9myZdi0aZOh19i2bRs2bdqEU089VXcbn8+HwcFBxT/CWuiFyVgCtVaYbNTPqskS9BkyUZhMtNniRjFUcD1n7Anyo4jM4MVtNsVQIBSJ6zNU7rLjgvkT8OTKkzFzrPaUegYv/vmKsp2dQ9Jt1o+KIIxgEwTdx9ixX1NeHP2tLCOGuru7EQqF0NzcrLi/ubkZnZ2dCZ87ceJEuFwuLFq0CNdffz2uvfZa3W3XrFmDmpoa6V9LS0tW9p/IH8kSqNVf2kgkIgmWRNVk2RBD2UqgdogCXHYRVVzIgw9/lOo4jo6BUax5dgf292a3youHP36GsiiGvBrOY4XTeE6SaBPA1i5+H/cckZ0h3tUiiGQkEkPDcc6Qtc81lhFDDEH14UQikbj71GzcuBFbtmzB/fffj7Vr1+Kxxx7T3famm27CwMCA9G///v1Z2W8ifwSCemGyWGm96kvLCxStMFnGTRcD2QuTsb+NuVzlXJ8ZvucMyxkKlJgYuv7Rt/HAho/xpd+9mbPf4Q/K7+k/3u3ASzu7svK6WuMM+A7TRpCKBLjP/WCfnG9EYohICZ2lNRKJcGGy4nCGLJNJ19jYCFEU41ygrq6uOLdIzdSpUwEAc+bMweHDh3Hrrbfi0ksv1dzW5XLB5Uq/KR5ReAJhnTCZjjPEL0K5d4YyS6D2qXKb+Oo3/jb720ttHMfb7f0AgI+5PJlsoz5+Hn+zHafPbMr4dflEewYvcI3gsAnwA7js12/ghdWnwi7acIhLvqYwGZEKejaDNxCWQro1ZZQzlFecTicWLlyI9evXK+5fv349lixZYvh1IpEIfD5KIixmko3jiBdDcu8e9XOAbPQZ4sSQxoKXzmuxfarkSqt5h1Rqumhx6zqbHB70KoRBuqiPn3+9fzgr7hBfss9I1RnyxHLf9vWM4MNYrhBfiaYerklYi91dw1iy5gWsfHRrXnqI6QVd+kejotpuE6RB2BQmyyOrV6/Gb37zG/zud7/Djh07cOONN6K9vR0rVqwAEA1xXXXVVdL2v/rVr/C3v/0Nu3btwq5du/DQQw/hpz/9Ka644opC/QlEHmCiRb+aTPml9UoNF7WvwqUwWbql9ZwblGkvDq+qOeTRYyo1tyvVnCG7Tfvs3TXkxSn/8xKW/PhFvHdwIKPfwcRQHdfa4MsPvaXZiyUVBkfj848qMyiD3901DEBZQWb1q/dS5629vTg04MWz2zsVuWC5QuC8Ib7ZaPdQVAw1VDql86PVjy3LhMkA4OKLL0ZPTw9uv/12dHR0YPbs2Xj22WcxeXJ0NlBHRwfa2+USwHA4jJtuugmffPIJ7HY7jj76aPz4xz/GddddV6g/gcgDcjK09jgO9RXMaILu0wDXZygYNpSjFrc/gdw5Q19c1IJn2g4pFmaA+1tLTAw5RBuC4fhQ5P7eEUmIftg5hNkTatL+HUxsT6grk2bCAdHjymlP7djgUU8BB4DyFBKoAaD1qAa8/nEPgGiPmHNmj5WOb7aPhHUZ9sqCWd0BOhfwp7rRQEgq0ugejgrsxkoXnDrpB1bDUmIIAFauXImVK1dqPvbwww8rfv7GN76Bb3zjG3nYK8JM+FWCgSGXmyu/tCOx0EKZzsLDT7L3h8Ka5feJ4B0lrZyhVASWJIZiwu3kaY149NrFmFRfrtjOXqI5Qw5RgIamUCQ9q2cqpYo/JijG15ThvYOD3P1hxbGSKv/Y3hF3X6oNEn979SL8c3snvv1/7+DJtw/i66dNUzyeijMZiUQQiQA2HbeNyD989eKIP/diiDc7Pf6gdDwyt7Gx0qWbfmA1LBUmIwgjMMGgXpicOl9a1mOIH3rJU+myg60HA2nkXPBukF+VTH3Nw29h+T0bNSuJtF+L9UOS/7aTpzWiRSWGSjVnyKkjVHkRoJWbkwrs+KlyO3BVqzyx3mfwM9TiQN8I/vbOobj7U3WGyp12XLRgAmrLHQiGI3hrb6/i8VQm2n/9j2/j0z95KeP3i8gevDO0+eNe3PzUdvQM5y4Hlm/3MOKTbx/hnCF7gjFHVoLEEFF0aAkGALpfWnaFpecMiTYBDZXRCsOuNDr46nWgvnv9R3jhwy582DmEjw3G/70J+iHxsDBZqc0m49PEeNHLi9C1/96F7QfSzxsKSGJbwO0XzJbylDLJB/vndu1eaekMghUEATOaow0a34iFzBipXL0/934nDvSN4rHN8d2HicIw7JOF6S9f2o0/bW7HLU+9l7Pfxy4UAeCMu17Gc+914tP/+xJ+EutMXVfu0O3fZjVIDBFFB1uUXHE5Q9ojKlhORaKr8DExMZTOOAOt0voPOwfxwCsfS/dr5YuoCYbCUkJ0skVS/ltLSwzxCeP8yVl9ov7ML19FX5o9d9TVipm2XgCgO3ss1fw0xuSYU7ijY0hxv9Grd/5v+aCDuvCbBa08obfb+3L2+/hzVzgCrPjjVrRzDU0rXPaiSaAmMUQUFaFwRDrhq3N75C+t2hlKLoaaqpkz5E15n7TGcezpUjpBRkIR/Osky1sSSzRniP9s/RrvO0/nYOqfJSDnDGVTDHVzoY4ZzdoVgqnAjmV1KwGj7tUQdzyWmqA2M8O++FAs795km2THdKXLnnDMkZUgMUQUFX6FYNB2hvRyhsoS9HRpqoqKocODaThDqg7Udz2/E3et36nYxkhSL59XZNgZsvgJKlUUAiikfZuRbjVOnDMkZtahHAB6hqMu1T2XzMfUxookWyfHHRNDzHFM9ep9kMtNybQCksgewxoXTSMZ5KolI9nxUuGyF02YzHLVZASRCD4/R51ArTeOQwqTJcjDaamLhh3a05h5xS/Q7x8axPuH4sMORsJkUmK4aEta4VOKOUORSESR8JnMGRpKMzGYnfRZSbHkDGWwGPClymIWqrfUxQBNVS4c6Bs1vGCt/0DOYcpH1RJhDI+GM5TLXmLJxZB8zrS6aCZniCgq2KJnE+Ib8OmVgCZLoAaASQ0xMdSTuhgy4hjwV+LJXsdIUq1cWl86YsgbCCtKgbVCZufNGYclRzcAAIYMvOdaSGLInr0wWY9HbmL3Hwujw6GPHVed9uuVOZXHSHO1G4A82y4Rr+7qxp3Pfij9PJLDMAyRGqM5dIG0MBImY13Sc+lQ5QMSQ0RRwTclVCef2rkO1Cv+sBVdsZwRIzlDkxuioYt9val3fTUyj8xImIy9jjoxXAtRJyRYzKgdDK0EapfDJnV1TlcM+VWDgPmmnOkQDIWlmWGNlS6cNnMM/vHNT+H/VrSm9XqAck4dIId5jbhX/95xWPFzLnNSiNTItxhKlgdU4bJL581RizuIJIaIoiKRYHDY5Puee78Tt/7tfQBcn6EEYmhs7Mq6e9if8tiFRPZxfUV0yCEvhiKRCK7/09u47g9bpMnQANDniW5T5VZ2m9aC/a2l5AypHQytMJnLbpPev2zlDLkydIZ6Pf5oc0MBqCt3QhAEHDe+JuWGizzq/DcmhtIRxyMBay9yxYReP7JczCmLRCJJxXOF0y456lZ3EEkMEUUFn1ejxqEalXCwL1ppM2IggZoNIwyFI9IwzFT3SYuJdWUAlNVkA6MB/OPdDvzr/cP489YD0v2sMmh8rTvp7xRLsLR+vyqfSyuB2inaUBUbbjucYZjMoQ6TpbkgdceSp+srnFnJFwLinaFxtdHjLB0xRM6QedATQ305GMBr5NxR4RI5Z8jaxwmJIaKoUI+r4FELJBZGMxImczts0vP7R4z3pwmHE19dTYgtUvyQTt5h6OF64XQMxMRQTVnS36s3eqSYuew3mxU/a1WWOUQ+TJbeAiINArapEqjTdIZY8nRDhSut52uhzhliottI+bNavGsl7RL5JxgK635+6bT8SIYR4dxQ6ZKS9ckZIggTwUJSWs6QXbQprrzZzdFYGEB9Nc0jCAKqY+6QkcovRjK3gIkh/jUVTRq5K8GD/dETHrvKT0SpNV3UCl1qdaB22m3SQN50S+HlQcDR4yXTnKEeT6ySrMqZ1vO1KOOqyQQBGBcT0Eb2sdejbB8xGggpJpYThcGb4LP72iNbs/77jBwrNWUOKUxm9eOExBBRVDBhU64T8uIrsdTOUKJqMgCoLU9dDCUrN9UKk/ECirfF+6Uk2+SLpr3Ecoa0qvG0coacdlvGTg5LYmWiir2eL00XjoXrqlzJc8GMwh/LDRUuSegfHvQmTejXSiz3GigCIHJLovmFeh3MM8Fo2Jd31K18nJAYIooKdiKv1Ek+5cWQ5AwZCJMBct5QKsNaky08E2P9i3iBxS/SXk5MsYTfigS5TQxRLC1nqEujm7RWNZlDtEmJz+nm+LBFyRUTGKwbeLriSnaasnc65o/lMVUuOO3y8XDxA28kfC57r765dLp0n9VDIMVAopyc6U2Zdy1XkyikeumJLXj6+pMBKOckWvk4ITFEFBUsv0GvEocfY5FKzhAA1MbEUCrJimyhK3OImsmxE2LO0Ig/hL+9cwiBUFixiCumRvsT/208Dltp5QxpXRlrjUFxZcEZYmKIuS1MxCS6cjfyeslGrKQCO1bZbQcXNm7b35/QMWTjRuZNrJH+RqsnxxYDiS6scnHJE9D4ftRXOHHB/PFYc9FczG+pBRAt1mAXmVY+TqgDNVFUsFL0Spf2wsJXlLFbUpjMkfjrwOaTHU5hphVf6i8I8VdOE+rK4LLb4AuG8Y3HtuHmc4/Bgkl10uP8Asv+tgqdv42HCa9wJJrEnaxjtdV58u2Dcfcpmi5yzpAzY2dImTMkNZ1Ls89KLpyh2nI5lGoXBcXVOxCtTGyJDXP1+IIoc4jSMcIWQbtoQ7lTxGggBI/Fe8gUA6P+6OdS4RTjKlpz0U+Mf83ZE6rx7WUzcfLRjXGd/YHoBZov6MfAaAAtWd+T/EDOEFFUSKEkHfeEz7O1xZwh1iwsmTM0tjrq4qQihtjC6bLbNDtHV7ns0hUWAPxl60HdMBlbkIw4Q3aup1Ioxb5IViMUjuClnV0AooKCiQqtnCG3w5bxlG2vKmco0z4r6rBbNuAXrEgk2syR5+PuaPPQrfv6cNyP/oUbHntbekwOKQpF00OmGGAucUNlfNWhlouTKexiobnahb9/4xScPrNJUwgBcrf0Nz/pzfp+5AsSQ0RRITtD2oJBXe0QiUSkNvLJxNC4mmh/n46BVJwhuSN2TVl8gqwgCDhhSr30s2gTFIm4SmcoFiYzkDPESuuB4h/WuqtrCEPeICpddrx/2zlYekwzAKXY4T+HTKu/JDEUC2uVZxhKkpwhA2NW0kUdon3y7QMYGAng8/dtAgA8u71T+m4EuJ5MxdJDxgoka+aaKJzvz8F3XGohoVGZq+ako6LnsO0HB7K+H/mCxBBRVAwnEUO8S/LR4SEM+YKSW5SsmmxsTAx1piSGWD6IDauXzUSFU8TnF0zEjOZKXLZ4EgDgmHFV0vZ2UVAs0nzeSzphMgAIhos7b6g31otpXI0bok3QdH58nEOXcc6QqrSeLy1O6/Vy4AzxCBoR0mfaDuH+DXsU9x2INSFl4UWHaONCgCSGckUkEsE3H9uGRXf8G3u79cf9sGpSVtXKk4vvODsOtNqUqGmqip4bU+nBZjYoZ4goKpKFyfgr3B6PH19+6C3pZ71yfAbrM5TKGAe+CeRn543HeXPGxV2lnz6zSbo9MBpQJlDHFspgKCy9liFniPsdxV5er260yf52Rb+mmChVlNancTUdCIWl97MsLmcoXTFkfABvOiyaHL1qX3PRHNz05Hbp/vteVoqhnYeHMKmhXJFfVS6FyShnKFe8f2gQf33nEADgnQP9mNJYEbfNP7d34Olt0bw4NsKHJxdhMvXYmUTUxARafwptR8wGOUNEUZEsTKYWMlv39QGIls0nG4WQzsLg58IzQHy4AogKtxe+fSoAoGfYr+kM8QmTRnKG+N9jpOuwlZF6CMVO2vbY/3x4kA+TSaX1afRE4d0fJr4yDSXJSfbZdYaev/HT+MHyY3DdqUcBAC49cRLevGWpbqL2zs5BAHIFotMucMc8OUO5gm+rofU+93r8+Pqjb+OlnUcAKJPjGbn4jrPjUj3GSIvaNNqOmA0SQ0RRkay0Xs8kaTDQyJA5AaksDL6gsav+xtgohmFfEKufeEe6nzlDTOQ5REE3iZFHEATJISkZZygmOB1SjyWNnCGHLaNZYuzzEAT5My3L0D2RqtOy7AzNaK7CilOPVlSSNVW58eF/L8f4mvj5dh92DgFQhsnKKEyWc/hhzFqCuk8Veqovd2L2hGjC8nHjo//7Q+GUB0gngw2GrtMQX2pq0mhIazZIDBFFxVAKeTU8DRrWsxp2lewLhg0LDF9AzhlKRHWZXRHaYrDFdySFSjKGPKzV+jlDXYNefO2RLfjaI1vw1LYDisf47tKAXEnHXy3znwNzkALB1BcPPveI9alKRyQrXjPIqtNykzOkxdEaTfpYlSQfJquQXC8Kk+UK/rjRyjsbVAmMugonfvulE/CdZTPw80uPl+7P9kUPGxNj5NxYWxbdpn80kHVRli9IDBFFBbvKYpPJjWJkSCafU2Q0WVbtWughCALqNE46zDUYTqGSjOHQCBdZlV+9tBvPf3AYz39wGD/4y3bFCZdPUgc4Z0ivmiwDZ0irC3h5mgnUbL9znTOkxTQNMTTiDyESiUi5Inx/onSTw4nk8KF7LXdRPWqmrtyB5mo3bjhjulThCmQ/VNYzHHWktEr51bCk7lA4Il2QWg0SQ0RR4UmSQH3Xf8yDy27Dzeceo7i/3kCYzB1rnAgYD4mok3sToXUFNhoIoXXNC/jFC7sApOZ4iUU0rJX/G3zBsELIxDlDGqNIFB2oMyit74/lRPAVPex216DPcN7QM20HcdwP/4V/vNsRN/g1H4ypkhc4duEw6g8hFI5I1ZVO0cYJS+sfQ2aFP5ewxoo86tATf9HE9xNLt4moHt2SGDJybhSlXla7Dg9ndT/yBYkhoqhINr/r8wsn4r3bzsby2eMU9586Y0zS1xYEIeXxBGrXIhH8AsXTMeDFCx9Gmwomq3jjKaacIXVCPN+MUp2kzhwxzT5DGeYMyeXN8gJx9JhKTKwrw2gghNd2dxt6nW893oZgOILr//S24VBqNuG/H6w6acQfUrgLDtEmJaMXexJ+IWGuLyAPmuaJE0PcsecQ+UKJ7IohFiZrNOCaA9HxLQDwzv7+rO5HviAxRBQNAa78XK+aDIie5NVOzcnTGg39jlSra+Qck+RX/Ww8QiIS/V1qmEOSi1b9+UY994v/2adyhtThwXA4IgkfRZgsmHrSKSsdruOcIUEQcMzYaK+o7mFfSq/H738+nSG+cR9bXEf8QYVA5IfaFsMxZFZGkiRQq3OG6jkxJAhCzty7nhScIQCY3hz9DuzvG8nqfuQLEkNE0dDRLzdDTJZoXKZaeMoNLkSpjicwWk0GAC11shhi5dBqknXJ5mEWejE4Q+qcFV4M+VXvMXPEArHEcX6Bd9ptir4pqToeLExWU6ZcINjxlkoPKiA6Z0puuphHZ8gV7wyNBkKKPCuHKMgDf4sgCd+s8G0ztM4rfR5lNVlthbLpYq4EK2tmqh7logfrsG/VijISQ0TR8N//+EC6naz8XH0VbnSQablDzq8wAj+oNRlTGmQxdOr0MXGCDUjNGSqmnCE+LAYoxZE6FKnuM+TjnuuyRyuk2HujLltOBguT1am6AFemK4Zc9rjxHvmAF0PMGQqEItJi7BCFqOsguWjWP4bMiqK0XiNRvVM1C7FKdQ5g4j+bOUORSERyOY06Q0wMqZ0sq0BiiLA8R4Z8+PYT72D9B4cNP8dIV1Ut3DFnZmA0gC8+8DpWPro1YaiFLeJOMflCd/oxTZg7sQblThHTm6s0m+OVp5BAzcJkxegM8WJU7Qyp+wwxsWQToguHXbRJVTgHUrT0tRKoAU4MeZOLIf54KXeKBQmTVXAOYz3nNLCrevb9sJMzlHOUCdQaYkg1/kdQzVfJdPCwFh5/SDoujVTaAtZ3hmgcB2F5bv/7B/hbrJ09APyC671hBK3ZTXq4YovE42+1SxOaA6EInBpdWiORCDbuinaNNVqR8eTXl8AbDKPSZY/lGSlPLFXu+LlEekgLWRHke8TnDMUnR+v1GeLL6tlCMrGuDAf6RrG/dxQLJxvfjz6NBGpAFkMeA1WGvDb1BsKSc5fPBGo+Eb+23AnRJiAUjkhX9UwMFVN7BrPi4RKotZxF5gx944xpOH/u+LjHc/EZ9cRcoQqnmHRmI8PqYoicIcLy7O5SlnKedWxzSs/XanaoBwt38RUTXp2xDr/ftFcafjm2Or7jr+a+iDZpYdVyhqZqzC3SQ4yJAiuGydRuW6IEanU1mV3VZ0grVDmhNhqSPNg/mtJ+sQRqtTPEwk5DBpwh3mUZ9MoLR16dIc5hnNxQLuXMyc6QEPs//co7whi8gO7XGGfRNRQVJl9c1IKZY6viHs/FZ3R4MFZJplPhqgWJIYIoMHx+UF25I+VFJZUkWtajhm+E5tXJH7r1b3IO01iN8QfJ0Po7ZjTHnwz1sGpp/c1Pbccp//uSQigkzhlSV5Mpc6W0ktiry2JOToo5Pmz2knpEQaXb+OvxnwefMFsoZ+joMZXS1T9byOQ5b8XjLpoV/pjpHPTiiS37pZ99wZAk9tmgaDVS1WiKfbPuen4nLvjlqxjyxouXQ7GLhPE1ZYZfj8QQQRSIV3d147o/bFFUWzRVpS46UkErMZstzB8fGcauw9H5TmoB0mzQGeJRD+4cX+OWSriNYNXS+j9tbseBvlH8Zas8doM5QWyRTlRaL4fJworH+fYGZWl2VmZhspqy9BOotZw6p2gznMSfDfj9n9pYIbkLbP/Ze+mkPkM5hw+TAcD3/vyudHuEe6xCJ1yV7mf0ixd3450DA/jdq3vjHmOO6fja1MWQNxCW3FgrQTlDhGW54reb4+4zGt8GgMsXT8Kjm9tx/KRaw8/RunofDUQ7955x1ysAgDdvWapIhJzfUotGgxUZPPzgzrv+Yx7OnTMuJdfLqs4Qg3eDmGipLXega8iXsJpM3XfFpzHugr2PascpEZFIRO4zpOoWzloeqBc2LUIai1Y+XSEgKnY2fu90CEL0vWDih+2/OuRoNUFtJRLlmbHHXHa5AaaaTEvr3z80EHcfc4Ym1Bq/iKty2yEIQCQSdYeaqvIX9s0GJIYISxLWWeB9KVjF/3X+sThmbBXOTCHHSNMZ8ocUVUQv7OjCmFhvjlnjqvHUyiVxFSBG4IVdfaUzJaEHWL+0nh+XwcJJY2vc6Bry4Zm2Q7jsxEkQBEF/UKsUJgspHgdkZ0idi5QIbyAs/S61M8TEg5G8jZBG9aHaBcwHfJNPJpzZ4qtuYEliKDdEIpGEPcvYY4n6pjkyFKz7Y3mNkUhEOk91xCrYUnGGbDYBVS47Br1BDI4Gcu7SZxsKkxGWZG+PR/P+r5w8xfBruB0irmydgnEpxMW1xJA3EMaQT46T7+wckmzmKQ3laQkhQHkCTKW/EEOqMrFoWbQ/FF0IfMGQ1ADutJlNAIA3P+nF5lg1nzoMFp9AreEMSdPYjYshPgSmDlmw5Gwj4QEtp04rWT6f2FVhsvg2BdYU1GbHFwwndG5ZPlGiZqupjkzpGfYpfqcvGMKRIR9OvPMFfPH+1zHoDcg5QymIIQCoKbdu3hCJIcKSbD8Yb+0+/rWT8B+LWnL6e7XGangDIcVC2TEwKv3MhmCmA99cLZ3XkZwhC+V78I4fO7l3xSpbnHYbls8eKz3+USw/S+0MqUuNs5UzxFykMocYJ3CZePAZCLtpCYt8h8nUOGOix6PKGVK3KSCyC588fceFs6XbzOWRnKEEMwmdKbh3j27eh4V3/BuX/foN6T5fIIwPOwdxZMiHN/f24s2Pe7mcodTcnWo3iSGCyCsfHBqMu09r6nu20Uug5sNkHQNe7oouAzHkzswZsmLOkNY0etZnZVyNG7PGVUvviyx2VB2oVeM4fBrjLpgTk0qYjC1MWlfp7HcbCpNp5gwVNr+CuQssZ4jCZPnBy4S8aMOlJ06Sep4xMSGdRxI0W2XunZFj7/+2RIsSmKsKKCvWAKC9d0RqEZGKaw7I4eMdHUMpPc8MkBgiLAnrvcGTak5NOjg1khhH/Upn6FC/LIbSETHS7+KEVyrNFhnq3BkrwOd8sdssf4FV5LHGc+w9Z4uA5GboOkOZ5QwxF0kriZ2JGSPOkFbOUCYOYjaQcob0wmQkhnICe18dogDRJqAydvHEml+yHK5kg6ejr5X8e75PI70gWv0VjtvGabclnfGohl383b3+o5SeZwZIDBGWRGumVCYujFG0Zox5g0ox1OvxYdiXPPExGQLkUEw6okpk4zgstJDxOTfsNuuGy5LSK12sciv6nqurxdQJpeqmjEB6YTI2NiGRM2QsZyj+81AnZOcbqZpMSqCO/o0OKq3PKex9ZQKeJdIzgc+cukQ5Q0bdu1F/CH0aTR2HfUFF7tyh2MWH1mzEZLAwttHB12aCqskIS6L1pU5lonu66DlDIpdDEo7IYq0yhVliiRDT6EFjt2A1GW/XsxM0S55m09Wlbs8qZ8ilk+eSKIE6ldJ6KWdI4zhjYiIciV7t65VBA9qfR6HFkJ4zRKX1uYUVNzABr8498yZwIxlGq8m27OvVfazHIzvtHQOjsd+Zuldy2swxAKLfzWTfA7NhnT0lCI5+lTMkCPlJQtX6Hb0ef1yzvSOxMF4mblUEmYkYuwXHcfB2PRMzPSoxJM0Bi3OGogtGuVMZAtMax8EmxKfmDMkJ1Gp41ylZewetcIZ6vEe+0csZcorWO4asRCAYc4ZszBlS5rKx70CiwdJGx3G8eyC+6ITROSCLoYOxUvt0nCG+S/aggdE0ZoLEEGFJ+K7TQNSWTbeEPRX4RW9KQ7RPS/ewL24mVddQ1GrOJEzWUleefKMEWDKBWiGGord7h6OfNRt2q54Qr84ZYifk4djVqVY1mZRAnUJpPXOqEjlDQHIxpPV5FNoZckpiSO0MxVy2FEc9EMZgSf4OO3OGYrlnsfc7IOUUJRBDdmM5Q4nm5rX3jki3meuezqw8h2iT2k5YraKMwmSE5QiGwnFXHWV5yBcClIve1MYK7O0ZQfewPy7BmTlDmSRQf2HhROzuGsbJ0xrTer4oWq+0XlMMxVxANg+MzQFjYiekmvrOJyMPeYOSc8R/dlKeRQo9mJiLpBWOFW0CHKKAQCii+Bu00EqgrinPfSVkIlg4TN10UV2ZR2QX9t10MGdIyj1TiiHW+kALh81YmGwkQafr9t74xOp0BwfXlDng8YdIDBFErtH6kpU582Ny8k3IJjdUADiCI0M+KbmXwS7+E5XEJsMu2vCf5x+b9vMdkjNknYWMd1XYQsFyhhpUOUPDvqAiNMCXg5c7RYz4Qxj0BuJK79k2/O8wAnOG9BYJp2hDIBRKmkSt5Qzloy1EIliYhu2aK/b+sPeUEqhzA6smY2LUrWremc0wWaK5eft6RuLuSydMBkSF/aEBr+XEEIXJCMuhWUnmyI+u5+eYTW2sABANkw3rXHVl4gxlimjB0nreVWEndymBOhYmq+Jyhvjt+eR2FnYaHA1qhsnsXGfliIZTo0WiPkOAXAmUTs7Qp6an5/5lC6dd1UQy9rfwoVa9EThE+rBjXMoZUrVokMJkCfIh2WMs/0gPTwIxpHXMptsVnTmzQ15riSFyhgjLwWLa7OofkKuDco1DtOF3Vy/CB4cG0Xp0A4BYB2qdeHwmOUOZwhZ8K+UM8a5KIBYCY+JXXU027AtKV7uiTVBUrlS7HegY8MacofhqMjtXnRcMR6SKnETIYTLtz1RqvGgwZ2hmcxU+O388pjVVSp17CwVbjBlMWPKLcCAchstmvZJpMyOFyeyJw2SOBNWkRkvrE81A0yLdvm0uu7H9MRskhgjL8fa+PgDApPpyfNgZ7XSaKKaebc44phlnHNOMvd3ROHsgFNG1oCvzlMukhd2C4zj8qjBZ/4gfzLiRcoZcrDFdEKf870sAAPVaUV0W3WZgNAC/RjUZL5yCoQiMRASShskM9hpi5dR2UcD1p09L/ovzgF1UO0MxMWRTvk8F1PZFiVRaHzuAZXeRhcmSJ1A7JZcz/TCZFunmDEniLIlTZTYoTEZYikgkgjX//BAA0M/1GirEOAO+BwtzhtQhlExyhjJF7jNknSs0nyqBmoXIasoc0kmWiSF/KCwJJXVOC5uYfah/VDtMpnCGjL0/ycJkzDFKVLUDAOEIK6fOn4BPhrp/FvuZF0lWEtVWQW66qOozFGQNQ5XOkRZM2PuTiI8RX7xIH1+jP3ssXTFk1aR7y4mhe++9F1OnToXb7cbChQuxceNG3W2ffPJJnHXWWRgzZgyqq6vR2tqKf/3rX3ncWyLbHIj1wACUPWIKMeiSH5DIrrr4BGun3Zbwii7XiBbsM8Q7Qx93e3DfK3sAKBOMtUKPXz55iuLnaU2VAICdnUNxHaoB5ZW20UWeH9SqxdjqaBJ9Z6yDrx7s96XTSDNXqJ0hdTUZYGz2FZEaaudH3XRRbsqYPIE6WViKnaP4Ng4t9frtO9JNoJZzmKx1vFhKDK1btw6rVq3CLbfcgm3btuGUU07B8uXL0d7errn9hg0bcNZZZ+HZZ5/F1q1bcfrpp+Mzn/kMtm3bluc9J7LFe9y0+rWXzJdua43JyDXsJBSOyBVurPcQAEVX6kIg5QxZ6IpeHWJ68u2DAIAJdUqRqR6Y+/1zjlH8PKO5CgCw8/CQZjWZaBOkoZhGr2BZabJeLgUTwof6RzUfZ7CcIVOJIVXOEHPRBEGQ55NZ7ErfCgRV1WLsffcGQ9jX48EzbYcAJE4DYI8ly1VjbRP4C4tJCcRQugnU8kWidc47gMXE0N13341rrrkG1157LWbNmoW1a9eipaUF9913n+b2a9euxfe+9z2ccMIJmD59Ou68805Mnz4df/vb3/K850S26I7NqTrnuLE4fWaTdD+r7Mon/NU0u+qKlttHSaW7cS6wol2tV4ml/nz5Kr0vtU6Os/TntdQAAN4/NCgl3KsFFMuHMeoMjSZxhiQxlMwZMqEYUr83mj2ZDOaAbNnbi5PufAHPbu/I3g4WKey7aZdyhmRn6NSfvCxtl2isBQvPehL1EeoZkdIKZo6tku6fkuC8ma4zZMXzDmAhMeT3+7F161YsW7ZMcf+yZcuwadMmQ68RDocxNDSE+vp63W18Ph8GBwcV/wjzoG589+urFuHC+eOx8rT8J6JqWdeJrrTyjWjFDtQ6Vv+UBn0xpNVwc2JdOSbUliEUjuCTWKK7Oq/MnmJTytEkOUPN1dH8i64hn+bjDDlnyDynX3X+klblndHFbcUft6Jz0IuVj74d99hLH3bh2t9vkS5qSh21M6Q3JiZRmKyuIhr26teY1wgAe44M49M/eUn6eX5LrXSbzRJjVHHfq7QTqA2W+psN83wbk9Dd3Y1QKITm5mbF/c3Nzejs7DT0GnfddRc8Hg+++MUv6m6zZs0a1NTUSP9aWloy2m8iu4z6oydkVkp/1rHNWHvJ8QUpYVefoARB6RYdPSb/bhWP1FjQQmLIpzE4dUyVC+fNHae4j/+89cTJ5AalMFWHUlNd5JM5Q+qZaXqYMWdIfSzzzhC7bVQ0ag1RZnz54bfw7x2HcfvfPkhjL4uPgKrpIquCHFT16EnU+qE2VmWp1X8NAD45ouwu/bnjJ6DabceyY5txVGOl4rFmLqE6bTFkwcINwEJiiKGePxWJRAzNpHrsscdw6623Yt26dWhqatLd7qabbsLAwID0b//+/RnvM5E9RgKxqq00v6jZRLQJipLuSpcdS4+RxfpjXz2pAHslI0ql9dY5KamdoaPHVGDj906XXBdGlQExNKZK2RVcnWSfahfqkQSzyQC52ZxezykGc+rMVE2m/pucCmcotb4xRpzIjw4PpbB3KNqGj1I1Wew9ZgN71bMX1dV+PLVliZ0hdei5qdqNt/7zTNx7+QKUOUXF+Jrmavk7k3YCtcGO2GbDMmKosbERoijGuUBdXV1xbpGadevW4ZprrsETTzyBM888M+G2LpcL1dXVin9E/ugc8OKmJ9+Vevio8SZZkPINf0VtEwSMrXHjrVvOxM47zkFTtX7Zaj6w4qBWtTM0qb5c8wq1gmtZoHcsNMWJIeV2osGZTgypmkzn9/HNIBPBnDqbmcSQ6j1WhMnE1N4nIyRrP8DzxJb9mHPrv/DGxz1Z+/1mgV2osA7gtWVRl+egKgk/YZgs5gwN+4Kan5FW3yuXXZTykPjvSXNVFpwhCpPlFqfTiYULF2L9+vWK+9evX48lS5boPu+xxx7D1VdfjT/96U8477zzcr2bRIZ87Q9b8Nib+7Hij1s1H092dZ5v+JMUqygbU+UqSN8jNZIzZCEx5A8pT9x63Z6NhMkMO0MG3x9pan2SMFkyMRRSJc2aAfXCx79XmVYHte3vxxNb9ivcnVQaAH7vz+/C4w/hq49sSev3m5lAWNsZUs8KU7c+4Kkuc0iVkVruULLxMHz+HR8mS3feo1XDZJbqJ7p69WpceeWVWLRoEVpbW/Hggw+ivb0dK1asABANcR08eBCPPPIIgKgQuuqqq3DPPffgpJNOklylsrIy1NTUFOzvIPR590C0dJ51llaTLG8j3xgZ41Ao2GJvZWcoWUgKAMp05tI1VKjEkDpnSBpXYuykrdW8UWufhn3BhOF7M5bWq99nzTluaThDvmAI336iDXuOeHR7hBkl1XESVkCdM8Tyf9QkCpOJNgHVbgcGRgMYGPXHXQT4krzXvBCuK5d7ELnTvKAz2vfIbFjGGQKAiy++GGvXrsXtt9+O+fPnY8OGDXj22WcxefJkAEBHR4ei59ADDzyAYDCI66+/HuPGjZP+fetb3yrUn0CkgFYiarKr83yTqOS10KQaBjID6jyDsTqhxgpncmco3u1QVZNJ709ysRgORyQHSV2GLu1T7Ao7FI7Aq5EIzgiaMWfIoZ8zlEoOiDq3Z8gbxJ5YAu9fth6Q7k/nT7eSqDdKUNV0sbZMe0Zdog7UAJdrlMQZ+vOK1rjHv3fOTJQ7RZw4pV5xAZHuvEcpTGaxPkOWcoYAYOXKlVi5cqXmYw8//LDi55dffjn3O0RkDfX08A86BnHCFGUbhNEkeRv5hr9iu/2C4wq4J/EkyhkKhML4+IgHM5orDRUg5Au1M8Q3W+SpdCcXQ+qwWCYJ1LwQ0HMDyx0iBAGIRIAhX0D3GJX7DJlHSMeJIZHPGTL+Po2oXAg+mZyvkCp0Q1KzEJBK66PvR02ZA7XljrhwV7IKxdpyJ/ZxvYR4WDPGS09swSLV+RQAFk6ux5u3nAmX3YbNH/dK96frDLHzznsHB+APhnUvHsyGNfaSKAnUuRtt7f1x25g5THbaDP0qxUJg18mJiUQi+ML9r+PstRuwcVd3IXZNF7X7MKO5UnM7ZZ8hHTGkCoupQw1SYrCBMBm/X3ond5tNkByrRBVl0niQAnRN10OdH8LvWyo5IOq/m88N4pOmA0Xo8qSDNLQ3JoxtNiHuArDSZcfcibUJX4c5Slrl9cnCu+x3OEQbxtZw1WRpXnCy78eHnUP42h+sk+dlnm8jUfKowzkvfHg4bptRkyVQ82Eys10B6U2t33NkGO/s7wcQ7dBsJthV7LyWWlx36lFYMKlOczs+L6JOJ8+CP/k7RVtc9ZY9hQ7U/KgDRwJHR+rJk2Cx1xoPUmjUIUVeOMphsuTvkzoxWq9qzB8MW6rlQ65gFVf8RdUlJ0R72zntNjy36hS8cfNS1FdoH+MMluvTrymGjB9vfAVsut4d30z05Z1H0nyV/GO5MBlRvKhLMd87GL9Qs0Up3bLPbMOvr2Za3AD9ajK+UsVs+UTsxL3i00dh+ZxxutudO2cc+jx+VLodiuG4PPznofXZOFJIDJYHagoJS+KN5GkZuVLPN7zTarcJCpGfSgK1OpyjtTgzhn1B3YRhHtEmFGW+EMCN4+De76WzmvGnry5GQ4VLMTojEex9TFRNZuT8VOWyY35LLQZHA7oh6mSYuagkESSGCFPQtr9fkWAJRE+W4XBEsfiwcIVZkk/5NCczhT0A/Wqp/b2yGEqWi5BvpBN3kvfSIdpw9clTE27Dv4bW60nNBA0stEyEJ6rqAeSQUqLF24zOEO+0xs0pS6E6qC3mODJ6PPpiaMF/r8f2W89O2j3eKdowGi6+SjJAdiXV57MlRzem9DrMKdUaEszCskaca0EQ8OTXlyAUiSTsbZQIsznkRiExRJiCC3/1mub9I4GQlB/S3jOC3tjJNd0varYJc2oo2UKZb/TCQPu5EudU+r3kAxYGzYbzxzsvWi5MKo4HE0PJqnpEMXmFmiSsTLRo8Mmy6pJ/9j4lC5Nt2tONH/31fcV9PcP6YigcATbuOoJzZus7gNLvjxkeRicOWAWWM5TpsTBrXNRBek8j7C2Lb2PfKZtNgC3tIJm5Zu6lgjX3migq1ItRhVOUTsgsIfPj2LBB1mvELAsJ7wyZrcxeL0zGDxI1mzPkDWZTDCUOk+nlVGnBHMnkzlDi3k77e0fwxJYDuvtUKHj3VV0ezz6LZP1q/ue5nXH39XoSD2TVGrKrxq7hDBcL/mB2hvYeNz7aN2/PkWFFfhsgv2f5cq6tGiYzz7eRKFn2qrqtOu22uG6+T287qNjGLM6QmXGI2iGbHm5i+LDPXOEHaRBvFvJpXBq9cnhYaMirMa5AjVE3J9k8uP957kN5/0yS96bGq1pMWeuCZE0P9/XEj9DhhbcWRsLd/EXGiMmO10yRqskyFBCsz1AkEt/QUqpezJP41usab3ZoRSEKTsdA/BwetRjao5q8bJacoWT5DoWELcydg17F4Ec+dDHs058wXgh8WewjlUxssGPMyJwsozlDyUag8FftZnKGeNTimXX4TiaGtP4erRwWHq25WWp4p0rdx8jqBEPx1WTpwB+XameIiaN8Jewnq3wzK+b8NhIlRa8qydIh2uImgB8e9Cq2MUuYrLrMvGLIzoVsjv/v9Xh08z4AQI+HD5MlXlxC4QgeeX0vduqMR8k27MTtzoKln0xsVLqiV9NG8qZYDlCy4y7ZCBTeoTKrGFLDnCFvEiHCV6TVxPreHOz36m0ee83UejyNmCysmykBVQfqdBEEQRJE6lAiO3eqBxfnChJDBJEm3aokS5fdFjcBXL20mCVMVuXSbp9vBtTW+y1PvYchb0AhPtlwWT3+9GY7fvjM+zh77Yac7CNPIBSWHJVsNNVM5h5WupM3SGSwAbJGw2R6lVe8A2BWMaR2KcqkMFni94nP82quji683cPxYbJ5LbXS7WQCC1A6HcU2n0yaTZaFpGN2bPLvVyQSwaGYINVrQZFt6irMe05MhDm/jURJ0aM6YWqFydSjOsySpGduZyj+PXp2ewd406JjYDRhGfibn/TqPpZt+IUxGwnUyaqOqgxOmQe4arJkCdQ6eVoMMzfpfPxrJ2FGcyUe+cpixf1Gc4b4KrRmnZlyAPCFBRMwMdbDJtlEdUApLItNDDHxn43zmZYY6h8JSG7r2Br9zySbmKl/ViqY69tIlCRxYTK7IJ2A9aZbm8UZOnlaav1A8onWVPQn35YT0e02AYFQBF1D+qEMI1fu2YIPmWTbNdHKTWHOkKGcIRYmM5gzpNe7iF/0IsmL2PLKSUc14PkbT0Xr0Q2K+6XvYhIhwgtAvQG7QPR9Z+5QsuMrGAorxHsyd8pqsPBrNipRpTAZJ4Y6YyGyhgqnaRrVmhVzrChESaMOkzlEmxQm8eqcgM0ihj47bzx++h/zsP7GTxd6V+Lg36MJMYt8c8zpqXCKkm2+pyu+Cohh5Mo9W3i5fKFs95LREtWy+5g8idxoNZmcM6T9vvF/lzrR1ayw8vdkrgyfND69uVL3vap0OaRqwWQ5Q+p+TUXnDLGcoSwUhEjOUEh+j5jQry7Lb+jqzs/NkW6rXX2zYo4VhShp1L1IHKINbpUzpG4Fo+V6FAJBEPCFhRMxvdlY2/x8wr9Hn1I5WGVOEQsm1QIA/m/rft3XyK8zlP0hvE+tXIKpjRW4/YLZcY9JOUOGEqiNhcnEJL2LfNzib5VhpeWxzyNZJRdzhuw2AZcvnowWnXEOlS67lCCfrJpMnQycaLyHFZGOqyw4oUwM8RcwTOhX5rnq9by5ciPNRA1IzQSJIaLgqFv2OzlniImhgEWuos0EnzM0rUk5/d1lF/GZeeMBAB8dHtZ9jXw6Q3IlWfbE0PGT6vDSd07D2ceNjXtMcoYMhMlYyX+yxnVS128docP3NFp6TFPS32sG5DBZ4veJ9cx5YkUrKlx2TGmo0Nyuym2X8kqSO0PqyqjEfYusRkBnHEc6aIXJWB+xCld+Q2R8mNsqjTJJDBEFR92y3yEKshiK2eJW+UKZCT4Poa7CiUtPbJF+LnOKUkLlkQSN8ZJ1Hc4m7LPOpjOUCCkUa6C8ezS2TbJ9syfpM8TCvmsummPqHlU8Ve5oiKVPYwgoT0i1sC+YXKfzesadIXUieudg4lJ9q8EEZDbC/loJ1EzoV+a56tWRoO+RWSExRBQUbyAUF6ZwiDa5O3BsMbbKF8pM8Feb9RUOXHHSZOlnt8MmDXfs9fh0q5/yGSZj+SD5EgluR+IkfZ5RgyG8ZPPOmDOUL8GXDSY1lAOIiuZ3VINYeZgAZKHCxVPrNberdBl3htSiUt1vzOpIg1qzWU3GHXts3E5lnp0h0SYkbTNhNkgMEQVFa6q1w26LW6hIDKUOnzNUV+6UGuEB0XEXDRUu2ITowEx1RR8jn/F+TywMU56F7tNGKEtSscjjNdgZmwnQ2/72AU7+8YvY0aEcnMkWfytV9vDHzQW/eg1XP/SmpniWc4aiy8rURu0wWSXvDBmoJuM52Je4o7XV8Gep6SIgh6aUYbLod6oQLqRW2M7MkBgiCkrnQPzJTZEzFHMLrHJ1YSZ4Z2hGc5ViUbMJ0Ss31i02UaiMcebdr+CVj45kf0djjPjy6wyxY8wfDCfstQRwlW5Jmy7Kjx/sH8UbH/dov06ehmZmiwpOBL688wg+6Y7PMwtFlM6QXidil12Uh78mWSjVztDeHk/SEn8rIY3jyELTxURiiBUL5BPWRsIqKQ7W+kYSpuCxN9vxyxd3ZeW1Dmm063eIAsqc0UOTnKH0EQQBbT88C1v/80xUuOyKihKWq8FKboe82vkgfFns7q5hfOl3b+Zsf/PuDHHuTLJwIFuA3Un2Td08T10KLoctrJEvxLj3ioWKn7Xyh9Q5Q1rtEVrqoxVmbOFO9r4zkVpf4URjpQvhCPDR4fyMhskH2RrUCiQJkxVgeCpzu/QqK7sGvbj8N2/gn9s78rlbuljrG0kUnHA4gpue3A4AOGf2WExryqykXMtpcGg4Qz6LXF2Yjdpy+eqcX5zYzKhkXZjzWRQr5Qzl6cTNV7yM+EMJHSmjOUPqlg9qF6OQYYtMmBLLG2L0aYRV1TlDALD24vl4pu0g/vP8Y/FM2yEpid+wMxSSX7OpyoXuYR/6k4yQsQqRSIRrupibajLWZ6gQzhD7m/Rc/dv//gFe292D13b3YO+Pz8vnrmlirW8kUXCGuEWzZ9iPaRlUBwdCYfx56wEA0Ss/lrfiEJU5Q5FIhJyhLMNmRiXrtZPPfmnsKrY8T8meNpsAt8MGbyCc1KEw2gNJnfuhdoaGLeoMVbuV1Uh9Gv1+pJwhbmG/8PgJuPD4CQCA1WfNkO436gxJzolNkBwnvYaWVoPPx0vW2dwIWn2GBmOOb5U7//PCkrWZONRvrvwvCpMRKTHA2ePJhnwC0ROkXgdSvjLky0umSLeddhvKY+7AaCBkmaZdVuD+KxbCJgA/vijaIZYtynojKfKZq5VvZwhAXD8rLYZ9Qfx7RxeA5InPcc4Q97rBUFhKoLaaGKpSOQvqMFkkEpGEi5GGqOx99CYprQ9yAou1iiiW8wF/bCRLzDcCO2d6uAsb9r1Wf375wJGksjKfPcyMYK1vJFFw+kflK0L1GA013kAIy362ATPHVuHXVy2Ke5yJoZb6MkzhKk/4PkNefyhuQb7l3Flp73+pc87ssfjg9nOkxYj1H9FzhvLpyOXbGQKiYqgPgYRJuX94fZ90O5kYUoc7+EaFHp/8O6wWJlPPzuLDZOs/OIzV69qkGWJGJrAzZ8iXpLSer1ATJWeoSMRQ7JgTbUJWnCGW/zfI5f+xXEC1s5cPkolXs7n91vpGEgWnn7siTFaB9OYnvWjvHUF77wgikUhcQmXHQFQMja12K8IL0T5DcgI1/6V56Msn4PSZ1ujca1b4BZ1dMep1YVZfvWXjpK0HE2TleSw7V4990YK/AEg2XVzdSZgPkw3HhJFTtJluYn2q8OHyrz6yRfGYEWfIZdQZ4nKGHElyUKwGn4eWjVl8rFp0YNQczpDcgNQazpC1v5FE3uGTFx97sz2hulcnqKrpjImh5mq3otRYnTPETph2m0BCKMuwNv1azlAkEokri81Goqcen3RHB8ZOqCtPsmX2MBIm4/OEkk24V7si/Ot6pORp6/QY4vnm0unSbb0ByoCx0RLs+5686aKcM8TaFuhVJ1kNqeN6lqonq2OCZ3CUd4Zig1oL4AwlqyZL1n0835AYIlJigEuc7Bz04qfP79Tdlr/a0Uq4ZF/aunKnYsFx2W2KUQmDo4W7uil2EoXJ8tkfxBcMYXdXtHfNseOr8/Z7+XCsHrwbenqSeWKJnKFCVvZkg9VnzcDtFxwHILGjY8gZinWgTtp0kcsZchRbmCwQPR6y1Y1cHSYLhMKSGC+IM5TEyTNbmIzEEJESwz7lyevBDR/rbstXivRr9CXxcOMX+KsjfhwHAPSoKp+I7FGZIEyWz5PVoX4vguEIyp0ixsdmpuUDI12oWZXjf543S9G4Ugt1+IsXQ0z8F+IqPVu4VS0vtEjJGUpyjIWkMJmcMxSweDUZK0IZ9Rubd2cUdlyx44x3MQvSdDFJNRmFyQjLEAiF8ez2DkVukJHRBQz+YNcSQyOxHIoKp6g4IThEG9x2+eeu2O/P97DBUiBRnyGtk1U6+Ro7O4ewK0mjPN4lzEb+hFGMzCdjrqZeR2Wt12PwFwSsYKC5On9iL9sYeb9ScYb8wbButSnAOUM2QQq7WNkZWvvvjzDv9ufxj3c75JyhLIXJaiRnKPpdZsnTZQ4xK+M+UiWZM6QnkgoFiSFCl6e2HcTKR9/Ghb96TbqPr45JBr8QXPHbzXGN2lh1TXmcMyTAZhOknCMmxqosVoFjBaTSeq0wmaYYiqQkiEb8QZy9dgPO+tmGhM9jbRrybeezbteJnA4pvGXg+HPFOUPy+9opiSFXyvtpFvjwNYA4ISMI2p2n1fA5gokcAkWfIWlxNdcimgpr/x3t3H/L09ulYyN7YbLo8TmgcobY/fnGniRnKK8dXQ1gWAx1d3fncj8IE/L6nuhcpYP9o9LCqJUIrTuhW3X1+HTbQcXPUkKpyhliJz0mkI7EwmSUM5R95DBZvHOnFyZLZZJ9D9d+IdGix/IcqpOEobKNvLjr/03sfTBSAaZ2hvjvy+HB6HHcVGVdZ0j9fg2qwqtGm3S6OOc30XvPN3IUi6jpot0mSH93tsbPsO+OPxhtIlrIhosApBwvvWqyMHew6K0h+cSwGGptbcWePXtyuS+EyWipl6t62ntHAGjb4x6f9slMnQ+gziVgs6jUOUPse8FOvMwZopyh7MPcjj1HPHEnJD3xkkqsn3eDEuUgsST5ZDk52cZI2IclkhsRQ2pniHecjgxZP0ymnhnYNRg/W9AIDlEAM5ASHRdBLmeI5aBY2Rli2ARBEsrJ5t0ZpdJpl97TQW+goGX1gHxR69f5vHgxZIZhrobF0NKlS9Ha2orNmzfncn8IEzHChU7YVYZWOGFYJ3SmrhRR5w1JHYddoiJHiF35MTHUFVtErNa11wrwJ8p7XlAO382GM8Sf5BKFyQYL1BxOSqD26+8bex94N0MPtTMUDMujZJjgqy23bu4bew/YeaAzTTEkCHIOUKKFMMTlDIlSR2PriyHRJouhbPXVstkE6fuzdW8frvvDVgCFc4bkMJn258t/imaoLDMshu6//37ceOONWLp0KZ5++ukc7hJhFjycyGEJruwLfPsFx0n2rkene7F60ewdUecMsSnldtg414gl1rGFhfUjKtSXupjhOyH/4sXdisf8IW3Rk4ozxIeJEjtDLEyWX8Gr1Wfok24PTv3JS3h0c7TztCyGUneGAFk4DFl0LhkPE4/su/3BocG0X8tlYLxGgMsZchRRmMwmCFJuTzbdUPb9+fqjb0v3FcoZksJkGp9vMBRWhFQtJYYA4KabbsIDDzyAyy67DL/4xS9ytU+ESeBLM9kXly0aDRUuNFQ647bjUTdU00ugVi8O7GqQfbH3HIk24xtTZd3EU7OSaGHWDZMlaZTHw/fvMZQzlG9nSCNnaNW6NuzrGcEtT70HQG4OZyhMpnGVPxLrJ1MMLSJ48TjiD+IRblRJqjjsyinr7x7ox+aPexTbKHOGYuLJZFVI6XCwf1RqS5JNp1Dr+1OoVg7M+dNqhaBOoTBDmX3K38rLL78c9fX1+OxnP4s///nPWLx4MRYuXIgFCxZg+vTpyV+AMAVdg140VLoSlsF6FGGy6G12lVvuFKWBmsmcoYYKJ3o8fvSqwmTMeVInELIrRXUpM4mh7MM7GeNq3Hjk9b1YMKkOsyfU6J6gko1Q4DHuDLHKlzznDGlUk72zv1+xjZRAbaA8Wc8Z+sMb+6QWEVauimRubSAUwbE//BeAaP5POnk8/HiNTbu7cdlvNkMQgLduORONldHvuiJnSCyupouMrDpDmmLIfNVk6hQKM4ihlJyhvr4+3HHHHfjyl7+MMWPGYOLEifjnP/+JK664AjNnzkRNTU2u9pPIIm/t7cWJd76Abz6+LeF2fGL0G3t6EIlE5HJQpyjZr3piiB3g7MTG5yBFX0tuuggAn18wEY2VTnx+wQQAQG25SgxVkhjKNoIg4IfnHwsgOivuh8+8j/N/8SoAWQQsmFSL9247G9OaKgGk5gyNcCe9RLkhA1JDwsKHydSkkkDNl4wzRvwh/NfT70k/Wzncq1UGfuLU+rRei72f/lAYOzqjfagiEbnJJSALH4eitL7wC2c66PVTqilP3r/KKFquY8HCZAmm1qudITOEyQy/S6tWrcJvf/tb1NfX47/+679w7bXXwuWKLk5erxfbtm3Dtm2JF1fCHLDckH+824FfXaa/Hd+I7x/bO7DwtTo56c8pSiXCOw8PYfmccXHPZ+EFduXDLzi+YFg60TFn6K4vzkMwFJauKOpU9jE5Q7nh5GmNmvfzJeWVLrs8aTwFZ8hr1BkqUGl9RezYG9JoLQAA4XBEcj2M5QzFiwW10LJymEzrPZhYWw6gJ37jJEgJ1MGwYp4WL7ZZiEUsgtlkeu5HZRZn1Wl9PgVLoE4Q1lTnk5phTplhZ+jvf/87fvazn2H37t24/vrrJSEEAG63G62trVi5cmVOdpLILix3IRkjqiqx2//+gdSNt67ciVNnjAEAbNyl3YOKLSLsyoT/AvBuUrlTXhzsXCiijrtisgnRMA6RfdSik+GTxFD0ZO1WNdwzAn8MJawmK9CoCiawj+h8J3g3y1jOUPw26lEn2aoeKgQ2mxDnfmn9zUZwSgnUYUkMA8qFkY3j4GeTma1zsVH0Gntms1WAupoRKLwztP3AANp7RhSPqcWQGZwhw0fxzp07ce2118LhsK7FS0Tp9cQPTdVC60qGLYR1FU5MaawAEJ8YzWALCbva5xdR5jCVOUTdvCU+Z+jY8dWKyicie+g5FeoqKvb//zz3oeHy+lHuM0/sDBWmWy5zN7sGfVIYg53EAWU7iFSbLrLjd1+PR7GNzcC4CjOjteAyUpmkwt7PQEgexgwozztM+IiK0vrCL5zpoHUxUFPmwCnTtZ3ZdNAK0xYqgZqFNV/d3Y2zfvaKIkyovqCyVJ8hUbTu1QyhpNugM6S3eDntNlQ4RSm8pdWVGgACseezPBD+ik9uuKh/XLUe3SDdPnNWs6F9JlLHbRc1FzG/qoqKLYKfdHtw9/qPDL22OjSqx2AOyoyNwJwhXzAsCTIb92bw3xUjCdR8v6zjW2oBAO9z5effP+eYjPbXDKjzhhZPlb+nrDGiERRhMs4Z4s87cp8huemiVROotRb8zTcvVTjjmaIVpi1Y00XuWPAFw+iLXVhEIpH4BOoU3OZcQZfaJYhRW5YtXqvOnC7N1AGi1WGCIEg9R9ThNPn3RJ/PYta8Tcz3GNJjXE0ZfvKFufiwcwhfP+1oQ/tMpI7NJqDcIcKjErXs5M36wfD5CJv2GBvPo+hAnWBsCzvW8p3fUOYUUeWyY8gXxJEhL6rddsV+soafTtFmaOaW027Dw18+AeFIBNsPDOKFD7vwXkwMjatxF8VxzDtDP1h+DM6dM1b62S4at4acUtPFiDJnKBifdO/gxnFYtbRefd4VufmL2ULLGdJq95APZk9QFlTt7fHgnQP9uHFdW9zFrRmcIRJDhC7spPTFRS34sGMIz73fCUC2/9VDG9WwLz8LfXj8IZz/i434wTmzpFbsyUJf/7GoJcO/gjBCmdMeJ4bY1RpzhjoG5G7DRs9d/FW8ntPIwrZ2m1CQMuD6SieGfEH0jwQQCEUUzeD2xXIdUlm0TpvZBEDuv7WjIyqGrNxsUY8rTpqsEImpTEdnfYYCnCsHKB1EFo6NTl63dtNFdZis3CkaEtipoOUMqccg5YulxzThwvnj8XTbIQDRcPGN694BAPx56wHFtpbKGSJKC3UVzeRGeU4ZS2RmYTJ/KKwZx/eH4q/23zs4iCt+u1kxpJUoPOpwZTAUlp2h2KI1sa5Menx/74huqTAPvwDoJVAzMVQXcxzzDQsjDHmDcVeoe7uj+T5G8oXUTOJm+/G/x+rwAledDO5IyRlis6vCONgnJ9jyIZNRbn6XaPHZZOoF38h4l1RRO0PzWmpxzNiqrP8eI9hsAtZecjwunD8eQDQvTw9LVZMRpQW/KLgcouLEfvykOgBQDFcd0UiolcNk8YvAwf5RAEB5EV4tWxF1uNIXDMdNa1915nRcemLUqRv2BXGgbzTp6xpxhnpiYqihInv9VlKBJZgOegNxuQyfxJyhdMTQlIYKxc+VFu4vxMNf+KiTwdWNUhPB3tP9vSMqZ0j+DFg/GrddlEJwVkyg7vP48V/PvJd8wwzhBdaUhnI8vXKJokK3ENRXRPPy1OOYeMgZIkwLb1U7RRumNsondlZS7xRtUhxfq2yUiSGtUNgd/9gBAKi38NDKYkLdBdwfDHOl9dHTxLSmKqy5aC5mNkevNPccGU76uvxVvH6YLHrFmMpCmk2YWB/0BuOSvDd8dASAdrPBZNSWOxQXAsXiDHVrVI/ef8UCzGyuws8vPd7w67CQGgsjMvjPgJ1XypxymMyKpfX//fcPsK29X3FfLkxQ3hmqdNsL4rSqYWObeof1xZAZOlAXx7eTSAlBAJJFONjVmSBEre/FUxtw3alHYe6EWikxThCiibdDvqBmRZlUmp3gyoQaKZoDtb3u48SQ2s5vqHQCh+Wu0Yng8zv0kiR7YifJwomhqCAf8gZ0T8rptHUQBAFjKl1S7pCVx3DwaInac2aPwzmz4xuvJoKJIT4XDVAujOw85HbYLN10cfvBgbj7sp08DSiT282So8a+11v39eluQ2KIKAg2QUAopoYikYjm1QOL27vs0SoaUQBuWj4rbju3MyqGtJ2hWN+WBF/6RhqxYQr2qZqi+TXCZAx2ktUb0MvDL1x6JdHsdfJdVs+olsRQUPqbGyqiSdXs53QXFj6UXFMkLqhTtMEfCmNCbVnyjRO9Tuy4Urf64EOVo1w/MnYBZ8XSeq1QVS7EEP+aDRXmOLey5rkfd3viHrPbBATDEezvHYl7LN9QmKwE4aWPnuUszWNKEm9m4ZVzf74R6z84rHyNICuL1X8NaqRoDj4dC30yfMGQ7jFQxYmHZPDHl96xxq4KEzXzyyVyAnVAciJcdpsidJjuccqH14pltt4frjkRS45uwO+/ckJGr8OOq25V+ERRTcY+D645q9YUdLOjlVjuzEECNV9Gz8JThWZyQ7nuYzNjyd2Pv7Uf9768O1+7pAmJoRKEN4L0KnwkZyjJAsUn3v5m48eKxwJcjxA9iiWPwuqsPO1o/GD5MdLi7QuGMepnvaCUxwAvHpIR5BYuvZJoXoAUAtYhfWBUzhlyOURFpVS6xynvDBWLC7r4qAb86asnYVpTZlVKeknpmjlDDlG6qLKkM6RR3p6TMJndfMfbrHHVcRdbQNRt5Rt2igXOb7KcGLr33nsxdepUuN1uLFy4EBs3btTdtqOjA5dddhlmzpwJm82GVatW5W9HTYzAeUOBoN7VurEFis+D2Hl4SLq9rb0PXUO+hK9xVetknKsx4JXIPxPryrHi1KMVHZk9vthQXpUrUu1OL0yWzBnKRamxERpjV9BHhryK8HCZwhlKb9/cJrxSNwvqi6QKjSaurIeZm3eGLJgzpBUma67Ovlg5eoxc6KLVgLFQfGvpNOn2zOYq3HPJfPz+KyeinQuPfWnJlALsmYx53i0DrFu3DqtWrcItt9yCbdu24ZRTTsHy5cvR3t6uub3P58OYMWNwyy23YN68eXneW2ugZznr5YuocdjlExq/OH7u3k3yNqIt7ovptNtw+wWzU2rSRuQe9nn7g2FpUVL3gqpMyRnicoZ0FjHWWC/dgZ+Z0lwtzyfzcSNIeNez0pVevk95ETpD2UL93Z9YFw2n8OcRdmy4HTZLl9armTexBj/8zHFZf92manmYtZHxMfniqMZK6bbDLuCC+ROwcHKdYl0oVJicYZ53ywB33303rrnmGlx77bWYNWsW1q5di5aWFtx3332a20+ZMgX33HMPrrrqKtTU1GhuU4rwAkg3TGbwap2vSguFI4oZQwyHaFPkTvzthk/hle+elsIeE/mCuXi+YAjDOiNTWM4Qe5wnEongpiffxS9fjI5vSSlnqEBhsqaYG9Y15FMMp+Wdoco0nSF+wScxpER9oTUh1tSTP4coO1BbN0zm4b4rLfVleOaGT2WcgK7H3V+ch7OObcYXTNS9v46rFOXPJ989eybOnNWM/1vRWojdUmAZMeT3+7F161YsW7ZMcf+yZcuwadMmnWeljs/nw+DgoOJfMREOK8cN6IXJpKv1FBeofo+2GPrsvGgX0mlNlZgzsQbjanJzIiAyQ+kMRY8BdSUV35dHzfuHBvHYm/vx0+c/gj+o7Eyut4gZzU/LFexqetgXxCexCfMuu6gQ8OkmUPODagvVOsCsqJ0L5tC9trsHB/qiHc5HJWfI2gnU/IUDCz/niosWTMSvr1pkmtJ6xs8vPR7jatz4IifSJjdU4DdfWoQTptQXcM+imOvdSkB3dzdCoRCam5UD3pqbm9HZ2Zm137NmzRrcdtttWXs9s6E+kej1fmFT5ZMljqr7FfWP+jExrBQ6TtGGHyyfhRljq2j6vMmRnaGwPExX5YpUxK7stNop8Ff1B/pGDDpDhU2grnTZMa2pEru7hvG7V/dK+8I7F+kKmRFuERQLNCPKrKjDZE1cz7HVT7yDP16zGOyQcTtEeWq9BXOG+O+KlqNaCnx23njpotiMWMYZYqh74uj1yUmXm266CQMDA9K//fv3Z+21zYC6YVlQ5yprOHbVn+zqIgLl6/WPBOIEl12MTri/fPFk6eqPMCes3NfHOUMVqjAZCx95/PEn9SNDcs+YfT0jKmcoceViIXMGrlg8CYDc88blUIbJ0i2LVw+/JWTUYTL+3LBlb6/CVYs2XbTu1Hr+otMMoyeIeCzjDDU2NkIUxTgXqKurK84tygSXywWXq3hj++qrc70v5pDPmBhSXzEPjAYUguv8ueOol5CFYO7MsDcgHSvqSiomjkY07P7Dg3I34QP9o4rQmFmdIQCYoRpm6RSVfYYa0hVDJeoCGCGRM2QTBKn5ok2Ifh4OCydQ88NnCXNiGWfI6XRi4cKFWL9+veL+9evXY8mSJQXaK+uhPpHc9fxHeHDDHsV9D7yyRwoXVCYJk91y3rGYN1FOTu9XiaG1F8/PbIeJvMKu1ntH5HCXOoGahc1GkjhDvkBIUQatmzNU4NJ6ADh6TKXiZ5ddREud3CyuMc2y+K+fdjQA4IL55g0PFAq1M9SkKjXn84UEQZCcIavNJotEIorhs7lKnCYyw1KX7KtXr8aVV16JRYsWobW1FQ8++CDa29uxYsUKANEQ18GDB/HII49Iz2lrawMADA8P48iRI2hra4PT6cSxxx5biD+h4KhPJK98dASvfHQEVy+ZCqfdhvcODmDNPz+UHk82T2lCbbQy4vt/fhfrtuzH4GhAYQlTnoS1YO7ML2LVYNVue9xnKDlD/lBcmHpIMX08bMgZ4sunC8WYShecdptcTeawYQ4n8tlIgVQ5f+54zB5fg4l1tACqcar6DDVUuvDlk6fgodf2QhBkMcQS2ZmTZLXZZMFwRMp9WnZsM753zjGF3SFCE0uJoYsvvhg9PT24/fbb0dHRgdmzZ+PZZ5/F5MmTAUSbLKp7Dh1/vDxFeevWrfjTn/6EyZMnY+/evfncddOgtyCNBkKSGOJJ5gwxpAqj0YCUh+QQBVNMTSaMw8QQS4yf1lQZtw3LpQmGI/CHwgpHhx/Y6wuGFfljeomvZnCGbDYB42vc2Bub0eay29B6VAOuXjIFzdVu2DIQ9VMaK5JvVIKow2RlDhE3njUDD722F4FQBAMxd5LlkrE+Q1YrredTEe655HhFLhphHiwlhgBg5cqVWLlypeZjDz/8cNx9kWTj2UsMvXi7NxBCTZkDOzqUrQSMNpsrl7rHhqRyfWqoaD3UgmRGc/zIBT6XZsQXSiCGQgZzhmQ3ppBMqCvjxFA0NHPrZ7PfGI+Iog6TuR02uO0iBCEqxlkHe+YYWrW0nh8vkqyJLVE46JMpMfRa2bNQxf6+UcX9RscQlHGhE3ay0prHQ5gb9cn6uAnxzUodolx2PhJQJlGPBrgwWSCsCGmEdS5M0u1plW3Gc72vaNHKPeqLJbddhM0moDJ2LjkiiaFYmMwmu5ZhC7lDLF/IIQqUNmBi6BtfYuiV0rP4/IG+EcX9RpP9mFswGghKCyA5Q9ZDLWAXTKrV3E6aI8VVS23d14fXdvdIP/uCYcXxZtap9YwJXF5PoYVZKcCfH5x2mxSKZCH3I8NKMSRyOUZWcofkeXcUHjMz9I0vMfSSD/d0eXD+Lzbio8PDivunGsx3KOPDZNK0ejq8rAZv6V96YguOHVetuR1LauXDYp+/T9kJPtqBmq8mi1/AgiE5ybrQAmR8LYmhfMK/x/woFjbupWswKobKVM4QYK0kalZQQseUubFczhCRGXpX5//vHx/g0IA37n6jnXf5CiMmhuwiWcJWgy+XX3PRXN3t2OgMdqLX6lflC4aUHag1FjAv97xCXzlPrOXDZHQVn2v4iyU+qTjeGVLmDAHWKq9nzhCFXs0NiaESQy+BmhdCjZUuLDuuGfMm1hiuBpPCZH55ASRnyHoYnZskje2Ineh7Pf64bXwGZpP5uJyjQl85H8O5YOrO6kT2cXAXS7wrx8TQ4dg5Saom48WQhRovmqGpKJEc+nRKDL1ZZDy15Q7c+bk5uPiESYZfVw6TBREIyqX1hLUw2jGZndiv+O1mvLDjsDTGgscXDCsS9rWu5llYzinaMipfzwb1FU7c/cV5WDy1HuccN7ag+1IK8E7JNK7pJQuT7Y/lL7IeTzabAHaIaAnrgdGA5ry8QmOG1hFEckgMlRh61WQ8cyfGVxAlg3eG2Owgu40OL6tx+jFNAJSjEbTgT+zX/H4LuobiQ6xef0ghvjWdoaC58ikuWjAR665rTXv8BmGcmjK5bcf0Zl4MySF3AKgrl7ezx9xm9XyyfT0enPI/L+Ki+zaZrg+R5AwVuHUEkRgKk5UYyYYEth7VgG8tnZ7y60p9hgIhycImZ8h6XHJCCxorXVgwuTbhduoT+46OobhtBkYDip+1nCGprL7AlWRE/qlyO/DHaxbjpZ1d+MLCFsX9PHVc3qLDJsCP+DDZw5v2YtAbxGDHIF7b3Y1PzxiT031PBT/nfhLmhcRQiZFMDN12wXGY3JB6x9wyjQRqyhmyHnbRhnNmJw8RqU/se44Mx23TP6rMI9KqJjObM0Tkl09Nb8Snpjcq7qtSdb3nR6GwJOpD/V7FeaqTy3nc1+MBYB4xZJamokRi6NMpMQJJcoZ46zoVyll1UTCMFX98GwBVkxUz6hP73m5P3Db9I8mdIV+AQgiEkmqVGOLFEcszuvTXbyjy2/gE/tGAufKGqM+QNaAzUIkhWbY6V+LV7vTEkNa8HXKGihf1iX1nZ3yYzKdyIbVyObyUXEqoUIfJ9Jzq3V2yG9k3wokhv7kqzaiazBrQp1NisITWuRpjFpyiLe3J4S67DepiIBJDxYv6xO7xyxPGH/nKiZrP0eoz5DPBxHrCXPDu9b2XL1AMC+b19OFBOTTW65FdSNM5QxQKtgT06ZQYzBkaV1uGRZPrFI9Vl9nTnjIvCALKnUp7m2aTFS96J/bLF09Cc7Vb8zErVJMRhYcfi3LunHG627X3Rkvvw+GIyhky1h4iX/iSuPGEOaBPp8QIcJVeT1zXim+eMU16LN0QGUMdKqt0U35+saJ3Ync7xLgqQpb0mrCajMJkRIzWoxrwv5+fi79/41Nxj0W4Yb+sQ/WwP6gQ2uZ1hugYNzO0WpUYfu5K3GYTUOGSD4HqNJOnGeUqMcRXgRDFhZ4YctltceHRCqeIQW9Qs5qMJcFWuuhUREQRBAFfPKFF8zFeTrNz2Yiqa/pogHKGiNShM1CJoS5757+gmYqhMlWvmNkTtId8EtancyC+4zTAnCHlSb/K7cCgN6jIGXrv4AD++MY+advqMjoVEcnhjCHJcfGowmJm60ItVZNRXpypoTNQieELKRuA8QMp0y2rZ/DO0Hlzx+GCeRMyej3CvAx65YTV+gqnVNrsctjiwmQ1ZQ4c7B+Vjj0AuOTBNzDMlUZnKsSJ0oOJDPUImdGAuXKG/NI5l8JkZoakaokRCMaGqNo1nKEMc3z4BOofnn9swWdNEblj1ZnTMaWhHGsvnq8Q0W67KB1bjIbKaLg0EApLOR/DqgUsUyFOlAb/73Ozpdss/KQeLkzOEJEO9OmUGP5Q9EQhO0PyIZDpgiRy4ker7xBRPBw3vgYvf/d0XHj8BIWIdjlscKhm0rHqskhEO4kayDx5nygNzp87Ht89eyYALmdIHSajnCEiDejTKTGYM+TUcoYyFEN8r5hymjVVMvDHTTSBWukIjuVK7f/adgiRSEQxfBOguU2EcdjxJOcMKS/wvFw1mTqEVgiomswa0BmoxPCrcob4AZmZXp3XcwMV7bS4lQwKMeQQIdoE8O2qmmtkMfTt/3sH/3r/MKY0KrsKBzQqzQhCCxZuksNkUcHTGAvHsjDZ/a/swXE/+hf+/cHhAuylTLKu/4Q5oE+nxGBfTHb1zl+Rj61xZfTavBgiSgdeRLvtIgRBUFSUjal0KbqTv7W3VzEw+Nhx1fjMvPF52VfC+rBzluQMxcRQQ2X0/MXCZj/+54cAgO/++Z1876ICCpNZA6omKzGkJncxR4i/WplUX57Ra8+bWJvR8wlrwpfFs6tzh00A6wlcV+6AQ7RJi5dNkBeyx756ElqPbsjr/hLWhp27WGLySMwJYon6XlXOUEBjDEw+8Qaoy7oVIDFUYgx5o1dN7Gqer7yYWJeZGDrr2GbctPwYHDc+fu4ZUbyUO3gxFL06d9htQOzYqnTb4bTzYkiQr5apwoZIESYqpDCZn4XJoseePxTGewcHpO39ocKGYFnlJHXkNzf06ZQYA6PR/jCscmx6szwE0Z1h0rMgCLju1KMzeg3CevCDNdlxxYfJyp12RThWEAS53JiulokUYccMEzlyzpAc5j//F69Kt4MFFkNDsZ5cVDFpbkgMlRisWR4LbTRXu/HCt0+lPi9E2rArdABSbykHlyRU7hQVYxREG1XYEOnDjhkpTBbrM1RX7oAgKLtUA8pJ94WAufFV5AyZGrosKzEGR+OvUo4eU6m4qiKIVFg4uT7uPn4BKnOKCvdIgEBJpUTayNVkynEcFS573EigQhMIhaWcpipyhkwNSdUSIhgKSz05aPwBkS3OPq4Z91wyH3O5BPogVypf7hAVc8n8obDsDFHOEJEiTECzYhAmNipcIsocovSzGRj2yn2OyBkyN3QmKiEGuS9mpqM3CIIhCAIumD8BU7neQXwFj120KcTRkDcghTIoTEakSpUreiHnC4bhC4aknKEKpz1p5/tn2g5i6V0vY9fhoZzvJyCHyMo0BhgT5oI+nRKiezg6abzKZaemiEROUSet8uKof0Qe8kphMiJVqtx2afRP/0hAmk1mJEz2rcfbsOeIBz94cntW9ymiTlSKwXI0yRUyP3QmKiF2dw0DAI5qqkyyJUFkRqKk1X++1yndpjEcRKrYbII0zqXX45dyhsqdIsoNzkTsH/En38ggW/f1Yv7t6/HElv1xj/V6or+ntpzSEswOnYlKiI9i1vB0EkNEjmmqNpaQb7MJyTciCBV15dEGi30eP5czZNftgh/OYUnZtx5vw8BoAN/787txjz3TdggAMK6mLGe/n8gOJIZKiFc+OgIAmDOBmiISuaVF1cAz0+7mBMFTFxM9PR6/1Menym3HmCptEf6T53cqfhaE7InwYZ1hsIf6R/GXtw8AgO5+EeaBxFCJsLfbg23t/bAJwPI5Ywu9O0SR860zpwMAzps7DgDwyFdOxAlT6gq5S0QRUR9zhnZ1DUv5aPUVTl3Rcd/Le3K2LyEd1+kjLkm7z5O9sByRGyirq0T4947o5OaTpzWiqcqdZGuCyIwTptTjtR+cgabY4jSlsQIrTj0ab+3dUuA9I4oB5gz9/IVd0n0uu4gxCfql8cOBsxmc1QvB7e8dkW5/7dNHZfE3ErmAnKESoS+WMHj0GMoXIvLDhNoyRTmxkyrHiCxRX6GdkDw2QW4OG0UEAGGd6q904LUQ3419b09UDH3u+AlYfBQNIzY7dHYqEUb90auiTOePEUS6UOUYkS1YArWaE6fGd0Nn8GIoV40ZB7i2EV1D0VYmx42vzsnvIrILnZ1KhNFYt1aztasnSgcXHXtEllCLIdZ3qL7Cic/OG6/5HF4Mse7VmRIMhaVza/R15VBcT6yvG406sgYkhkoEHxNDTvrIicJAzhCRLdQl9E+tXCLd/vmlx6NZo7XDoEIMZWeSPWv4yOCFUc9wNDWhoVLbxSLMBZ2dSgRyhohCo84Z+vLJUwqzI4Tl4avGvnnGNMVcPEAWIjxHYk4NED0f6nWNNsrT2w7i9LteVtzHHKdwOIKdsWqyhgpyhqwAiaESgYkhClUQhYIfvTG9qRI3nzurgHtDWJlZ4+Q8HK1p8CdPawSgbDDbNehVbOMLZuYOrVrXJnWYZrDz7C1Pvyfd10jOkCUgMVQijPrJGSIKCy+GPj1jDA2uJNJGtAm4/4qFOG/uOFy6eFLc4z/5j7n49lkz8Oi1i8H6Kx4e9Cm28WUpVMbzxFv7cemDb+CxN9sBAE1VLjRQzpAloD5DJYI3dhVEYogoFHyYjI5DIlPOmT0W58zWbiDbVOXGN5ZGG3+67SJGAyF0DSmdodFACDXI7sywJ7cdlG7bbQJe/f4ZUnI3YW7o0qxE8DJnyOAgQ4LINi67fOxRzyEiX7gc0WONlbozMqkoM5JvNLGujI5zC0GfVInAYtluB33kRGHgFwYKkRH5wh0T4V2qMJk3mL4Y0ptHxjOxjubxWQk6IxUQbyAk5fLkGlkMkTNEFAY+XOAQKXRA5Ae35AypwmQZnHv7PIGk2zRX09gjK0FiqECEwxGc8r8v4cQ7/62YmZMLIpEIPLErmXInpYkRhYfCB0S+YOFZNtCVkUmvoUGvLIYuPbEFF86Pb/RIk+qtBZ2RCsSQN4gjQz4MeYPoHPAmf0IG9Hr8Uvv5cTV0tUIUHmrASOQLPeGdSc4QO58eNaYCay6ai1qN8SB689MIc0JnpALBX1n4MohdG6E9Nj15bLWbwmSEKVg0RX+GFEFkE7UYGhsLX/Hn4FRhTntFzGkv1yhMCeXW8CeyDMVMCgQ/J2fIQDJeJjy8aS8AYFI9JfQRheXNm5eia8iHaVwzPILIJer8tLkTa9D5gRd7uobTfk2Pn6UdREWQVuPHlvqytF+fyD+Wc4buvfdeTJ06FW63GwsXLsTGjRsTbv/KK69g4cKFcLvdOOqoo3D//ffnaU8Tw8/JGfLmVgy17e8HAHxqemNOfw9BJKOp2o3ZE2oKvRtECeG0K12b4yfVAYA0LiMdRmIzySpcUT+hukz2FY4bX41VZ07HubPHpf36RP6xlBhat24dVq1ahVtuuQXbtm3DKaecguXLl6O9vV1z+08++QTnnnsuTjnlFGzbtg0333wzvvnNb+Ivf/lLnvc8HoUzlIFdawQmtvQalBEEQRQrfH6aTZBHdHRkkKs57NN3hs6dMw6rzpwBGzVbtBSWEkN33303rrnmGlx77bWYNWsW1q5di5aWFtx3332a299///2YNGkS1q5di1mzZuHaa6/FV77yFfz0pz/N857Hw4uh4Rw7Q+z1K10UFSUIorRw2mVRUuYQUemOngeN9ArSY8SvPKdWu+Vza0MFzSKzIpYRQ36/H1u3bsWyZcsU9y9btgybNm3SfM7rr78et/3ZZ5+NLVu2IBDQdmN8Ph8GBwcV/3LBQJ7CZL5gCP5YJl+lm8QQQRClBe8MuR2iJGAyuQj1xKrJWKuS6jLZGZrUQLmZVsQyYqi7uxuhUAjNzc2K+5ubm9HZ2an5nM7OTs3tg8Eguru7NZ+zZs0a1NTUSP9aWlqy8weoqOBcmlyGyfgvfCX1GCIIosTgq8l4MeTJxBli1WSuaJiMd92pUMWaWEYMMQRBGYeNRCJx9yXbXut+xk033YSBgQHp3/79+zPcY22uOGkyrvv0UQDknhW5YIgLkVEMmyCIUkMxINgpSheiHn8I4XDyGWNasHM2a1VSx/UZGldDVWRWxDJWQWNjI0RRjHOBurq64twfxtixYzW3t9vtaGho0HyOy+WCy5WfzqFsaOpoBs2/ksHi4pQvRBBEKcLPwSvjnCEAGAmE0jo3stQDV0xojaly4TdXLUKV205T6i2KZZwhp9OJhQsXYv369Yr7169fjyVLlmg+p7W1NW77559/HosWLYLDUfjuoGWxq4pczieTnCHKFyIIogRROEMOEW6HDUyvpBsqC8TEEP/aZx7bjMVHaV9kE+bHMmIIAFavXo3f/OY3+N3vfocdO3bgxhtvRHt7O1asWAEgGuK66qqrpO1XrFiBffv2YfXq1dixYwd+97vf4be//S2+853vFOpPUMDKMnMZJmPOUBWJIYIgShAXn0DtFCEIghQqS7eijM2TpLEyxYOlVsiLL74YPT09uP3229HR0YHZs2fj2WefxeTJkwEAHR0dip5DU6dOxbPPPosbb7wRv/rVrzB+/Hj8/Oc/x+c///lC/QkKymIJzbkMk7HkbAqTEQRRiijDZNHblS47hrzBtJ0hf2zoq4PEUNFguRVy5cqVWLlypeZjDz/8cNx9p556Kt5+++0c71V65CNMRs4QQRCljDpMBsjVvOmW1/tj8yQdOkNgCetBn2QBKc9DAvUQNVwkCKKE4cUQE0GVGYbJAjFniMJkxQN9kgWkTMoZyl3TRdkZKnzCOEEQRL7hxVBjZbRSWOo1lOa5V8oZslPlWLFAYqiA5KeajHKGCIIoXVzcoNbGymg/INYscdiX3rlXqiYTxSRbElaBVsgCko8wGYuJU84QQRClCN8RmjlDFWl2oR4YCaDb45OcIYdIzlCxQCtkAWFfyCFvEN5ASOpmmk0ogZogiFJmRnOldJudY9MdyXHK/76IQS7p2kkJ1EUDfZIFZFyNGxNqyxAMR/Dqrm54AyEMZmFO2a7DQ3jxw8OIRCKSGCqnuWQEQZQgteVOKdF51rhqAEi7z9CgqvqMSuuLB1ohC4ggCFgwuQ4H+0ext8eD//fsDnzS7cE7P1qGmrL0E56v+8NWfNztwffPOQajgaidW5YD14kgCMIKvPaDMzAwGsDYGjcA2Rn62zuH8MpHR/CzL87HvJbahK/B5lrykDNUPNAnWWAqXXIX6k+6PQCAt/f1pf16kUgEH8deZ/0HnfDF8pFY5RpBEESpMabKhWlNcriMiaHuYT8+PuLBlx9+K+lraLlIVFpfPNAnWWBY+Kp72Cfdp5VQvbNzCEeGfHH3q+G/sC67KL2W20EfNUEQBAC01Csny/d6/EmfMzAan8JATReLB/okCwyrKOsalIWOuivq7q5hnL12Az77y1eTvl6fR/7ChiMReGNiiC8vJQiCKGWmN1Wl/Jz+kXgxRM5Q8UCfZIFhzlDXkFe678iw0gF6dnsHAKBjwKsZt+bpHZGvcEb8IamHEYXJCIIgokyoLYMrRVdnUMMZIjFUPNAnWWCYM3SYc4a6VWLoUP+odDvZhPtej/zcEX8Q3lg/jFyU7RMEQVgRm01Iud2IupIMoATqYoI+yQLDxNBBTvB4VTlDQ1weUM9w4th2LxcmG/YFpeZgbvrSEgRBSKTabkR9Xgao6WIxQStkgdH6QvpiAoYxwoshT+Ik6j4uEZDPHyJniCAIQqY8xdQBtRhyiALsFCYrGuiTLDDlrvgvpFoMebjQWLKqBz5nyB+SX4fEEEEQhEyFal5jsnxMtRhyU1FKUUFiqMCUa4gUv0oM8YNc+zQqGnj6NMSSU7RBtJGdSxAEwVA7Q4FQYjGkvkh10QVmUUFiqMDUljvj7lOLIY9fDpMNJRnXoeUcuajHEEEQhIJ4MRTW2TKKN6B8vMxJ59Vigj7NAjO+1i3dnlgXbQSmFkMjPtkZGtKoaODpG4kXQxQiIwiCUKLO10wqhoIUJitmSAwVmCq3PIOMiSC/6ks5wjlDWr0ueLTEUgX1GCIIglBgE5SpA+qLUDU+lTNEF5nFBYkhE9FQ6QIA+FRXIHxvoWTOkNYXmibWEwRBKNnb41H8rL4IVRPnDFH6QVFBn6YJuOeS+Tht5hh844xpAJSCxh8MIxiWE/uGfImdIXWSHwBUaFSsEQRBlDJXtU5W/Kx1IXnTk+/igl++Cl8wpFFaT8tnMUGWgQm4YP4EXDB/At7a2wtA+aXkQ2RAcmeIiSGnaJOudMrIGSIIglDwmbnjsWBSHS781Wvo8fg1q8kee3M/AOClD7viwmRaF56EdSFpayLYnBteDHlU4zeS5Qz5Y1Zubbmci0Q5QwRBEEpsNgEt9eXSSA11AjXfd6hvJBCXvtAznLgBLmEtSAyZCFYCz8euR9N0huq4kn3KGSIIgtCGhbvUTg//8+BoIK60vr13JPc7R+QNEkMmgjlDvB3r8amcoQRiKBKJSEJK4QxRzhBBEIQmbL6Y2hnic4QGRgNxOUMT68pzv3NE3iAxZCKYXTvkC2LT7m6EwxGpkow1CBtM0HQxGI6AObu8M1RGYTKCIAhNmDOkFkOjAWXnf1ZN9p/nzcLZxzXj3ssX5G8niZxDYshEOLnJ8pf9ZjNe2tklJVA3V0ebM/qD4bjYNYO3desq+JwhCpMRBEFo4bLH52oCyjFIHl9QcuyPHV+NB65chNkTavK3k0TOITFkIlyqjqafdHukBOoxVS7p/u//+V3N5/Nf5nE1ZdJt9UBCgiAIIoqeM8T3dxsNhCRniJotFickhkxElUq0DPuCUgI1/9jTbYdweNAb93zmGDlEAZMb5Hj2pHqKbRMEQWjBxJBfVVrP5wiN+kNSArXLTstmMUKfqomwqSbLD3mDUgK1Ou+ncyBeDPm5HkNszhkATG+qzPauEgRBFAVOvTAZJ4ZG/EFJHJEzVJyQGDIxQ96A9IWscNoVokYrkVpquGi3YUpDhXR/CzlDBEEQmlTGXPch1TmVzxka8Yek8yuJoeKExJCJiTpD0TBZmVPEE9e1oiz2RRwcjS+xZ1c2LruIhkoXnlq5BP9a9WmIKseJIAiCiNJQGa287VY1UVQ6QyHu/ErLZjFCn6rJOH/uOOn2oFdu9FXuFFFX4cTJ0xqkx9TwzhAAHD+pDjPHVuV6lwmCICxLY2xAdveQX3E/3+2/b0R+jJyh4oTEkMn4yRfm4SsnTwUQdYZYBQOrNKt2R0vmtcZy+KRt6WMlCIIwAhNDPR6lM8R3mOY7/7vp/FqU0KdqMsqcIs6ZPRZA9AvIeluwUR3VZTExpOEM+VXOEEEQBJGYxliY7Miw0hna1xM/bsNuE2CnafVFCX2qJqTKLSf0qd2e6thjWjlD6jAZQRAEkZixNdGGtu/s78eeI8PS/VpiiFz34oU+WRMiuT+jQUngSGEyA84QfWEJgiCM0cLNGPv8fZsAAOFwBPt6PXHbUr5Q8UKrpglhzpA/FJZyg2RnSD9nSA6T0ReWIAjCCPxQ6/6R6Hm1a8gHbyAM0SbgtJljpMdJDBUvJIZMSKXTDiFWDc/KPeNzhhKEySimTRAEYQhBiG898o/tHQCAiXVlOHWGLIZo6HXxQqumCbHZBFTGhqt2x5L63FKYjOUMaTlDsfwiB32sBEEQRrn7i/MAAONq3Ogc8OL3m/YCAE6bMQaLJtdL21XSnMeihT5Zk1LltmPIF8QAC5M5VGGyBH2GXOQMEQRBGGbuxFoAQM+wH2fc9bI0pPU/FrVgQq082shNF5pFC32yJoWFwxgsgbqGS65WQ6X1BEEQqcMcH38orJhW31JXrjgXh8NxTyWKBFo1TQqf1AfEJ1CPBuT28KFwBB0Do/jVy7sV2xIEQRDJKXfF5wI5RRtqyh2KcUZBUkNFC4XJTEpduVPxMwuTVbrlj2zYF8S+Qx5c9uvNijk65AwRBEEYp8IZvxTefO4xcfeFIvnYG6IQ0KppUtQFDixMJtoEyfkZ8Qfxnf97RyGE+G0JgiCI5Ig2QZEPNK+lFlfHxiLxhMOkhooVEkMmJai6BOFDX+Wx8s4RfwgDGrlD5AwRBEGkBl8pVu3WDpoESQwVLbRqmpRwRP7SNVe7pGGCAFAes3Q9viBG/SSGCIIgMoXvIaQuoZ/aWAEAOOe4sXndJyJ/UM6QSfnW0hn4944unDilHr+9epFC4FTEkv1G/SGMqEJk7H6CIAjCOG4uvaBK5Qytu+4kvL6nB8tnj8v3bhF5gsSQSZkzsQbv/HAZqsvscR1Sy5gz5A8houHaftIdP1OHIAiC0EfpDCmreZuq3Lhg/oR87xKRRyieYmJqyh2areIrpJyhoGYTsHNmk5VLEASRCvzcseZqV4ItiWLEMmKor68PV155JWpqalBTU4Mrr7wS/f39CZ/z5JNP4uyzz0ZjYyMEQUBbW1te9jXXsJyhEX8obg7Zf543C8tJDBEEQaREGSeGJnKT7InSwDJi6LLLLkNbWxuee+45PPfcc2hra8OVV16Z8Dkejwcnn3wyfvzjH+dpL/MDqybz+IJx1Q0nT2vUdJMIgiAIfZRiqCzBlkQxYomcoR07duC5557DG2+8gcWLFwMAfv3rX6O1tRU7d+7EzJkzNZ/HxNLevXvztat5gU+gDoSUHVH5LzRBEARhDAdXpDK+lsRQqWEJZ+j1119HTU2NJIQA4KSTTkJNTQ02bdqU1d/l8/kwODio+Gc2WJhs2BdEQNWPyE1iiCAIImX4NiX1Fc4EWxLFiCXEUGdnJ5qamuLub2pqQmdnZ1Z/15o1a6S8pJqaGrS0tGT19bMBC5OxifY85AwRBEGkzrBPFkP8PDKiNCioGLr11lshCELCf1u2bAEAzTyYSCSS9fyYm266CQMDA9K//fv3Z/X1swFzhrTEkEujuowgCIJIzAj1ZytpCpozdMMNN+CSSy5JuM2UKVPw7rvv4vDhw3GPHTlyBM3NzVndJ5fLBZfL3GWVLGeob8Qf9xiFyQiCIFKHd4aI0qOgYqixsRGNjY1Jt2ttbcXAwADefPNNnHjiiQCAzZs3Y2BgAEuWLMn1bpoOFgrrH4l3hgiCIIjUWTy1AR8f8aC23JF8Y6LosEQ12axZs3DOOefgq1/9Kh544AEAwNe+9jWcf/75ikqyY445BmvWrMHnPvc5AEBvby/a29tx6NAhAMDOnTsBAGPHjsXYsdbtxVPh0g+TEQRBEKlz07nHYGJdGT4zd3yhd4UoAJZJMHn00UcxZ84cLFu2DMuWLcPcuXPxhz/8QbHNzp07MTAwIP3817/+FccffzzOO+88AMAll1yC448/Hvfff39e9z3bsARqcoYIgiCyQ7XbgetPn4ZJDdRwsRQRIhGt6VYEY3BwEDU1NRgYGEB1dXWhdwcA8OYnvfjiA69rPrb3x+fleW8IgiAIwnyksn5bxhkiZMqdlCRNEARBENmCxJAFYTlDPFMbK/Dwl08owN4QBEEQhLWxRAI1oaS2TFntML2pEutXn1qgvSEIgiAIa0POkAWpLXcoptU7RPoYCYIgCCJdaBW1IIIgYEyV3BiSHzBIEARBEERq0CpqUXgx5CJniCAIgiDShlZRi9LEiyGaR0YQBEEQaUOrqEWp5pKoXXYqtScIgiCIdCExZFGq3HIhoJucIYIgCIJIG1pFLUqVm5whgiAIgsgGJIYsShXXeJFyhgiCIAgifWgVtSiKMBk5QwRBEASRNiSGLIoiTEbOEEEQBEGkDa2iFqWSc4Zc1HSRIAiCINKGVlGLUsOV1rsdFCYjCIIgiHQhMWRR+A7UTupATRAEQRBpQ6uoRRlTKYshbzBUwD0hCIIgCGtDYsiiOLk8oYGRQAH3hCAIgiCsDYmhIiAUjhR6FwiCIAjCspAYsjA3n3sMjhpTgWtPOarQu0IQBEEQlkWIRCJkKyRgcHAQNTU1GBgYQHV1daF3hyAIgiAIA6SyfpMzRBAEQRBESUNiiCAIgiCIkobEEEEQBEEQJQ2JIYIgCIIgShoSQwRBEARBlDQkhgiCIAiCKGlIDBEEQRAEUdKQGCIIgiAIoqQhMUQQBEEQRElDYoggCIIgiJKGxBBBEARBECUNiSGCIAiCIEoaEkMEQRAEQZQ0JIYIgiAIgihp7IXeAbMTiUQAAIODgwXeE4IgCIIgjMLWbbaOJ4LEUBKGhoYAAC0tLQXeE4IgCIIgUmVoaAg1NTUJtxEiRiRTCRMOh3Ho0CFUVVVBEISsvvbg4CBaWlqwf/9+VFdXZ/W1CRl6n/MDvc/5gd7n/EDvc37I5fsciUQwNDSE8ePHw2ZLnBVEzlASbDYbJk6cmNPfUV1dTV+2PEDvc36g9zk/0PucH+h9zg+5ep+TOUIMSqAmCIIgCKKkITFEEARBEERJQ2KogLhcLvzoRz+Cy+Uq9K4UNfQ+5wd6n/MDvc/5gd7n/GCW95kSqAmCIAiCKGnIGSIIgiAIoqQhMUQQBEEQRElDYoggCIIgiJKGxBBBEARBECUNiaECce+992Lq1Klwu91YuHAhNm7cWOhdKjo2bNiAz3zmMxg/fjwEQcDTTz9d6F0qOtasWYMTTjgBVVVVaGpqwoUXXoidO3cWereKkvvuuw9z586VmtO1trbin//8Z6F3q+hZs2YNBEHAqlWrCr0rRcWtt94KQRAU/8aOHVuw/SExVADWrVuHVatW4ZZbbsG2bdtwyimnYPny5Whvby/0rhUVHo8H8+bNwy9/+ctC70rR8sorr+D666/HG2+8gfXr1yMYDGLZsmXweDyF3rWiY+LEifjxj3+MLVu2YMuWLTjjjDNwwQUX4P333y/0rhUtb731Fh588EHMnTu30LtSlBx33HHo6OiQ/m3fvr1g+0Kl9QVg8eLFWLBgAe677z7pvlmzZuHCCy/EmjVrCrhnxYsgCHjqqadw4YUXFnpXipojR46gqakJr7zyCj796U8XeneKnvr6evzkJz/BNddcU+hdKTqGh4exYMEC3Hvvvbjjjjswf/58rF27ttC7VTTceuutePrpp9HW1lboXQFAzlDe8fv92Lp1K5YtW6a4f9myZdi0aVOB9oogssPAwACA6CJN5I5QKITHH38cHo8Hra2thd6douT666/HeeedhzPPPLPQu1K07Nq1C+PHj8fUqVNxySWX4OOPPy7YvtCg1jzT3d2NUCiE5uZmxf3Nzc3o7Ows0F4RROZEIhGsXr0an/rUpzB79uxC705Rsn37drS2tsLr9aKyshJPPfUUjj322ELvVtHx+OOP4+2338Zbb71V6F0pWhYvXoxHHnkEM2bMwOHDh3HHHXdgyZIleP/999HQ0JD3/SExVCAEQVD8HIlE4u4jCCtxww034N1338Wrr75a6F0pWmbOnIm2tjb09/fjL3/5C770pS/hlVdeIUGURfbv349vfetbeP755+F2uwu9O0XL8uXLpdtz5sxBa2srjj76aPz+97/H6tWr874/JIbyTGNjI0RRjHOBurq64twigrAK3/jGN/DXv/4VGzZswMSJEwu9O0WL0+nEtGnTAACLFi3CW2+9hXvuuQcPPPBAgfeseNi6dSu6urqwcOFC6b5QKIQNGzbgl7/8JXw+H0RRLOAeFicVFRWYM2cOdu3aVZDfTzlDecbpdGLhwoVYv3694v7169djyZIlBdor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", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -161,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 5, "id": "d31ce324", "metadata": {}, "outputs": [ @@ -169,8 +165,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "* mean(Y) [0] = 0.165\n", - "* cov(Y) [0.05] = 0.0151\n" + "* mean(Y) [0] = 0.161\n", + "* cov(Y) [0.05] = 0.013\n" ] } ], @@ -180,7 +176,7 @@ " return c**2 * Q / (2 * a) * exp(-a * abs(tau))\n", " \n", "print(\"* mean(Y) [%0.3g] = %0.3g\" % (0, np.mean(Y)))\n", - "print(\"* cov(Y) [%0.3g] = %0.3g\" % (r(0), np.cov(Y)))" + "print(\"* cov(Y) [%0.3g] = %0.3g\" % (r(0).item(), np.cov(Y)))" ] }, { @@ -193,20 +189,18 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 6, "id": "1cf5a4b1", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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1vi5AQBA9ksp2GzcuFF///vf9fzzz+ujjz5SWVlZ1bRdd91VAwcO1CWXXKIDDjigWoWZM2eOBgwYUPV4xIgRkqRzzz1XTz31lK6//npt3rxZl19+udavX68+ffpo6tSpys/Pr9bzAQAAt+QYY0wyM95333267bbb1LFjRx1//PE68MADtcsuuygvL08///yzvvjiC82aNUuvvPKKDjroID344IPq3LlzXZc/JaWlpSooKFBJSYlatGiR7uIAqE3eRQQ77iitX5/WogCoXakcv5MONqeddppuueUW7b333nHnKysr09/+9jc1adJEF110UfKlrgcEG8BhBBvAWXUSbPzeeOMNHXPMMWrUKGvaHksi2ABOI9gAzkrl+F2tZHLCCSdo3bp11SocAABAXalWsKlGJQ8AAECdq/a5pHnz5mnTpk1h41auXMkpHgAAkDbVvo9NcXGxcnJy1LFjR+2zzz7q2rWrvvvuO+244461WDwAAIDkVTvYLFq0SGvWrNHnn3+u+fPn6/PPP1dlZaUeffTR2iwfAABA0qodbPLz87X77rvr4IMPrs3yAAAAVFu12tgMHjxY22+/fW2XBQAAoEaqVWPz6quv1nY5AAAAaiy77rAHAAAQR9LBZvny5SmteOXKlSkXBgAAoCaSDjYHHHCALr74Yn300Ucx5ykpKdFjjz2mHj166OWXX66VAgIAACQr6TY2Cxcu1Lhx43T00Udr++23V+/evVVUVKSmTZtq/fr1+vLLL7VgwQL17t1bd911l4qLi+uy3AAAAFFS7gRzy5YtmjJlimbNmqVly5Zp8+bNatWqlfbff38NGjRIPXr0qKuy1hidYAIOoxNMwFl13ru3Z/PmzcrLy6vu4vWOYAM4jGADOKvOe/f2dOvWTQ888IDKyspqshoAAIBaUaNgM3v2bC1evFh77bWXJkyYoK1bt9ZWuQAAAFJWo2Czyy676KGHHtLMmTP15ZdfqkePHnr88cdVUVFRW+UDAABIWq3coM8LONOmTdO8efO0zz776JlnnqmNVQMAACStVoLN+vXr9cEHH+jdd99VQUGBCgoKdP7559fGqgEAAJKWcl9RFRUVmjx5so466igVFxfr22+/VUFBgbp27aquXbuqW7duOuaYY9StW7e6KC8AAEBM1brcOy8vTwsWLFB5ebn22GMPbbddtfrSrHdc7g04jMu9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AAAAZxBsAACAMwg2AADAGQQbAADgDIINAABwBsEGAAA4g2ADAACcQbABAADOINgAAABnEGwAAIAztkt3AeqTMUaS7f4cAABkB++47R3H42lQwWbDhg2SpHbt2qW5JAAAIFUbNmxQQUFB3HlyTDLxxxGVlZVatWqV8vPzlZOTk+7ipF1paanatWunFStWqEWLFukujrPYzvWD7Vx/2Nb1g+0cYozRhg0bVFRUpEaN4reiaVA1No0aNdKuu+6a7mJknBYtWjT4L019YDvXD7Zz/WFb1w+2s5WopsZD42EAAOAMgg0AAHAGwaYBy83N1a233qrc3Nx0F8VpbOf6wXauP2zr+sF2rp4G1XgYAAC4jRobAADgDIINAABwBsEGAAA4g2ADAACcQbBBmLKyMu23337KycnRvHnz0l0cpyxbtkwXXnihOnXqpLy8PO2+++669dZbVV5enu6iOWHChAnq1KmTmjZtql69emnWrFnpLpJTxo8frwMOOED5+flq3bq1TjzxRH399dfpLpbzxo8fr5ycHF1zzTXpLkrWINggzPXXX6+ioqJ0F8NJX331lSorK/XII49owYIFuu+++/TXv/5VN910U7qLlvVefPFFXXPNNRo1apTmzp2rfv36qbi4WMuXL0930ZwxY8YMDR8+XB988IHeeecdbdu2TQMHDtSmTZvSXTRnffzxx3r00Ue1zz77pLsoWYXLvVHlrbfe0ogRI/Svf/1L3bt319y5c7Xffvulu1hOu+uuuzRx4kR9++236S5KVuvTp4969uypiRMnVo3bc889deKJJ2r8+PFpLJm71q5dq9atW2vGjBk67LDD0l0c52zcuFE9e/bUhAkTNHbsWO233366//77012srECNDSRJP/74oy6++GI9++yzatasWbqL02CUlJSoZcuW6S5GVisvL9cnn3yigQMHho0fOHCgZs+enaZSua+kpESS+PzWkeHDh+vYY4/VkUceme6iZJ0G1QkmghljdN5552nYsGHq3bu3li1blu4iNQjffPONHnzwQd1zzz3pLkpWW7dunSoqKlRYWBg2vrCwUKtXr05TqdxmjNGIESN06KGHqkePHukujnNeeOEFffrpp/r444/TXZSsRI2Nw0aPHq2cnJy4w5w5c/Tggw+qtLRUI0eOTHeRs1Ky29lv1apVOvroo3XaaafpoosuSlPJ3ZKTkxP22BgTNQ6144orrtD8+fP1/PPPp7sozlmxYoWuvvpqTZo0SU2bNk13cbISbWwctm7dOq1bty7uPB07dtTQoUP1+uuvhx0EKioq1LhxY5111ll6+umn67qoWS3Z7eztpFatWqUBAwaoT58+euqpp9SoEb8vaqK8vFzNmjXTP/7xD5100klV46+++mrNmzdPM2bMSGPp3HPllVdq8uTJmjlzpjp16pTu4jhn8uTJOumkk9S4ceOqcRUVFcrJyVGjRo1UVlYWNg3RCDbQ8uXLVVpaWvV41apVGjRokP75z3+qT58+2nXXXdNYOresXLlSAwYMUK9evTRp0iR2ULWkT58+6tWrlyZMmFA1bq+99tIJJ5xA4+FaYozRlVdeqVdeeUXTp09X586d010kJ23YsEHfffdd2Ljzzz9f3bp10w033MCpvyTQxgZq37592OMddthBkrT77rsTamrRqlWr1L9/f7Vv315333231q5dWzWtTZs2aSxZ9hsxYoTOPvts9e7dWwcffLAeffRRLV++XMOGDUt30ZwxfPhwPffcc3r11VeVn59f1X6poKBAeXl5aS6dO/Lz86PCS/PmzbXTTjsRapJEsAHqydSpU7VkyRItWbIkKjBScVozQ4YM0U8//aQ//elP+uGHH9SjRw9NmTJFHTp0SHfRnOFdSt+/f/+w8U8++aTOO++8+i8QEAOnogAAgDNotQgAAJxBsAEAAM4g2AAAAGcQbAAAgDMINgAAwBkEGwAA4AyCDQAAcAbBBgAAOINgAwAAnEGwAQAAziDYAMhqxx9/vHJycgKH1157Ld3FA1DP6CsKQFb76aeftHXrVm3cuFGdO3fWlClTtP/++0uSWrVqpe22o69foCEh2ABwwvvvv69DDjlEJSUlys/PT3dxAKQJp6IAOGH+/Pnq2LEjoQZo4Ag2AJwwf/587bPPPukuBoA0I9gAcMKyZcvUtWvXdBcDQJoRbAA4obKyUt99952+//570XQQaLhoPAzACW+99ZYuueQSrV+/XqWlpWrUiN9tQENEsAEAAM7gJw0AAHAGwQYAADiDYAMAAJxBsAEAAM4g2AAAAGcQbAAAgDMINgAAwBkEGwAA4AyCDQAAcAbBBgAAOINgAwAAnEGwAQAAzvg/F0muUgjYcGEAAAAASUVORK5CYII=", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -221,20 +215,18 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": 7, "id": "62af90a4", "metadata": {}, "outputs": [ { "data": { - "image/png": "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zry5Npzqeov/EgkMGxV9JosVB5N4sJc3zVxrWrucCsvKyuLChQsAvPLKK2zZsoW33nrL0DmGRNWTkZFBSEgId999NxMmTOD06dPWLqnCajs78M8RbTicmMG60KpXf0VIYNigS1N81HZy4IlBraxdToVorfn1119p3749L7zwAgDdunVj4MCB1i1M2CQ3Nzf27NnDW2+9xerVq2nbti0LFiwwfEEmc7u7iydtG7vw3sZI8gurVu0VIYFhgzYePc3WqFRmDvXDrXbVGW4aHh7OyJEjGTNmDLVr12bixInWLklUAU5OTrz66quEhITQpUsX3njjjbKr06rC3k7x4si2nEjL5ps9x61djmEkMGxMdn4hb64Np21jFybf5mPtcky2dOlSOnbsSFBQEB999BFHjhzhtttus3ZZogpp3bo1f/zxB3v27KFu3brk5+czc+ZM4uLirF2aSQa28WBQGw8++P0YieeyrV2OISQwbMziHfGcOp/LW3e2t/mHgc6dO0dCQgIA/fv3Z8aMGURHR/PMM8/IMxaiUpRSeHt7A3D48GEWL16Mv78/jz/+eNm/NVullOLfd3VAo/lo8zFrl2MI2/6NdIvJzi/kq13xDPVvaOg04jcrNTWV119/HR8fn7IlOX19ffn0009l/QphNj169CA6OpopU6awePFiWrVqxfTp07l48aK1S7umZq41ebC3D78cOklMStW6rWYKCQwb8tP+RNKzC3h0QEtrl1KuyMhIpk+fjpeXF2+99RbDhw/n7bfftnZZohpr2rQpX3zxBTExMTzyyCMcOXKEWrVKlgZOTEy0cnXlm9HfFycHOxZuj73xwVWMBIaNyC8sZuH2OLp5u9HNhq4uCgsLKSwsBOCHH35g2bJlPPTQQ4SHh/Pf//6X9u3bW7lCcSto3rw5n376Kbt370YpRWZmJu3ataNnz55899135OXlWbvEMvXrOPO3bl6sOnSS0+dvvBpgVSKBYSNWHkzi1PlcnhxsG8NoY2JiePnll2nevDlr1qwB4JlnniEhIYEFCxbg7+9v5QrFrcjevmTqfkdHR959910yMjKYOHEiTZo04cknnyQ+Pt7KFZZ4pJ8vxRqW7KwaHfamksCwARnZ+bz/exSBXq4MaG29PoDCwkLmzZtH37598fPzY/bs2XTt2rVsjQo3NzfpoxA2oWbNmjzxxBNERETw+++/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\n", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -284,7 +276,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.1" + "version": "3.13.9" } }, "nbformat": 4, diff --git a/examples/type2_type3.py b/examples/type2_type3.py index 52e0645e2..f0d79dc51 100644 --- a/examples/type2_type3.py +++ b/examples/type2_type3.py @@ -4,9 +4,10 @@ import os import matplotlib.pyplot as plt # Grab MATLAB plotting functions -from control.matlab import * # MATLAB-like functions -from numpy import pi -integrator = tf([0, 1], [1, 0]) # 1/s +import control as ct +import numpy as np + +integrator = ct.tf([0, 1], [1, 0]) # 1/s # Parameters defining the system J = 1.0 @@ -29,20 +30,20 @@ # System Transfer Functions # tricky because the disturbance (base motion) is coupled in by friction -closed_loop_type2 = feedback(C_type2*feedback(P, friction), gyro) +closed_loop_type2 = ct.feedback(C_type2*ct.feedback(P, friction), gyro) disturbance_rejection_type2 = P*friction/(1. + P*friction+P*C_type2) -closed_loop_type3 = feedback(C_type3*feedback(P, friction), gyro) +closed_loop_type3 = ct.feedback(C_type3*ct.feedback(P, friction), gyro) disturbance_rejection_type3 = P*friction/(1. + P*friction + P*C_type3) # Bode plot for the system plt.figure(1) -bode(closed_loop_type2, logspace(0, 2)*2*pi, dB=True, Hz=True) # blue -bode(closed_loop_type3, logspace(0, 2)*2*pi, dB=True, Hz=True) # green +ct.bode(closed_loop_type2, np.logspace(0, 2)*2*np.pi, dB=True, Hz=True) # blue +ct.bode(closed_loop_type3, np.logspace(0, 2)*2*np.pi, dB=True, Hz=True) # green plt.show(block=False) plt.figure(2) -bode(disturbance_rejection_type2, logspace(0, 2)*2*pi, Hz=True) # blue -bode(disturbance_rejection_type3, logspace(0, 2)*2*pi, Hz=True) # green +ct.bode(disturbance_rejection_type2, np.logspace(0, 2)*2*np.pi, Hz=True) # blue +ct.bode(disturbance_rejection_type3, np.logspace(0, 2)*2*np.pi, Hz=True) # green if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: plt.show() diff --git a/examples/vehicle.py b/examples/vehicle.py index 07af35c9f..f89702d4e 100644 --- a/examples/vehicle.py +++ b/examples/vehicle.py @@ -3,7 +3,6 @@ import numpy as np import matplotlib.pyplot as plt -import control as ct import control.flatsys as fs # diff --git a/pyproject.toml b/pyproject.toml index a81fc117c..b76a3731f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,13 +10,12 @@ build-backend = "setuptools.build_meta" name = "control" description = "Python Control Systems Library" authors = [{name = "Python Control Developers", email = "python-control-developers@lists.sourceforge.net"}] -license = {text = "BSD-3-Clause"} +license = "BSD-3-Clause" readme = "README.rst" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -52,16 +51,22 @@ source = "https://github.com/python-control/python-control" write_to = "control/_version.py" [tool.pytest.ini_options] -addopts = "-ra" +addopts = "-ra --strict-markers" filterwarnings = [ "error:.*matrix subclass:PendingDeprecationWarning", ] +markers = [ + "slycot: tests needing slycot", + "noslycot: test needing slycot absent", + "cvxopt: tests needing cvxopt", + "pandas: tests needing pandas", +] + [tool.ruff] # TODO: expand to cover all code -include = ['control/**.py'] -exclude = ['control/tests/*.py'] +include = ['control/**.py', 'benchmarks/*.py', 'examples/*.py'] [tool.ruff.lint] select = [