Skip to content

Commit adcb486

Browse files
authored
Merge branch 'python-control:main' into system-norms
2 parents 457c623 + a8a54d1 commit adcb486

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1399
-805
lines changed

.github/conda-env/build-env.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
name: build-env
22
dependencies:
33
- boa
4-
- numpy !=1.23.0
4+
- numpy

.github/conda-env/doctest-env.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
name: test-env
1+
name: doctest-env
22
dependencies:
3-
- conda-build # for conda index
43
- pip
5-
- coverage
6-
- coveralls
74
- pytest
8-
- pytest-cov
95
- pytest-timeout
106
- pytest-xvfb
117
- numpy

.github/conda-env/test-env.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
name: test-env
22
dependencies:
3-
- conda-build # for conda index
43
- pip
54
- coverage
6-
- coveralls
75
- pytest
86
- pytest-cov
97
- pytest-timeout

.github/workflows/control-slycot-src.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- name: Set up Python
1515
uses: actions/setup-python@v4
1616
with:
17-
python-version: '3.11'
17+
python-version: '3.12'
1818
- name: Install Python dependencies and test tools
1919
run: pip install -v './python-control[test]'
2020

.github/workflows/doctest.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ jobs:
1515
- name: Setup Conda
1616
uses: conda-incubator/setup-miniconda@v2
1717
with:
18-
python-version: 3.11
19-
activate-environment: test-env
18+
python-version: 3.12
19+
activate-environment: doctest-env
2020
environment-file: .github/conda-env/doctest-env.yml
2121
miniforge-version: latest
2222
miniforge-variant: Mambaforge
@@ -32,8 +32,6 @@ jobs:
3232
3333
- name: Run doctest
3434
shell: bash -l {0}
35-
env:
36-
MPLBACKEND: ${{ matrix.mplbackend }}
3735
working-directory: doc
3836
run: |
3937
make html

.github/workflows/install_examples.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
--channel conda-forge \
1919
--strict-channel-priority \
2020
--quiet --yes \
21-
python=3.11 pip \
21+
python=3.12 pip \
2222
numpy matplotlib scipy \
2323
slycot pmw jupyter
2424

.github/workflows/os-blas-test-matrix.yml

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,24 @@ jobs:
2121
- 'ubuntu'
2222
- 'macos'
2323
python:
24-
- '3.8'
25-
- '3.11'
24+
- '3.10'
25+
- '3.12'
2626
bla_vendor: [ 'unset' ]
2727
include:
2828
- os: 'ubuntu'
29-
python: '3.11'
29+
python: '3.12'
3030
bla_vendor: 'Generic'
3131
- os: 'ubuntu'
32-
python: '3.11'
32+
python: '3.12'
3333
bla_vendor: 'OpenBLAS'
3434
- os: 'macos'
35-
python: '3.11'
35+
python: '3.12'
3636
bla_vendor: 'Apple'
3737
- os: 'macos'
38-
python: '3.11'
38+
python: '3.12'
3939
bla_vendor: 'Generic'
4040
- os: 'macos'
41-
python: '3.11'
41+
python: '3.12'
4242
bla_vendor: 'OpenBLAS'
4343

4444
steps:
@@ -108,7 +108,7 @@ jobs:
108108
- 'macos'
109109
- 'windows'
110110
python:
111-
- '3.9'
111+
# build on one, expand matrix in conda-build from the Sylcot/conda-recipe/conda_build_config.yaml
112112
- '3.11'
113113

114114
steps:
@@ -133,14 +133,14 @@ jobs:
133133
shell: bash -l {0}
134134
run: |
135135
set -e
136-
numpyversion=$(python -c 'import numpy; print(numpy.version.version)')
137-
conda mambabuild --python "${{ matrix.python }}" --numpy $numpyversion conda-recipe
136+
conda mambabuild conda-recipe
138137
# preserve directory structure for custom conda channel
139138
find "${CONDA_PREFIX}/conda-bld" -maxdepth 2 -name 'slycot*.tar.bz2' | while read -r conda_pkg; do
140139
conda_platform=$(basename $(dirname "${conda_pkg}"))
141140
mkdir -p "slycot-conda-pkgs/${conda_platform}"
142141
cp "${conda_pkg}" "slycot-conda-pkgs/${conda_platform}/"
143142
done
143+
conda index --no-progress ./slycot-conda-pkgs
144144
- name: Save to local conda pkg channel
145145
uses: actions/upload-artifact@v3
146146
with:
@@ -247,7 +247,7 @@ jobs:
247247
- name: Install Wheel
248248
run: |
249249
python -m pip install --upgrade pip
250-
pip install matplotlib scipy pytest pytest-cov pytest-timeout coverage coveralls
250+
pip install matplotlib scipy pytest pytest-cov pytest-timeout coverage
251251
pip install slycot-wheels/${{ matrix.packagekey }}/slycot*.whl
252252
pip show slycot
253253
- name: Test with pytest
@@ -316,7 +316,6 @@ jobs:
316316
echo "libblas * *mkl" >> $CONDA_PREFIX/conda-meta/pinned
317317
;;
318318
esac
319-
conda index --no-progress ./slycot-conda-pkgs
320319
mamba install -c ./slycot-conda-pkgs slycot
321320
conda list
322321
- name: Test with pytest

.github/workflows/python-package-conda.yml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ jobs:
1616
max-parallel: 5
1717
fail-fast: false
1818
matrix:
19-
python-version: ['3.8', '3.11']
19+
python-version: ['3.10', '3.12']
2020
slycot: ["", "conda"]
2121
pandas: [""]
2222
cvxopt: ["", "conda"]
2323
mplbackend: [""]
2424
include:
25-
- python-version: '3.11'
25+
- python-version: '3.12'
2626
slycot: conda
2727
pandas: conda
2828
cvxopt: conda
@@ -61,20 +61,27 @@ jobs:
6161
shell: bash -l {0}
6262
env:
6363
MPLBACKEND: ${{ matrix.mplbackend }}
64-
run: pytest -v --cov=control --cov-config=.coveragerc control/tests
64+
run: |
65+
pytest -v --cov=control --cov-config=.coveragerc control/tests
66+
coverage xml
6567
66-
- name: Coveralls parallel
67-
# https://github.com/coverallsapp/github-action
68-
uses: AndreMiras/coveralls-python-action@develop
68+
- name: report coverage
69+
uses: coverallsapp/github-action@v2
6970
with:
71+
flag-name: conda-pytest_py${{ matrix.python-version }}_${{ matrix.slycot || 'no' }}-Slycot_${{ matrix.pandas || 'no' }}-Pandas_${{ matrix.cvxopt || 'no' }}_CVXOPT-${{ matrix.mplbackend && format('; {0}', matrix.mplbackend) }}
7072
parallel: true
73+
file: coverage.xml
7174

72-
coveralls:
73-
name: coveralls completion
74-
needs: test-linux-conda
75+
coveralls-final:
76+
name: Finalize parallel coveralls
77+
if: always()
78+
needs:
79+
- test-linux-conda
7580
runs-on: ubuntu-latest
7681
steps:
7782
- name: Coveralls Finished
78-
uses: AndreMiras/coveralls-python-action@develop
83+
uses: coverallsapp/github-action@v2
7984
with:
80-
parallel-finished: true
85+
parallel-finished: true
86+
87+

control/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ def __missing__(self, key):
4848
else:
4949
raise KeyError(key)
5050

51+
# New get function for Python 3.12+ to replicate old behavior
52+
def get(self, key, defval=None):
53+
# If the key exists, return it
54+
if self.__contains__(key):
55+
return self[key]
56+
57+
# If not, see if it is deprecated
58+
repl = self._check_deprecation(key)
59+
if self.__contains__(repl):
60+
return self.get(repl, defval)
61+
62+
# Otherwise, call the usual dict.get() method
63+
return super().get(key, defval)
64+
5165
def _check_deprecation(self, key):
5266
if self.__contains__(f"deprecated.{key}"):
5367
repl = self[f"deprecated.{key}"]

control/flatsys/linflat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@ def forward(self, x, u, params):
119119
x = np.reshape(x, (-1, 1))
120120
u = np.reshape(u, (1, -1))
121121
zflag = [np.zeros(self.nstates + 1)]
122-
zflag[0][0] = self.Cf @ x
122+
zflag[0][0] = (self.Cf @ x).item()
123123
H = self.Cf # initial state transformation
124124
for i in range(1, self.nstates + 1):
125-
zflag[0][i] = H @ (self.A @ x + self.B @ u)
125+
zflag[0][i] = (H @ (self.A @ x + self.B @ u)).item()
126126
H = H @ self.A # derivative for next iteration
127127
return zflag
128128

control/freqplot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def bode_plot(
162162
values with no plot.
163163
rcParams : dict
164164
Override the default parameters used for generating plots.
165-
Default is set up config.default['freqplot.rcParams'].
165+
Default is set by config.default['freqplot.rcParams'].
166166
wrap_phase : bool or float
167167
If wrap_phase is `False` (default), then the phase will be unwrapped
168168
so that it is continuously increasing or decreasing. If wrap_phase is
@@ -473,7 +473,7 @@ def bode_plot(
473473
if ax is None:
474474
with plt.rc_context(_freqplot_rcParams):
475475
ax_array = fig.subplots(nrows, ncols, squeeze=False)
476-
fig.set_tight_layout(True)
476+
fig.set_layout_engine('tight')
477477
fig.align_labels()
478478

479479
# Set up default sharing of axis limits if not specified

control/grid.py

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import numpy as np
2-
from numpy import cos, sin, sqrt, linspace, pi, exp
1+
# grid.py - code to add gridlines to root locus and pole-zero diagrams
2+
#
3+
# This code generates grids for pole-zero diagrams (including root locus
4+
# diagrams). Rather than just draw a grid in place, it uses the AxisArtist
5+
# package to generate a custom grid that will scale with the figure.
6+
#
7+
38
import matplotlib.pyplot as plt
4-
from mpl_toolkits.axisartist import SubplotHost
5-
from mpl_toolkits.axisartist.grid_helper_curvelinear \
6-
import GridHelperCurveLinear
79
import mpl_toolkits.axisartist.angle_helper as angle_helper
10+
import numpy as np
811
from matplotlib.projections import PolarAxes
912
from matplotlib.transforms import Affine2D
13+
from mpl_toolkits.axisartist import SubplotHost
14+
from mpl_toolkits.axisartist.grid_helper_curvelinear import \
15+
GridHelperCurveLinear
16+
from numpy import cos, exp, linspace, pi, sin, sqrt
17+
18+
from .iosys import isdtime
1019

1120

1221
class FormatterDMS(object):
@@ -65,14 +74,15 @@ def __call__(self, transform_xy, x1, y1, x2, y2):
6574
return lon_min, lon_max, lat_min, lat_max
6675

6776

68-
def sgrid():
77+
def sgrid(scaling=None):
6978
# From matplotlib demos:
7079
# https://matplotlib.org/gallery/axisartist/demo_curvelinear_grid.html
7180
# https://matplotlib.org/gallery/axisartist/demo_floating_axis.html
7281

7382
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
74-
# system in degree
83+
# system in degrees
7584
tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
85+
7686
# polar projection, which involves cycle, and also has limits in
7787
# its coordinates, needs a special method to find the extremes
7888
# (min, max of the coordinate within the view).
@@ -89,6 +99,7 @@ def sgrid():
8999
tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1,
90100
tick_formatter1=tick_formatter1)
91101

102+
# Set up an axes with a specialized grid helper
92103
fig = plt.gcf()
93104
ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
94105

@@ -97,15 +108,20 @@ def sgrid():
97108
ax.axis[:].major_ticklabels.set_visible(visible)
98109
ax.axis[:].major_ticks.set_visible(False)
99110
ax.axis[:].invert_ticklabel_direction()
111+
ax.axis[:].major_ticklabels.set_color('gray')
100112

113+
# Set up internal tickmarks and labels along the real/imag axes
101114
ax.axis["wnxneg"] = axis = ax.new_floating_axis(0, 180)
102115
axis.set_ticklabel_direction("-")
103116
axis.label.set_visible(False)
117+
104118
ax.axis["wnxpos"] = axis = ax.new_floating_axis(0, 0)
105119
axis.label.set_visible(False)
120+
106121
ax.axis["wnypos"] = axis = ax.new_floating_axis(0, 90)
107122
axis.label.set_visible(False)
108-
axis.set_axis_direction("left")
123+
axis.set_axis_direction("right")
124+
109125
ax.axis["wnyneg"] = axis = ax.new_floating_axis(0, 270)
110126
axis.label.set_visible(False)
111127
axis.set_axis_direction("left")
@@ -119,43 +135,41 @@ def sgrid():
119135
ax.axis["bottom"].get_helper().nth_coord_ticks = 0
120136

121137
fig.add_subplot(ax)
122-
123-
# RECTANGULAR X Y AXES WITH SCALE
124-
# par2 = ax.twiny()
125-
# par2.axis["top"].toggle(all=False)
126-
# par2.axis["right"].toggle(all=False)
127-
# new_fixed_axis = par2.get_grid_helper().new_fixed_axis
128-
# par2.axis["left"] = new_fixed_axis(loc="left",
129-
# axes=par2,
130-
# offset=(0, 0))
131-
# par2.axis["bottom"] = new_fixed_axis(loc="bottom",
132-
# axes=par2,
133-
# offset=(0, 0))
134-
# FINISH RECTANGULAR
135-
136138
ax.grid(True, zorder=0, linestyle='dotted')
137139

138-
_final_setup(ax)
140+
_final_setup(ax, scaling=scaling)
139141
return ax, fig
140142

141143

142-
def _final_setup(ax):
144+
# Utility function used by all grid code
145+
def _final_setup(ax, scaling=None):
143146
ax.set_xlabel('Real')
144147
ax.set_ylabel('Imaginary')
145-
ax.axhline(y=0, color='black', lw=1)
146-
ax.axvline(x=0, color='black', lw=1)
147-
plt.axis('equal')
148+
ax.axhline(y=0, color='black', lw=0.25)
149+
ax.axvline(x=0, color='black', lw=0.25)
148150

151+
# Set up the scaling for the axes
152+
scaling = 'equal' if scaling is None else scaling
153+
plt.axis(scaling)
149154

150-
def nogrid():
151-
f = plt.gcf()
152-
ax = plt.axes()
153155

154-
_final_setup(ax)
155-
return ax, f
156+
# If not grid is given, at least separate stable/unstable regions
157+
def nogrid(dt=None, ax=None, scaling=None):
158+
fig = plt.gcf()
159+
if ax is None:
160+
ax = fig.gca()
161+
162+
# Draw the unit circle for discrete time systems
163+
if isdtime(dt=dt, strict=True):
164+
s = np.linspace(0, 2*pi, 100)
165+
ax.plot(np.cos(s), np.sin(s), 'k--', lw=0.5, dashes=(5, 5))
156166

167+
_final_setup(ax, scaling=scaling)
168+
return ax, fig
157169

158-
def zgrid(zetas=None, wns=None, ax=None):
170+
# Grid for discrete time system (drawn, not rendered by AxisArtist)
171+
# TODO (at some point): think about using customized grid generator?
172+
def zgrid(zetas=None, wns=None, ax=None, scaling=None):
159173
"""Draws discrete damping and frequency grid"""
160174

161175
fig = plt.gcf()
@@ -206,5 +220,9 @@ def zgrid(zetas=None, wns=None, ax=None):
206220
ax.annotate(r"$\frac{"+num+r"\pi}{T}$", xy=(an_x, an_y),
207221
xytext=(an_x, an_y), size=9)
208222

209-
_final_setup(ax)
223+
# Set default axes to allow some room around the unit circle
224+
ax.set_xlim([-1.1, 1.1])
225+
ax.set_ylim([-1.1, 1.1])
226+
227+
_final_setup(ax, scaling=scaling)
210228
return ax, fig

0 commit comments

Comments
 (0)