diff --git a/.github/scripts/run-tests.sh b/.github/scripts/run-tests.sh index 2ba81756..f9a8b9fa 100644 --- a/.github/scripts/run-tests.sh +++ b/.github/scripts/run-tests.sh @@ -18,6 +18,7 @@ donttest="$donttest or test_default_deprecation" pytest control/tests \ --cov=$slycot_libdir \ --cov-config=${slycot_srcdir}/.coveragerc \ + --ignore=control/tests/docstrings_test.py \ -k "not ($donttest)" mv .coverage ${slycot_srcdir}/.coverage.control popd diff --git a/.github/scripts/set-conda-test-matrix.py b/.github/scripts/set-conda-test-matrix.py index 954480cb..2e0b9568 100644 --- a/.github/scripts/set-conda-test-matrix.py +++ b/.github/scripts/set-conda-test-matrix.py @@ -10,10 +10,8 @@ 'win': 'windows', } -blas_implementations = ['unset', 'Generic', 'OpenBLAS', 'Intel10_64lp'] - -combinations = {'ubuntu': blas_implementations, - 'macos': blas_implementations, +combinations = {'ubuntu': ['unset', 'Generic', 'OpenBLAS', 'Intel10_64lp'], + 'macos': ['unset', 'Generic', 'OpenBLAS'], 'windows': ['unset', 'Intel10_64lp'], } diff --git a/.github/workflows/slycot-build-and-test.yml b/.github/workflows/slycot-build-and-test.yml index 60b978c6..9970af4d 100644 --- a/.github/workflows/slycot-build-and-test.yml +++ b/.github/workflows/slycot-build-and-test.yml @@ -12,6 +12,24 @@ on: jobs: + ruff-lint: + name: Static lint checks with ruff + runs-on: ubuntu-latest + steps: + - name: Checkout Slycot + uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: 'recursive' + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Run ruff check + run: | + pip install ruff + ruff check slycot + build-sdist: # Super fast sniff build. If this fails, don't start the other jobs name: Build sdist on Ubuntu @@ -106,14 +124,14 @@ jobs: unset | Generic | Apple ) ;; # Found in system OpenBLAS ) brew install openblas - echo "BLAS_ROOT=/usr/local/opt/openblas/" >> $GITHUB_ENV - echo "LAPACK_ROOT=/usr/local/opt/openblas/" >> $GITHUB_ENV + echo "LDFLAGS=-L/opt/homebrew/opt/openblas/lib" >> $GITHUB_ENV + echo "CPPFLAGS=-I/opt/homebrew/opt/openblas/include" >> $GITHUB_ENV ;; *) echo "bla_vendor option ${{ matrix.bla_vendor }} not supported" exit 1 ;; esac - echo "FC=gfortran-11" >> $GITHUB_ENV + echo "FC=gfortran-14" >> $GITHUB_ENV - name: Build wheel env: BLA_VENDOR: ${{ matrix.bla_vendor }} @@ -126,10 +144,11 @@ jobs: mkdir -p ${wheeldir} cp ./slycot*.whl ${wheeldir}/ - name: Save wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: slycot-wheels + name: slycot-wheels-${{ matrix.os }}-${{ matrix.python }}-${{ matrix.bla_vendor }} path: slycot-wheels + retention-days: 5 build-conda: name: Build conda, ${{ matrix.os }} @@ -153,21 +172,21 @@ jobs: fetch-depth: 0 submodules: 'recursive' - name: Setup Conda - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python }} activate-environment: build-env environment-file: .github/conda-env/build-env.yml miniforge-version: latest - miniforge-variant: Mambaforge channel-priority: strict + channels: conda-forge,defaults auto-update-conda: false auto-activate-base: false - name: Conda build - shell: bash -l {0} + shell: bash -el {0} run: | set -e - conda mambabuild conda-recipe + conda build conda-recipe # preserve directory structure for custom conda channel find "${CONDA_PREFIX}/conda-bld" -maxdepth 2 -name 'slycot*.tar.bz2' | while read -r conda_pkg; do conda_platform=$(basename $(dirname "${conda_pkg}")) @@ -176,10 +195,11 @@ jobs: done python -m conda_index ./slycot-conda-pkgs - name: Save to local conda pkg channel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: slycot-conda-pkgs + name: slycot-conda-pkgs-${{ matrix.os }}-${{ matrix.python }} path: slycot-conda-pkgs + retention-days: 5 create-wheel-test-matrix: name: Create wheel test matrix @@ -189,15 +209,23 @@ jobs: outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: + - name: Merge artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: slycot-wheels + pattern: slycot-wheels-* - name: Checkout Slycot uses: actions/checkout@v3 - name: Download wheels (if any) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: 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: name: Create conda test matrix @@ -207,15 +235,23 @@ jobs: outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: + - name: Merge artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: slycot-conda-pkgs + pattern: slycot-conda-pkgs-* - name: Checkout Slycot uses: actions/checkout@v3 - name: Download conda packages - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: 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: @@ -249,7 +285,7 @@ jobs: sudo apt-get -y update case ${{ matrix.blas_lib }} in Generic ) sudo apt-get -y install libblas3 liblapack3 ;; - unset | OpenBLAS ) sudo apt-get -y install libopenblas-base ;; + unset | OpenBLAS ) sudo apt-get -y install libopenblas0 ;; *) echo "BLAS ${{ matrix.blas_lib }} not supported for wheels on Ubuntu" exit 1 ;; @@ -272,7 +308,7 @@ jobs: exit 1 ;; esac - name: Download wheels - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: slycot-wheels path: slycot-wheels @@ -305,7 +341,7 @@ jobs: defaults: run: - shell: bash -l {0} + shell: bash -el {0} steps: - name: Checkout Slycot @@ -321,17 +357,17 @@ jobs: if: matrix.os == 'macos' run: brew install coreutils - name: Setup Conda - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python }} miniforge-version: latest - miniforge-variant: Mambaforge activate-environment: test-env environment-file: slycot-src/.github/conda-env/test-env.yml + channels: conda-forge,defaults channel-priority: strict auto-activate-base: false - name: Download conda packages - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: slycot-conda-pkgs path: slycot-conda-pkgs @@ -340,25 +376,25 @@ jobs: set -e case ${{ matrix.blas_lib }} in unset ) # the conda-forge default (os dependent) - mamba install libblas libcblas liblapack + conda install libblas libcblas liblapack ;; Generic ) - mamba install 'libblas=*=*netlib' 'libcblas=*=*netlib' 'liblapack=*=*netlib' + conda install 'libblas=*=*netlib' 'libcblas=*=*netlib' 'liblapack=*=*netlib' echo "libblas * *netlib" >> $CONDA_PREFIX/conda-meta/pinned ;; OpenBLAS ) - mamba install 'libblas=*=*openblas' openblas + conda install 'libblas=*=*openblas' openblas echo "libblas * *openblas" >> $CONDA_PREFIX/conda-meta/pinned ;; Intel10_64lp ) - mamba install 'libblas=*=*mkl' mkl + conda install 'libblas=*=*mkl' mkl echo "libblas * *mkl" >> $CONDA_PREFIX/conda-meta/pinned ;; esac - mamba install -c ./slycot-conda-pkgs slycot + conda install -c ./slycot-conda-pkgs slycot conda list - name: Slycot and python-control tests - run: JOBNAME="$JOBNAME" bash slycot-src/.github/scripts/run-tests.sh + run: JOBNAME="$JOBNAME" bash -el slycot-src/.github/scripts/run-tests.sh env: JOBNAME: conda ${{ matrix.packagekey }} ${{ matrix.blas_lib }} - name: report coverage diff --git a/pyproject.toml b/pyproject.toml index c984562f..dea6895f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,3 +51,6 @@ write_to = "slycot/version.py" [tool.pytest.ini_options] # run the tests with compiled and installed package addopts = "--pyargs slycot" + +[tool.ruff.lint] +ignore = [ "E741" ] diff --git a/slycot/__init__.py b/slycot/__init__.py index c1243ea5..6cb415d0 100644 --- a/slycot/__init__.py +++ b/slycot/__init__.py @@ -79,6 +79,17 @@ from .version import __version__ + __all__ = [ + ab01nd, ab04md, ab05md, ab05nd, ab07nd, ab08nd, ab08nz, + ab09ad, ab09ax, ab09bd, ab09md, ab09nd, ab13bd, ab13dd, + ab13ed, ab13fd, ab13md, ag08bd, mb02ed, mb03rd, mb03vd, + mb03vy, mb03wd, mb05md, mb05nd, mc01td, sb01bd, sb02md, + sb02mt, sb02od, sb03md, sb03md57, sb03od, sb04md, sb04qd, + sb10ad, sb10dd, sb10fd, sb10hd, sb10jd, sb10yd, sg02ad, + sg03ad, sg03bd, tb01id, tb01pd, tb03ad, tb04ad, tb05ad, + tc01od, tc04ad, td04ad, tf01md, tf01rd, tg01ad, tg01fd, + __version__ + ] def test(): import pytest diff --git a/slycot/analysis.py b/slycot/analysis.py index 230eedb9..4d5383c4 100644 --- a/slycot/analysis.py +++ b/slycot/analysis.py @@ -1827,7 +1827,7 @@ def ab13md(Z, nblock, itype, x=None): else: fact='F' if len(x) != m+mr-1: - raise ValueError(f'Require len(x)==m+mr-1, but' + raise ValueError('Require len(x)==m+mr-1, but' + f' len(x)={len(x)}, m={m}, mr={mr}') x = np.concatenate([x,np.zeros(2*m-1-len(x))]) diff --git a/slycot/examples.py b/slycot/examples.py index cf306913..9e41a256 100644 --- a/slycot/examples.py +++ b/slycot/examples.py @@ -33,7 +33,6 @@ def sb02md_example(): print('rcond =', out[1]) def sb03md_example(): - from numpy import zeros A = array([ [3, 1, 1], [1, 3, 0], [0, 0, 3]]) @@ -47,7 +46,7 @@ def sb03md_example(): print('scaling factor:', out[3]) def ab08nd_example(): - from numpy import zeros, size + from numpy import zeros from scipy.linalg import eigvals A = array([ [1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0], @@ -153,7 +152,7 @@ def mc01td_example(): print('The polynomial has', out[2], 'unstable zeros') def sb02od_example(): - from numpy import zeros, shape, dot, ones + from numpy import dot, ones A = array([ [0, 1], [0, 0]]) B = array([ [0], diff --git a/slycot/src/analysis.pyf b/slycot/src/analysis.pyf index 1789d893..d161e16f 100644 --- a/slycot/src/analysis.pyf +++ b/slycot/src/analysis.pyf @@ -315,7 +315,7 @@ subroutine ab09nd(dico,job,equil,ordsel,n,m,p,nr,alpha,a,lda,b,ldb,c,ldc,d,ldd,n double precision intent(out),dimension(n),depend(n) :: hsv double precision :: tol1 =0.0 double precision :: tol2 =0.0 - integer intent(hide,cache),dimension(max(m,p)) :: iwork + integer intent(hide,cache),dimension(max(1,2*n)) :: iwork double precision intent(hide,cache),dimension(ldwork) :: dwork integer optional :: ldwork = max(1,n*(2*n+max(n,max(m,p))+5)+n*(n+1)/2) integer intent(out) :: iwarn diff --git a/slycot/tests/test_ab09nd.py b/slycot/tests/test_ab09nd.py new file mode 100644 index 00000000..dd08097a --- /dev/null +++ b/slycot/tests/test_ab09nd.py @@ -0,0 +1,93 @@ +# ab09nd - model order reduction + +import numpy as np +from slycot import ab09nd + +# SLICOT reference test; see SLICOT-Reference/examples/AB09ND.dat, AB09ND.res, TAB09ND.f +def test_slicot_ref(): + n = 7 + m = 2 + p = 3 + nr = None # Slycot uses None for ordsel = 'A' + alpha = -0.6 + tol1 = 1e-1 + tol2 = 1e-14 + dico = 'C' + job = 'N' + equil = 'N' + + a = np.array([[-0.04165, 0.0000, 4.9200, -4.9200, 0.0000, 0.0000, 0.0000], + [-5.2100, -12.500, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], + [0.0000, 3.3300, -3.3300, 0.0000, 0.0000, 0.0000, 0.0000], + [0.5450, 0.0000, 0.0000, 0.0000, -0.5450, 0.0000, 0.0000], + [0.0000, 0.0000, 0.0000, 4.9200, -0.04165, 0.0000, 4.9200], + [0.0000, 0.0000, 0.0000, 0.0000, -5.2100, -12.500, 0.0000], + [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 3.3300, -3.3300]]) + + b = np.array([[0.0000, 0.0000], + [12.500, 0.0000], + [0.0000, 0.0000], + [0.0000, 0.0000], + [0.0000, 0.0000], + [0.0000, 12.500], + [0.0000, 0.0000]]) + + c = np.array([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], + [0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000], + [0.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000]]) + + d = np.zeros((3,2)) + + nr, ar, br, cr, dr, ns, hsv = \ + ab09nd(dico, job, equil, n, m, p, a, b, c, d, alpha, nr, tol1, tol2) + + # reference values + ref_nr = 5 + ref_hsv = np.array([1.9178, 0.8621, 0.7666, 0.0336, 0.0246]) + ref_ar = np.array([[-0.5181, -1.1084, 0.0000, 0.0000, 0.0000], + [ 8.8157, -0.5181, 0.0000, 0.0000, 0.0000], + [ 0.0000, 0.0000, 0.5847, 0.0000, 1.9230], + [ 0.0000, 0.0000, 0.0000, -1.6606, 0.0000], + [ 0.0000, 0.0000, -4.3823, 0.0000, -3.2922]]) + + ref_br = np.array([[-1.2837, 1.2837], + [-0.7522, 0.7522], + [-0.6379, -0.6379], + [ 2.0656, -2.0656], + [-3.9315, -3.9315]]) + + ref_cr = np.array([[-0.1380, -0.6445, -0.6416, -0.6293, 0.2526], + [ 0.6246, 0.0196, 0.0000, 0.4107, 0.0000], + [ 0.1380, 0.6445, -0.6416, 0.6293, 0.2526]]) + + ref_dr = np.array([[ 0.0582, -0.0090], + [ 0.0015, -0.0015], + [-0.0090, 0.0582]]) + + assert nr == ref_nr + + np.testing.assert_array_almost_equal(hsv[:nr], ref_hsv, decimal=4) + np.testing.assert_array_almost_equal(ar, ref_ar, decimal=4) + np.testing.assert_array_almost_equal(br, ref_br, decimal=4) + np.testing.assert_array_almost_equal(cr, ref_cr, decimal=4) + np.testing.assert_array_almost_equal(dr, ref_dr, decimal=4) + + +# gh-242 regression test +# iwork was incorrectly sized +def test_gh242_regression(): + n = 67 + m = 1 + p = 1 + + a = -np.eye(n) + b = np.zeros((n, m)) + c = np.zeros((p, n)) + d = np.array([[42.24]]) + + nr, ar, br, cr, dr, ns, hsv = \ + ab09nd(dico='C', job='B', equil='S', n=a.shape[0], + m=b.shape[1], p=c.shape[0], A=a, B=b, C=c, D=d) + + assert nr == 0 + np.testing.assert_equal(d, dr) diff --git a/slycot/tests/test_ab13bd.py b/slycot/tests/test_ab13bd.py index dd2a735a..ed4f6b92 100644 --- a/slycot/tests/test_ab13bd.py +++ b/slycot/tests/test_ab13bd.py @@ -2,7 +2,7 @@ # ab08n* tests import numpy as np -from numpy.testing import assert_allclose, assert_array_equal, assert_equal +from numpy.testing import assert_allclose, assert_array_equal from scipy import linalg, signal from slycot import analysis diff --git a/slycot/tests/test_ab13md.py b/slycot/tests/test_ab13md.py index 5a30f5ba..ac933e1f 100644 --- a/slycot/tests/test_ab13md.py +++ b/slycot/tests/test_ab13md.py @@ -1,6 +1,6 @@ import numpy as np import pytest -from numpy.testing import assert_allclose, assert_array_less +from numpy.testing import assert_allclose from slycot import ab13md diff --git a/slycot/tests/test_mb.py b/slycot/tests/test_mb.py index 5dc5dcd5..745d382a 100644 --- a/slycot/tests/test_mb.py +++ b/slycot/tests/test_mb.py @@ -2,8 +2,6 @@ # test_mb.py - test suite for linear algebra commands # bnavigator , Aug 2019 -import sys - import numpy as np import pytest from numpy.testing import assert_allclose @@ -96,19 +94,6 @@ def test_mb02ed_parameter_errors(): [1.0000, 2.0000], ] ) - X = np.array( - [ - [0.2408, 0.4816], - [0.1558, 0.3116], - [0.1534, 0.3068], - [0.2302, 0.4603], - [0.1467, 0.2934], - [0.1537, 0.3075], - [0.2349, 0.4698], - [0.1498, 0.2995], - [0.1653, 0.3307], - ] - ) # Test for wrong parameter typet with pytest.raises(expected_exception=SlycotParameterError, match='typet must be either "R" or "C"') as cm: @@ -162,19 +147,6 @@ def test_mb02ed_matrix_error(): [1.0000, 2.0000], ] ) - X = np.array( - [ - [0.2408, 0.4816], - [0.1558, 0.3116], - [0.1534, 0.3068], - [0.2302, 0.4603], - [0.1467, 0.2934], - [0.1537, 0.3075], - [0.2349, 0.4698], - [0.1498, 0.2995], - [0.1653, 0.3307], - ] - ) with pytest.raises(SlycotArithmeticError, match = "The reduction algorithm failed. " diff --git a/slycot/tests/test_sb.py b/slycot/tests/test_sb.py index e6f97a07..ceccad3b 100644 --- a/slycot/tests/test_sb.py +++ b/slycot/tests/test_sb.py @@ -7,7 +7,7 @@ from slycot import synthesis from slycot.exceptions import (SlycotArithmeticError, SlycotParameterError, - SlycotResultWarning, raise_if_slycot_error) + SlycotResultWarning) from .test_exceptions import assert_docstring_parse diff --git a/slycot/tests/test_tb05ad.py b/slycot/tests/test_tb05ad.py index 900a49a2..e940c45f 100644 --- a/slycot/tests/test_tb05ad.py +++ b/slycot/tests/test_tb05ad.py @@ -1,8 +1,6 @@ # =================================================== # tb05ad tests -import sys - import numpy as np import pytest from numpy.testing import assert_almost_equal diff --git a/slycot/tests/test_tg01ad.py b/slycot/tests/test_tg01ad.py index 864d41be..885f4c1c 100644 --- a/slycot/tests/test_tg01ad.py +++ b/slycot/tests/test_tg01ad.py @@ -2,7 +2,7 @@ # tg01ad tests import numpy as np -from numpy.testing import assert_almost_equal, assert_equal, assert_raises +from numpy.testing import assert_almost_equal from slycot import transform diff --git a/slycot/tests/test_tg01fd.py b/slycot/tests/test_tg01fd.py index 902d1ef2..27bc9cb4 100644 --- a/slycot/tests/test_tg01fd.py +++ b/slycot/tests/test_tg01fd.py @@ -2,7 +2,7 @@ # tg01fd tests import numpy as np -from numpy.testing import assert_almost_equal, assert_equal, assert_raises +from numpy.testing import assert_almost_equal, assert_equal from slycot import transform