From dd3d4d0e4f23557f34b88ea9338969ba738aeb38 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 4 Jul 2025 19:12:53 -0400 Subject: [PATCH 1/6] documentation updates for 0.10.2 release --- control/margins.py | 12 +++++++----- control/pzmap.py | 2 +- control/tests/margin_test.py | 12 ++++++------ doc/examples.rst | 1 + 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/control/margins.py b/control/margins.py index e51aa40af..57e825c65 100644 --- a/control/margins.py +++ b/control/margins.py @@ -536,10 +536,12 @@ def disk_margins(L, omega, skew=0.0, returnall=False): 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. + 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. @@ -553,7 +555,7 @@ def disk_margins(L, omega, skew=0.0, returnall=False): DPM : float or array_like Disk-based phase margin. - Example + Examples -------- >> omega = np.logspace(-1, 3, 1001) >> P = control.ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], diff --git a/control/pzmap.py b/control/pzmap.py index f1e3c6545..6bf928f56 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -129,7 +129,7 @@ def replot(self, cplt: ControlPlot): Parameters ---------- - cplt: ControlPlot + cplt : `ControlPlot` Graphics handles of the existing plot. """ pole_zero_replot(self, cplt) diff --git a/control/tests/margin_test.py b/control/tests/margin_test.py index 23ef00aac..679c1c685 100644 --- a/control/tests/margin_test.py +++ b/control/tests/margin_test.py @@ -384,7 +384,7 @@ def test_siso_disk_margin(): # 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([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 @@ -408,13 +408,13 @@ def test_mimo_disk_margin(): # 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([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([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 else: # Slycot not installed. Should throw exception. @@ -435,7 +435,7 @@ def test_siso_disk_margin_return_all(): 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 + 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 @@ -457,7 +457,7 @@ def test_mimo_disk_margin_return_all(): 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 + 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 @@ -468,7 +468,7 @@ def test_mimo_disk_margin_return_all(): 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 + 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 else: 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 ================= From 939a640a2a3abdf96a9abb6d41dcab2b2a67af45 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 4 Jul 2025 22:05:51 -0400 Subject: [PATCH 2/6] update licensing info to conform to new standard --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index db70b8f48..b47f7462c 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", From 72653b743965f0fdf85170dd1b8101bff2ce61f2 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 4 Jul 2025 22:06:00 -0400 Subject: [PATCH 3/6] update os-blast-test-matrix.yml to load slycot from conda-forge for windows --- .github/scripts/set-conda-test-matrix.py | 14 ++++++++++++++ .github/workflows/os-blas-test-matrix.yml | 9 +++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/scripts/set-conda-test-matrix.py b/.github/scripts/set-conda-test-matrix.py index 6bcd0fa6f..49714e7eb 100644 --- a/.github/scripts/set-conda-test-matrix.py +++ b/.github/scripts/set-conda-test-matrix.py @@ -27,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/os-blas-test-matrix.yml b/.github/workflows/os-blas-test-matrix.yml index 263afb7a4..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' @@ -332,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 From fa15e6f080a7f46d1e18973b3971beec9e7690f5 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 4 Jul 2025 23:07:14 -0400 Subject: [PATCH 4/6] update discrete time tests to be stable --- control/tests/discrete_test.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 9b87bd61b..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 From d1b005c1eba7d47d63f558bf0c02620f3ac2558f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 5 Jul 2025 09:05:34 -0400 Subject: [PATCH 5/6] additional documentation updates + legacy settings for 0.10.1 Nyquist plots --- control/config.py | 10 +++++++++- control/freqplot.py | 6 +++--- control/matlab/__init__.py | 2 +- doc/develop.rst | 4 ++-- doc/intro.rst | 6 +++--- doc/releases/0.10.2-notes.rst | 19 ++++++++++--------- examples/python-control_tutorial.ipynb | 6 +++--- 7 files changed, 31 insertions(+), 22 deletions(-) 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/freqplot.py b/control/freqplot.py index c97566d77..475467147 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -1659,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 @@ -1690,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. 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/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/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/releases/0.10.2-notes.rst b/doc/releases/0.10.2-notes.rst index 3e13239aa..175fdaff2 100644 --- a/doc/releases/0.10.2-notes.rst +++ b/doc/releases/0.10.2-notes.rst @@ -9,15 +9,16 @@ Version 0.10.2 Release Notes (current) * `GitHub release page `_ -This release contains numerous bug fixes and improvements, including -substantial updates to the documentation, including refactoring of the -online manual into a User Guide and a Reference Manual, as well as -more consistent and complete docstrings. In addition, signals and -systems can now be referenced using signal labels in addition to -offsets, and phase plane plots make use of the matplotlib -`~matplotlib.pyplot.streamplot` function. Numerous other changes have -been made to improve consistency of keyword arguments and function -names, with legacy aliases available. +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. 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" ] }, { From 3993c7952417f8e34ff2b58b643c50c8447f356d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 5 Jul 2025 10:35:17 -0400 Subject: [PATCH 6/6] add missing example: disk_margins.py --- doc/examples/disk_margins.py | 1 + 1 file changed, 1 insertion(+) create mode 120000 doc/examples/disk_margins.py 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