Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build system and test suite update #759

Merged
merged 7 commits into from Aug 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/conda-env/test-env.yml
Expand Up @@ -6,6 +6,7 @@ dependencies:
- pytest
- pytest-cov
- pytest-timeout
- pytest-xvfb
- numpy
- matplotlib
- scipy
- scipy
20 changes: 4 additions & 16 deletions .github/workflows/control-slycot-src.yml
Expand Up @@ -13,19 +13,8 @@ jobs:
path: python-control
- name: Set up Python
uses: actions/setup-python@v2
- name: Install Python dependencies
run: |
# Set up conda
echo $CONDA/bin >> $GITHUB_PATH

# Set up (virtual) X11
sudo apt install -y xvfb

# Install test tools
conda install pip pytest pytest-timeout

# Install python-control dependencies
conda install numpy matplotlib scipy
- name: Install Python dependencies and test tools
run: pip install -v -e './python-control[test]'

- name: Checkout Slycot
uses: actions/checkout@v3
Expand All @@ -43,11 +32,10 @@ jobs:
# Install compilers, libraries, and development environment
sudo apt-get -y install gfortran cmake --fix-missing
sudo apt-get -y install libblas-dev liblapack-dev
conda install -c conda-forge scikit-build setuptools-scm

# Compile and install slycot
pip install -v --no-build-isolation --no-deps .
pip install -v .

- name: Test with pytest
working-directory: python-control
run: xvfb-run --auto-servernum pytest control/tests
run: pytest -v control/tests
murrayrm marked this conversation as resolved.
Show resolved Hide resolved
29 changes: 13 additions & 16 deletions .github/workflows/install_examples.yml
@@ -1,4 +1,4 @@
name: setup.py, examples
name: Setup, Examples, Notebooks

on: [push, pull_request]

Expand All @@ -7,26 +7,23 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install Python dependencies
- uses: actions/checkout@v3
- name: Install Python dependencies from conda-forge
run: |
# Set up conda
# Set up conda using the preinstalled GHA Miniconda environment
echo $CONDA/bin >> $GITHUB_PATH
conda config --add channels conda-forge
conda config --set channel_priority strict

# Set up (virtual) X11
sudo apt install -y xvfb
# Install build tools
conda install pip setuptools setuptools-scm

# Install test tools
conda install pip pytest
# Install python-control dependencies and extras
conda install numpy matplotlib scipy
conda install slycot pmw jupyter

# Install python-control dependencies
conda install numpy matplotlib scipy jupyter
conda install -c conda-forge slycot pmw

- name: Install with setup.py
run: python setup.py install
- name: Install from source
run: pip install .

- name: Run examples
run: |
Expand Down
20 changes: 11 additions & 9 deletions .github/workflows/python-package-conda.yml
Expand Up @@ -8,30 +8,32 @@ jobs:
Py${{ matrix.python-version }};
${{ matrix.slycot || 'no' }} Slycot;
${{ matrix.pandas || 'no' }} Pandas;
${{ matrix.cvxopt || 'no' }} CVXOPT;
${{ matrix.cvxopt || 'no' }} CVXOPT
${{ matrix.array-and-matrix == 1 && '; array and matrix' || '' }}
${{ matrix.mplbackend && format('; {0}', matrix.mplbackend) }}
runs-on: ubuntu-latest

strategy:
max-parallel: 5
fail-fast: false
matrix:
python-version: [3.7, 3.9]
python-version: ['3.7', '3.10']
slycot: ["", "conda"]
pandas: [""]
cvxopt: ["", "conda"]
mplbackend: [""]
array-and-matrix: [0]
include:
- python-version: 3.9
- python-version: '3.10'
slycot: conda
pandas: conda
cvxopt: conda
mplbackend: QtAgg
array-and-matrix: 1

steps:
- uses: actions/checkout@v3

- name: Set up (virtual) X11
run: sudo apt install -y xvfb

- name: Setup Conda
uses: conda-incubator/setup-miniconda@v2
with:
Expand All @@ -55,15 +57,15 @@ jobs:
mamba install pandas
fi
if [[ '${{matrix.cvxopt}}' == 'conda' ]]; then
mamba install cvxopt
mamba install cvxopt
bnavigator marked this conversation as resolved.
Show resolved Hide resolved
fi

- name: Test with pytest
shell: bash -l {0}
env:
PYTHON_CONTROL_ARRAY_AND_MATRIX: ${{ matrix.array-and-matrix }}
run: |
xvfb-run --auto-servernum pytest --cov=control --cov-config=.coveragerc control/tests
MPLBACKEND: ${{ matrix.mplbackend }}
run: pytest -v --cov=control --cov-config=.coveragerc control/tests
bnavigator marked this conversation as resolved.
Show resolved Hide resolved

- name: Coveralls parallel
# https://github.com/coverallsapp/github-action
Expand Down
15 changes: 6 additions & 9 deletions README.rst
Expand Up @@ -97,17 +97,14 @@ To install using pip::
If you install Slycot using pip you'll need a development environment
(e.g., Python development files, C and Fortran compilers).

Distutils
---------
Installing from source
----------------------

To install in your home directory, use::
To install from source, get the source code of the desired branch or release
from the github repository or archive, unpack, and run from within the
toplevel `python-control` directory::

python setup.py install --user

To install for all users (on Linux or Mac OS)::

python setup.py build
sudo python setup.py install
pip install .


Development
Expand Down
20 changes: 6 additions & 14 deletions control/tests/config_test.py
Expand Up @@ -8,7 +8,6 @@
from math import pi, log10

import matplotlib.pyplot as plt
from matplotlib.testing.decorators import cleanup as mplcleanup
import numpy as np
import pytest

Expand All @@ -18,7 +17,6 @@
@pytest.mark.usefixtures("editsdefaults") # makes sure to reset the defaults
# to the test configuration
class TestConfig:

# Create a simple second order system to use for testing
sys = ct.tf([10], [1, 2, 1])

Expand All @@ -28,8 +26,7 @@ def test_set_defaults(self):
assert ct.config.defaults['freqplot.deg'] == 2
assert ct.config.defaults['freqplot.Hz'] is None

@mplcleanup
def test_get_param(self):
def test_get_param(self, mplcleanup):
assert ct.config._get_param('freqplot', 'dB')\
== ct.config.defaults['freqplot.dB']
assert ct.config._get_param('freqplot', 'dB', 1) == 1
Expand Down Expand Up @@ -92,8 +89,7 @@ def test_default_deprecation(self):
assert ct.config.defaults['bode.Hz'] \
== ct.config.defaults['freqplot.Hz']

@mplcleanup
def test_fbs_bode(self):
def test_fbs_bode(self, mplcleanup):
ct.use_fbs_defaults()

# Generate a Bode plot
Expand Down Expand Up @@ -137,8 +133,7 @@ def test_fbs_bode(self):
phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data()
np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2)

@mplcleanup
def test_matlab_bode(self):
def test_matlab_bode(self, mplcleanup):
ct.use_matlab_defaults()

# Generate a Bode plot
Expand Down Expand Up @@ -182,8 +177,7 @@ def test_matlab_bode(self):
phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data()
np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2)

@mplcleanup
def test_custom_bode_default(self):
def test_custom_bode_default(self, mplcleanup):
ct.config.defaults['freqplot.dB'] = True
ct.config.defaults['freqplot.deg'] = True
ct.config.defaults['freqplot.Hz'] = True
Expand All @@ -204,8 +198,7 @@ def test_custom_bode_default(self):
np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3)
np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2)

@mplcleanup
def test_bode_number_of_samples(self):
def test_bode_number_of_samples(self, mplcleanup):
# Set the number of samples (default is 50, from np.logspace)
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87)
assert len(mag_ret) == 87
Expand All @@ -219,8 +212,7 @@ def test_bode_number_of_samples(self):
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87)
assert len(mag_ret) == 87

@mplcleanup
def test_bode_feature_periphery_decade(self):
def test_bode_feature_periphery_decade(self, mplcleanup):
# Generate a sample Bode plot to figure out the range it uses
ct.reset_defaults() # Make sure starting state is correct
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=False)
Expand Down
14 changes: 6 additions & 8 deletions control/tests/conftest.py
@@ -1,13 +1,11 @@
"""conftest.py - pytest local plugins and fixtures"""

import os
import sys
from contextlib import contextmanager

import matplotlib as mpl
import numpy as np
import pytest
import scipy as sp

import control

Expand Down Expand Up @@ -45,15 +43,15 @@ def control_defaults():
params=[pytest.param("arrayout", marks=matrixerrorfilter),
pytest.param("matrixout", marks=matrixfilter)])
def matarrayout(request):
"""Switch the config to use np.ndarray and np.matrix as returns"""
"""Switch the config to use np.ndarray and np.matrix as returns."""
restore = control.config.defaults['statesp.use_numpy_matrix']
control.use_numpy_matrix(request.param == "matrixout", warn=False)
yield
control.use_numpy_matrix(restore, warn=False)


def ismatarrayout(obj):
"""Test if the returned object has the correct type as configured
"""Test if the returned object has the correct type as configured.

note that isinstance(np.matrix(obj), np.ndarray) is True
"""
Expand All @@ -63,15 +61,15 @@ def ismatarrayout(obj):


def asmatarrayout(obj):
"""Return a object according to the configured default"""
"""Return a object according to the configured default."""
use_matrix = control.config.defaults['statesp.use_numpy_matrix']
matarray = np.asmatrix if use_matrix else np.asarray
return matarray(obj)


@contextmanager
def check_deprecated_matrix():
"""Check that a call produces a deprecation warning because of np.matrix"""
"""Check that a call produces a deprecation warning because of np.matrix."""
use_matrix = control.config.defaults['statesp.use_numpy_matrix']
if use_matrix:
with pytest.deprecated_call():
Expand All @@ -94,13 +92,13 @@ def check_deprecated_matrix():
False)]
if usebydefault or TEST_MATRIX_AND_ARRAY])
def matarrayin(request):
"""Use array and matrix to construct input data in tests"""
"""Use array and matrix to construct input data in tests."""
return request.param


@pytest.fixture(scope="function")
def editsdefaults():
"""Make sure any changes to the defaults only last during a test"""
"""Make sure any changes to the defaults only last during a test."""
restore = control.config.defaults.copy()
yield
control.config.defaults = restore.copy()
Expand Down
2 changes: 2 additions & 0 deletions control/tests/rlocus_test.py
Expand Up @@ -85,6 +85,8 @@ def test_root_locus_neg_false_gain_nonproper(self):

# TODO: cover and validate negative false_gain branch in _default_gains()

@pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None,
reason="Requires the zoom toolbar")
def test_root_locus_zoom(self):
"""Check the zooming functionality of the Root locus plot"""
system = TransferFunction([1000], [1, 25, 100, 0])
Expand Down
4 changes: 4 additions & 0 deletions control/tests/sisotool_test.py
Expand Up @@ -46,6 +46,8 @@ def sys221(self):
D221 = [[1., -1.]]
return StateSpace(A222, B222, C221, D221)

@pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None,
reason="Requires the zoom toolbar")
murrayrm marked this conversation as resolved.
Show resolved Hide resolved
def test_sisotool(self, tsys):
sisotool(tsys, Hz=False)
fig = plt.gcf()
Expand Down Expand Up @@ -114,6 +116,8 @@ def test_sisotool(self, tsys):
assert_array_almost_equal(
ax_step.lines[0].get_data()[1][:10], step_response_moved, 4)

@pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None,
reason="Requires the zoom toolbar")
@pytest.mark.parametrize('tsys', [0, True],
indirect=True, ids=['ctime', 'dtime'])
def test_sisotool_tvect(self, tsys):
Expand Down