Skip to content

Commit e9cf123

Browse files
Merge branch 'python-control:main' into dev-delay
2 parents 1150646 + abeb0e4 commit e9cf123

Some content is hidden

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

48 files changed

+1598
-128
lines changed

.github/scripts/set-conda-test-matrix.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,19 @@
2727
'blas_lib': cbl}
2828
conda_jobs.append(cjob)
2929

30+
# Make sure Windows jobs are included even if we didn't build any
31+
windows_pythons = ['3.11'] # Whatever you want to test
32+
33+
for py in windows_pythons:
34+
for blas in combinations['windows']:
35+
cjob = {
36+
'packagekey': f'windows-{py}',
37+
'os': 'windows',
38+
'python': py,
39+
'blas_lib': blas,
40+
'package_source': 'conda-forge'
41+
}
42+
conda_jobs.append(cjob)
43+
3044
matrix = { 'include': conda_jobs }
3145
print(json.dumps(matrix))

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ jobs:
107107
os:
108108
- 'ubuntu'
109109
- 'macos'
110-
- 'windows'
111110
python:
112111
# build on one, expand matrix in conda-build from the Sylcot/conda-recipe/conda_build_config.yaml
113112
- '3.11'
@@ -332,7 +331,13 @@ jobs:
332331
echo "libblas * *mkl" >> $CONDA_PREFIX/conda-meta/pinned
333332
;;
334333
esac
335-
conda install -c ./slycot-conda-pkgs slycot
334+
if [ "${{ matrix.os }}" = "windows" ]; then
335+
echo "Installing slycot from conda-forge on Windows"
336+
conda install slycot
337+
else
338+
echo "Installing built conda package from local channel"
339+
conda install -c ./slycot-conda-pkgs slycot
340+
fi
336341
conda list
337342
- name: Test with pytest
338343
run: JOBNAME="$JOBNAME" pytest control/tests

control/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def use_legacy_defaults(version):
297297
Parameters
298298
----------
299299
version : string
300-
Version number of the defaults desired. Ranges from '0.1' to '0.10.1'.
300+
Version number of `python-control` to use for setting defaults.
301301
302302
Examples
303303
--------
@@ -342,6 +342,14 @@ def use_legacy_defaults(version):
342342
#
343343
reset_defaults() # start from a clean slate
344344

345+
# Version 0.10.2:
346+
if major == 0 and minor < 10 or (minor == 10 and patch < 2):
347+
from math import inf
348+
349+
# Reset Nyquist defaults
350+
set_defaults('nyquist', arrows=2, max_curve_magnitude=20,
351+
blend_fraction=0, indent_points=50)
352+
345353
# Version 0.9.2:
346354
if major == 0 and minor < 9 or (minor == 9 and patch < 2):
347355
from math import inf

control/ctrlplot.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
# # Customize axes (curvilinear grids, shared axes, etc)
4242
#
4343
# # Plot the data
44-
# lines = np.full(ax_array.shape, [])
44+
# lines = np.empty(ax_array.shape, dtype=object)
45+
# for i in range(ax_array.shape[0]):
46+
# for j in range(ax_array.shape[1]):
47+
# lines[i, j] = []
4548
# line_labels = _process_line_labels(label, ntraces, nrows, ncols)
4649
# color_offset, color_cycle = _get_color_offset(ax)
4750
# for i, j in itertools.product(range(nrows), range(ncols)):

control/descfcn.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ def describing_function_plot(
521521
# Plot the Nyquist response
522522
cplt = dfresp.response.plot(**kwargs)
523523
ax = cplt.axes[0, 0] # Get the axes where the plot was made
524-
lines[0] = cplt.lines[0] # Return Nyquist lines for first system
524+
lines[0] = np.concatenate( # Return Nyquist lines for first system
525+
cplt.lines.flatten()).tolist()
525526

526527
# Add the describing function curve to the plot
527528
lines[1] = ax.plot(dfresp.N_vals.real, dfresp.N_vals.imag)

control/freqplot.py

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,13 +1101,14 @@ def gen_zero_centered_series(val_min, val_max, period):
11011101
_nyquist_defaults = {
11021102
'nyquist.primary_style': ['-', '-.'], # style for primary curve
11031103
'nyquist.mirror_style': ['--', ':'], # style for mirror curve
1104-
'nyquist.arrows': 2, # number of arrows around curve
1104+
'nyquist.arrows': 3, # number of arrows around curve
11051105
'nyquist.arrow_size': 8, # pixel size for arrows
11061106
'nyquist.encirclement_threshold': 0.05, # warning threshold
11071107
'nyquist.indent_radius': 1e-4, # indentation radius
11081108
'nyquist.indent_direction': 'right', # indentation direction
1109-
'nyquist.indent_points': 50, # number of points to insert
1110-
'nyquist.max_curve_magnitude': 20, # clip large values
1109+
'nyquist.indent_points': 200, # number of points to insert
1110+
'nyquist.max_curve_magnitude': 15, # rescale large values
1111+
'nyquist.blend_fraction': 0.15, # when to start scaling
11111112
'nyquist.max_curve_offset': 0.02, # offset of primary/mirror
11121113
'nyquist.start_marker': 'o', # marker at start of curve
11131114
'nyquist.start_marker_size': 4, # size of the marker
@@ -1639,6 +1640,10 @@ def nyquist_plot(
16391640
The matplotlib axes to draw the figure on. If not specified and
16401641
the current figure has a single axes, that axes is used.
16411642
Otherwise, a new figure is created.
1643+
blend_fraction : float, optional
1644+
For portions of the Nyquist curve that are scaled to have a maximum
1645+
magnitude of `max_curve_magnitude`, begin a smooth rescaling at
1646+
this fraction of `max_curve_magnitude`. Default value is 0.15.
16421647
encirclement_threshold : float, optional
16431648
Define the threshold for generating a warning if the number of net
16441649
encirclements is a non-integer value. Default value is 0.05 and can
@@ -1655,7 +1660,7 @@ def nyquist_plot(
16551660
portions of the contour are plotted using a different line style.
16561661
label : str or array_like of str, optional
16571662
If present, replace automatically generated label(s) with the given
1658-
label(s). If sysdata is a list, strings should be specified for each
1663+
label(s). If `data` is a list, strings should be specified for each
16591664
system.
16601665
label_freq : int, optional
16611666
Label every nth frequency on the plot. If not specified, no labels
@@ -1686,8 +1691,8 @@ def nyquist_plot(
16861691
elements is equivalent to providing `omega_limits`.
16871692
omega_num : int, optional
16881693
Number of samples to use for the frequency range. Defaults to
1689-
`config.defaults['freqplot.number_of_samples']`. Ignored if data is
1690-
not a list of systems.
1694+
`config.defaults['freqplot.number_of_samples']`. Ignored if `data`
1695+
is not a system or list of systems.
16911696
plot : bool, optional
16921697
(legacy) If given, `nyquist_plot` returns the legacy return values
16931698
of (counts, contours). If False, return the values with no plot.
@@ -1752,8 +1757,8 @@ def nyquist_plot(
17521757
to avoid poles, resulting in a scaling of the Nyquist plot, the line
17531758
styles are according to the settings of the `primary_style` and
17541759
`mirror_style` keywords. By default the scaled portions of the primary
1755-
curve use a dotted line style and the scaled portion of the mirror
1756-
image use a dashdot line style.
1760+
curve use a dashdot line style and the scaled portions of the mirror
1761+
image use a dotted line style.
17571762
17581763
Examples
17591764
--------
@@ -1785,6 +1790,8 @@ def nyquist_plot(
17851790
ax_user = ax
17861791
max_curve_magnitude = config._get_param(
17871792
'nyquist', 'max_curve_magnitude', kwargs, _nyquist_defaults, pop=True)
1793+
blend_fraction = config._get_param(
1794+
'nyquist', 'blend_fraction', kwargs, _nyquist_defaults, pop=True)
17881795
max_curve_offset = config._get_param(
17891796
'nyquist', 'max_curve_offset', kwargs, _nyquist_defaults, pop=True)
17901797
rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
@@ -1879,10 +1886,16 @@ def _parse_linestyle(style_name, allow_false=False):
18791886
legend_loc, _, show_legend = _process_legend_keywords(
18801887
kwargs, None, 'upper right')
18811888

1889+
# Figure out where the blended curve should start
1890+
if blend_fraction < 0 or blend_fraction > 1:
1891+
raise ValueError("blend_fraction must be between 0 and 1")
1892+
blend_curve_start = (1 - blend_fraction) * max_curve_magnitude
1893+
18821894
# Create a list of lines for the output
1883-
out = np.empty(len(nyquist_responses), dtype=object)
1884-
for i in range(out.shape[0]):
1885-
out[i] = [] # unique list in each element
1895+
out = np.empty((len(nyquist_responses), 4), dtype=object)
1896+
for i in range(len(nyquist_responses)):
1897+
for j in range(4):
1898+
out[i, j] = [] # unique list in each element
18861899

18871900
for idx, response in enumerate(nyquist_responses):
18881901
resp = response.response
@@ -1893,20 +1906,31 @@ def _parse_linestyle(style_name, allow_false=False):
18931906

18941907
# Find the different portions of the curve (with scaled pts marked)
18951908
reg_mask = np.logical_or(
1896-
np.abs(resp) > max_curve_magnitude,
1897-
splane_contour.real != 0)
1898-
# reg_mask = np.logical_or(
1899-
# np.abs(resp.real) > max_curve_magnitude,
1900-
# np.abs(resp.imag) > max_curve_magnitude)
1909+
np.abs(resp) > blend_curve_start,
1910+
np.logical_not(np.isclose(splane_contour.real, 0)))
19011911

19021912
scale_mask = ~reg_mask \
19031913
& np.concatenate((~reg_mask[1:], ~reg_mask[-1:])) \
19041914
& np.concatenate((~reg_mask[0:1], ~reg_mask[:-1]))
19051915

19061916
# Rescale the points with large magnitude
1907-
rescale = np.logical_and(
1908-
reg_mask, abs(resp) > max_curve_magnitude)
1909-
resp[rescale] *= max_curve_magnitude / abs(resp[rescale])
1917+
rescale_idx = (np.abs(resp) > blend_curve_start)
1918+
1919+
if np.any(rescale_idx): # Only process if rescaling is needed
1920+
subset = resp[rescale_idx]
1921+
abs_subset = np.abs(subset)
1922+
unit_vectors = subset / abs_subset # Preserve phase/direction
1923+
1924+
if blend_curve_start == max_curve_magnitude:
1925+
# Clip at max_curve_magnitude
1926+
resp[rescale_idx] = max_curve_magnitude * unit_vectors
1927+
else:
1928+
# Logistic scaling
1929+
newmag = blend_curve_start + \
1930+
(max_curve_magnitude - blend_curve_start) * \
1931+
(abs_subset - blend_curve_start) / \
1932+
(abs_subset + max_curve_magnitude - 2 * blend_curve_start)
1933+
resp[rescale_idx] = newmag * unit_vectors
19101934

19111935
# Get the label to use for the line
19121936
label = response.sysname if line_labels is None else line_labels[idx]
@@ -1917,7 +1941,7 @@ def _parse_linestyle(style_name, allow_false=False):
19171941
p = ax.plot(
19181942
x_reg, y_reg, primary_style[0], color=color, label=label, **kwargs)
19191943
c = p[0].get_color()
1920-
out[idx] += p
1944+
out[idx, 0] += p
19211945

19221946
# Figure out how much to offset the curve: the offset goes from
19231947
# zero at the start of the scaled section to max_curve_offset as
@@ -1929,12 +1953,12 @@ def _parse_linestyle(style_name, allow_false=False):
19291953
x_scl = np.ma.masked_where(scale_mask, resp.real)
19301954
y_scl = np.ma.masked_where(scale_mask, resp.imag)
19311955
if x_scl.count() >= 1 and y_scl.count() >= 1:
1932-
out[idx] += ax.plot(
1956+
out[idx, 1] += ax.plot(
19331957
x_scl * (1 + curve_offset),
19341958
y_scl * (1 + curve_offset),
19351959
primary_style[1], color=c, **kwargs)
19361960
else:
1937-
out[idx] += [None]
1961+
out[idx, 1] += [None]
19381962

19391963
# Plot the primary curve (invisible) for setting arrows
19401964
x, y = resp.real.copy(), resp.imag.copy()
@@ -1949,15 +1973,15 @@ def _parse_linestyle(style_name, allow_false=False):
19491973
# Plot the mirror image
19501974
if mirror_style is not False:
19511975
# Plot the regular and scaled segments
1952-
out[idx] += ax.plot(
1976+
out[idx, 2] += ax.plot(
19531977
x_reg, -y_reg, mirror_style[0], color=c, **kwargs)
19541978
if x_scl.count() >= 1 and y_scl.count() >= 1:
1955-
out[idx] += ax.plot(
1979+
out[idx, 3] += ax.plot(
19561980
x_scl * (1 - curve_offset),
19571981
-y_scl * (1 - curve_offset),
19581982
mirror_style[1], color=c, **kwargs)
19591983
else:
1960-
out[idx] += [None]
1984+
out[idx, 3] += [None]
19611985

19621986
# Add the arrows (on top of an invisible contour)
19631987
x, y = resp.real.copy(), resp.imag.copy()
@@ -1967,12 +1991,15 @@ def _parse_linestyle(style_name, allow_false=False):
19671991
_add_arrows_to_line2D(
19681992
ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=-1)
19691993
else:
1970-
out[idx] += [None, None]
1994+
out[idx, 2] += [None]
1995+
out[idx, 3] += [None]
19711996

19721997
# Mark the start of the curve
19731998
if start_marker:
1974-
ax.plot(resp[0].real, resp[0].imag, start_marker,
1975-
color=c, markersize=start_marker_size)
1999+
segment = 0 if 0 in rescale_idx else 1 # regular vs scaled
2000+
out[idx, segment] += ax.plot(
2001+
resp[0].real, resp[0].imag, start_marker,
2002+
color=c, markersize=start_marker_size)
19762003

19772004
# Mark the -1 point
19782005
ax.plot([-1], [0], 'r+')

control/iosys.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ class NamedSignal(np.ndarray):
4646
This class modifies the `numpy.ndarray` class and allows signals to
4747
be accessed using the signal name in addition to indices and slices.
4848
49+
Signals can be either a 2D array, index by signal and time, or a 3D
50+
array, indexed by signal, trace, and time.
51+
52+
Attributes
53+
----------
54+
signal_labels : list of str
55+
Label names for each of the signal elements in the signal.
56+
trace_labels : list of str, optional
57+
Label names for each of the traces in the signal (if multi-trace).
58+
59+
Examples
60+
--------
61+
>>> sys = ct.rss(
62+
... states=['p1', 'p2', 'p3'], inputs=['u1', 'u2'], outputs=['y'])
63+
>>> resp = ct.step_response(sys)
64+
>>> resp.states['p1', 'u1'] # Step response from u1 to p1
65+
NamedSignal(...)
66+
4967
"""
5068
def __new__(cls, input_array, signal_labels=None, trace_labels=None):
5169
# See https://numpy.org/doc/stable/user/basics.subclassing.html
@@ -314,7 +332,7 @@ def _repr_latex_(self):
314332
def _repr_html_(self):
315333
# Defaults to using __repr__; override in subclasses
316334
return None
317-
335+
318336
def _repr_markdown_(self):
319337
return self._repr_html_()
320338

0 commit comments

Comments
 (0)