diff --git a/.circleci/config.yml b/.circleci/config.yml index 96cba7e6dc5d..2e3cb30a58ef 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,12 +42,6 @@ commands: command: | echo $(git log -1 --pretty=%B) | tee gitlog.txt echo ${CI_PULL_REQUEST//*pull\//} | tee merge.txt - if [[ $(cat merge.txt) != "" ]]; then - echo "Merging $(cat merge.txt)"; - git remote add upstream https://github.com/scipy/scipy.git; - git pull --ff-only upstream "refs/pull/$(cat merge.txt)/merge"; - git fetch upstream main; - fi jobs: # Build SciPy from source diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1e9e39ab3a5f..f1f1e694bc7d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,9 +3,9 @@ contact_links: - name: Stack Overflow url: https://stackoverflow.com/questions/tagged/scipy about: Please ask and answer usage questions on Stack Overflow - - name: Developer Mailing list - url: https://mail.python.org/mailman3/lists/scipy-dev.python.org/ - about: Development discussions and announcements on the mailing list + - name: Developer Forum + url: https://discuss.scientific-python.org/c/contributor/scipy + about: Development discussions and announcements on the forum - name: Blank issue url: https://github.com/scipy/scipy/issues/new about: Please note that other templates should be used in most cases diff --git a/.github/workflows/array_api.yml b/.github/workflows/array_api.yml index 1166702a5662..75289af89abc 100644 --- a/.github/workflows/array_api.yml +++ b/.github/workflows/array_api.yml @@ -78,7 +78,7 @@ jobs: echo "timestamp=${NOW}" >> $GITHUB_OUTPUT - name: Setup compiler cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-ccache with: path: ${{ steps.prep-ccache.outputs.dir }} @@ -100,8 +100,4 @@ jobs: python dev.py --no-build test -b all -t scipy.special.tests.test_support_alternative_backends -- --durations 3 --timeout=60 python dev.py --no-build test -b all -t scipy._lib.tests.test_array_api -- --durations 3 --timeout=60 python dev.py --no-build test -b all -t scipy._lib.tests.test__util -- --durations 3 --timeout=60 - python dev.py --no-build test -b all -t scipy.stats.tests.test_stats -- --durations 3 --timeout=60 - python dev.py --no-build test -b all -t scipy.stats.tests.test_morestats -- --durations 3 --timeout=60 - python dev.py --no-build test -b all -t scipy.stats.tests.test_variation -- --durations 3 --timeout=60 - python dev.py --no-build test -b all -t scipy.stats.tests.test_resampling -- --durations 3 --timeout=60 - python dev.py --no-build test -b all -t scipy.optimize.tests.test_chandrupatla -- --durations 3 --timeout=60 + python dev.py --no-build test -b all -s stats -- --durations 3 --timeout=60 diff --git a/.github/workflows/free_threaded_wheels.yml b/.github/workflows/free_threaded_wheels.yml new file mode 100644 index 000000000000..ecb0800dc8ad --- /dev/null +++ b/.github/workflows/free_threaded_wheels.yml @@ -0,0 +1,160 @@ +# Workflow to build and test wheels for the free-threaded Python build. +# +# This should be merged back into wheels.yml when free-threaded wheel +# builds can be uploaded to pypi along with the rest of scipy's release +# artifacts. +# +# To work on the wheel building infrastructure on a fork, comment out: +# +# if: github.repository == 'scipy/scipy' +# +# in the get_commit_message job. Be sure to include [wheel build] in your commit +# message to trigger the build. All files related to wheel building are located +# at tools/wheels/ +name: Free-Threaded Wheel Builder + +on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + - cron: "9 9 * * *" + push: + branches: + - maintenance/** + pull_request: + branches: + - main + - maintenance/** + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + get_commit_message: + name: Get commit message + runs-on: ubuntu-latest + if: github.repository == 'scipy/scipy' + outputs: + message: ${{ steps.commit_message.outputs.message }} + steps: + - name: Checkout scipy + uses: actions/checkout@v4.1.1 + # Gets the correct commit message for pull request + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Get commit message + id: commit_message + run: | + set -xe + COMMIT_MSG=$(git log --no-merges -1) + RUN="0" + if [[ "$COMMIT_MSG" == *"[wheel build]"* ]]; then + RUN="1" + fi + echo "message=$RUN" >> $GITHUB_OUTPUT + echo github.ref ${{ github.ref }} + + build_wheels: + name: Wheel, ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }} + ${{ matrix.buildplat[2] }} ${{ matrix.buildplat[3] }} + ${{ matrix.buildplat[4] }} + needs: get_commit_message + if: >- + contains(needs.get_commit_message.outputs.message, '1') || + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' + runs-on: ${{ matrix.buildplat[0] }} + strategy: + # Ensure that a wheel builder finishes even if another fails + fail-fast: false + matrix: + # Github Actions doesn't support pairing matrix values together, let's improvise + # https://github.com/github/feedback/discussions/7835#discussioncomment-1769026 + buildplat: + - [ubuntu-22.04, manylinux, x86_64, "", ""] + - [ubuntu-22.04, musllinux, x86_64, "", ""] + # TODO: build scipy and set up Windows and MacOS + # cibuildwheel does not yet support Mac for free-threaded python + # windows is supported but numpy doesn't build on the image yet + python: [["cp313t", '3.13']] + env: + IS_32_BIT: ${{ matrix.buildplat[2] == 'x86' }} + # upload to staging if it's a push to a maintenance branch and the last + # commit message contains '[wheel build]' + IS_PUSH: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/maintenance') && contains(needs.get_commit_message.outputs.message, '1') }} + IS_SCHEDULE_DISPATCH: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + steps: + - name: Checkout scipy + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: true + + # Used to push the built wheels + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: "3.x" + + - name: Build wheels + uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1 + env: + CIBW_PRERELEASE_PYTHONS: True + CIBW_FREE_THREADED_SUPPORT: True + CIBW_BUILD: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}* + CIBW_ARCHS: ${{ matrix.buildplat[2] }} + # TODO: remove along with installing build deps in + # cibw_before_build.sh when a released cython can build numpy + CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation" + + - uses: actions/upload-artifact@v4 + with: + path: ./wheelhouse/*.whl + name: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }} + ${{ matrix.buildplat[2] }} ${{ matrix.buildplat[3] }} + ${{ matrix.buildplat[4] }} + + - uses: mamba-org/setup-micromamba@422500192359a097648154e8db4e39bdb6c6eed7 + with: + # for installation of anaconda-client, required for upload to + # anaconda.org + # Note that this step is *after* specific pythons have been used to + # build and test the wheel + # for installation of anaconda-client, for upload to anaconda.org + # environment will be activated after creation, and in future bash steps + init-shell: bash + environment-name: upload-env + create-args: >- + anaconda-client + + - name: Upload wheels + if: success() + shell: bash -el {0} + # see https://github.com/marketplace/actions/setup-miniconda for why + # `-el {0}` is required. + env: + SCIPY_STAGING_UPLOAD_TOKEN: ${{ secrets.SCIPY_STAGING_UPLOAD_TOKEN }} + SCIPY_NIGHTLY_UPLOAD_TOKEN: ${{ secrets.SCIPY_NIGHTLY_UPLOAD_TOKEN }} + run: | + conda install -y anaconda-client + source tools/wheels/upload_wheels.sh + set_upload_vars + # For cron jobs (restricted to main branch) or "Run workflow" trigger + # an upload to: + # + # https://anaconda.org/scientific-python-nightly-wheels/scipy + # + # Pushes to a maintenance branch that contain '[wheel build]' will + # cause wheels to be built and uploaded to: + # + # https://anaconda.org/multibuild-wheels-staging/scipy + # + # The tokens were originally generated at anaconda.org + upload_wheels diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml index cce256d207a3..aaf86160aec5 100644 --- a/.github/workflows/issue-labeler.yml +++ b/.github/workflows/issue-labeler.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: # label based on issue title - - uses: github/issue-labeler@v3.3 + - uses: github/issue-labeler@v3.4 if: github.repository == 'scipy/scipy' with: configuration-path: .github/labeler.yml diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2c9789aba166..27676a8253be 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -84,7 +84,7 @@ jobs: echo "timestamp=${NOW}" >> $GITHUB_OUTPUT - name: Setup compiler cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-ccache # Reference: https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows#matching-a-cache-key # NOTE: The caching strategy is modeled in a way that it will always have a unique cache key for each workflow run @@ -308,7 +308,7 @@ jobs: sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev ccache gfortran - name: Caching Python dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache with: path: ~/.cache/pip @@ -321,10 +321,8 @@ jobs: # (it can be put back after matplotlib has made a 2.0-compatible # release on PyPI. python -m pip install --pre --upgrade pytest pytest-cov pytest-xdist mpmath gmpy2 threadpoolctl pooch hypothesis - # TODO: once the scipy_ symbol prefix issue is fixed, install - # scipy-openblas32 from the pre-releases bucket again (see gh-19640) - python -m pip install "scipy-openblas32<=0.3.23.293.2" - # Install numpy last, to ensure we get 2.0.0-dev (avoid possible <2.0 constraints). + python -m pip install -r requirements/openblas.txt + # Install numpy last, to ensure we get nightly (avoid possible <2.0 constraints). python -m pip install --pre --upgrade --timeout=60 -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy - name: Prepare compiler cache @@ -337,7 +335,7 @@ jobs: echo "timestamp=${NOW}" >> $GITHUB_OUTPUT - name: Setup compiler cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-ccache with: path: ${{ steps.prep-ccache.outputs.dir }} @@ -382,20 +380,27 @@ jobs: - name: build + test run: | - set -euo pipefail + set -exuo pipefail docker pull quay.io/pypa/manylinux2014_i686 docker run -v $(pwd):/scipy --platform=linux/i386 quay.io/pypa/manylinux2014_i686 /bin/bash -c "cd /scipy && \ uname -a && \ - basedir=\$(python3.10 tools/openblas_support.py) && \ - cp -r \$basedir/lib/* /usr/local/lib && \ - cp \$basedir/include/* /usr/local/include && \ - export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig && \ python3.10 -m venv test && \ source test/bin/activate && \ python -m pip install doit click rich_click pydevtool meson ninja && \ + python -m pip install -r requirements/openblas.txt && \ + # Ensure that scipy-openblas is picked up by the numpy<1.26 build + cat > \$HOME/.numpy-site.cfg < + needs.get_commit_message.outputs.message == 1 + && (github.repository == 'scipy/scipy' || github.repository == '') + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: recursive + fetch-tags: true + # TODO: replace with setup-python when there is support + - uses: deadsnakes/action@6c8b9b82fe0b4344f4b98f2775fcc395df45e494 # v3.1.0 + with: + python-version: '3.13-dev' + nogil: true + - name: Install Ubuntu dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev ccache gfortran + # TODO: remove pip pre-release install after Python 3.13 release + - name: Install pre-release pip + run: | + pip install -U --pre pip + # TODO: remove cython nightly install when cython does a release + - name: Install nightly Cython + run: | + pip install git+https://github.com/cython/cython + - name: Install nightly NumPy + run: | + pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy + - name: Install Python dependencies + run: | + pip install git+https://github.com/serge-sans-paille/pythran + pip install ninja meson-python pybind11 click rich_click pydevtool + pip install --pre --upgrade pytest pytest-xdist gmpy2 threadpoolctl pooch hypothesis + pip install -r requirements/openblas.txt + - name: Build and run tests + env: + PYTHON_GIL: 0 + # TODO: For some reason the Meson installation path points to + # python3/site-packages as opposed to python3.13/site-packages, + # then the dev.py scripts do not work as expected. + run: | + # python dev.py build --with-scipy-openblas + # python dev.py --no-build test -j2 --mode full + python -c "import scipy_openblas32; print(scipy_openblas32.get_pkg_config())" > scipy-openblas.pc + PKG_CONFIG_PATH="$PWD" pip install . -vv --no-build-isolation + pushd $RUNNER_TEMP + PYTHON_GIL=0 python -m pytest --pyargs scipy -n2 --durations=10 + ################################################################################# + clang-17-build-only: + # Purpose is to check for warnings in builds with latest clang. + # We do not run the test suite here. + name: Clang-17 build-only (-Werror) + needs: get_commit_message + if: > + needs.get_commit_message.outputs.message == 1 + && (github.repository == 'scipy/scipy' || github.repository == '') + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4.1.1 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup system dependencies + run: | + sudo apt-get -y update + wget https://apt.llvm.org/llvm.sh + chmod u+x llvm.sh + sudo ./llvm.sh 17 + sudo apt install -y libopenblas-dev liblapack-dev + + - name: Setup Python build deps + run: | + pip install -r requirements/build.txt + pip install build + + - name: Build wheel, check for compiler warnings + run: | + # specify which compilers to use using environment variables + CC=clang-17 CXX=clang++-17 FC=gfortran python -m build -wnx -Csetup-args=--werror diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 5c7ae992f287..a27be838b703 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -57,7 +57,7 @@ jobs: echo "timestamp=${NOW}" >> $GITHUB_OUTPUT - name: Setup compiler cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-ccache # Reference: https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows#matching-a-cache-key # NOTE: The caching strategy is modeled in a way that it will always have @@ -92,7 +92,7 @@ jobs: shell: bash - name: Cache conda - uses: actions/cache@v3 + uses: actions/cache@v4 env: # Increase this value to reset cache if environment.yml has not changed CACHE_NUMBER: 1 @@ -151,12 +151,12 @@ jobs: python-version: ["3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -170,14 +170,13 @@ jobs: ln -s $GFORTRAN_LOC gfortran export PATH=$PWD:$PATH - # make sure we have openblas and gfortran dylibs - bash tools/wheels/cibw_before_build_macos.sh $PWD + # Ensure we have gfortran dylib GFORTRAN_LIB=$(dirname `gfortran --print-file-name libgfortran.dylib`) - export DYLD_LIBRARY_PATH=$GFORTRAN_LIB:/opt/arm64-builds/lib - export PKG_CONFIG_PATH=/opt/arm64-builds/lib/pkgconfig + export DYLD_LIBRARY_PATH=$GFORTRAN_LIB pip install click doit pydevtool rich_click meson cython pythran pybind11 ninja numpy - python dev.py build + pip install -r requirements/openblas.txt + python dev.py build --with-scipy-openblas pip install pooch pytest hypothesis python dev.py -n test @@ -202,7 +201,7 @@ jobs: submodules: recursive - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/musllinux.yml b/.github/workflows/musllinux.yml index 8137d9aff623..4158d74d79eb 100644 --- a/.github/workflows/musllinux.yml +++ b/.github/workflows/musllinux.yml @@ -72,6 +72,7 @@ jobs: # python -m pip install --upgrade --pre -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy python -m pip install meson ninja pybind11 pythran pytest hypothesis python -m pip install click rich_click doit pydevtool pooch + python -m pip install -r requirements/openblas.txt chmod +x tools/wheels/cibw_before_build_linux.sh tools/wheels/cibw_before_build_linux.sh --nightly . @@ -82,4 +83,5 @@ jobs: cd $RUNNER_TEMP source test_env/bin/activate cd $GITHUB_WORKSPACE + export PKG_CONFIG_PATH=$PWD python dev.py test diff --git a/.github/workflows/pull-request-labeler.yml b/.github/workflows/pull-request-labeler.yml index 687bd74a628c..27c81c234728 100644 --- a/.github/workflows/pull-request-labeler.yml +++ b/.github/workflows/pull-request-labeler.yml @@ -20,7 +20,7 @@ jobs: repo-token: "${{ secrets.GITHUB_TOKEN }}" configuration-path: ".github/label-globs.yml" # label based on PR title - - uses: github/issue-labeler@v3.3 + - uses: github/issue-labeler@v3.4 if: github.repository == 'scipy/scipy' with: configuration-path: .github/labeler.yml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 331f4e08f099..691c0a3963c8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -115,6 +115,15 @@ jobs: echo "c:\rtools40\ucrt64\bin;" >> $env:GITHUB_PATH if: ${{ runner.os == 'Windows' && env.IS_32_BIT == 'false' }} + - name: windows - set PKG_CONFIG_PATH + run: | + $env:CIBW = "${{ github.workspace }}" + # It seems somewhere in the env passing, `\` is not + # passed through, so convert it to '/' + $env:CIBW=$env:CIBW.replace("\","/") + echo "CIBW_ENVIRONMENT_WINDOWS=PKG_CONFIG_PATH=$env:CIBW" >> $env:GITHUB_ENV + if: ${{ runner.os == 'Windows' }} + - name: Setup macOS if: startsWith( matrix.buildplat[0], 'macos-' ) run: | @@ -126,12 +135,6 @@ jobs: echo "PATH=$PATH" >> "$GITHUB_ENV" LIB_PATH=$(dirname $(gfortran --print-file-name libgfortran.dylib)) fi - # Add libraries installed by cibw_before_build_macos.sh to path - if [[ ${{ matrix.buildplat[2] }} == 'arm64' ]]; then - LIB_PATH=$LIB_PATH:/opt/arm64-builds/lib - else - LIB_PATH=$LIB_PATH:/usr/local/lib - fi if [[ ${{ matrix.buildplat[4] }} == '10.9' ]]; then # Newest version of Xcode that supports macOS 10.9 XCODE_VER='13.4.1' @@ -144,19 +147,24 @@ jobs: # installed in cibw_before_build_macos.sh sudo xcode-select -s /Applications/Xcode_${XCODE_VER}.app CIBW="MACOSX_DEPLOYMENT_TARGET=${{ matrix.buildplat[4] }}\ - LD_LIBRARY_PATH=$LIB_PATH:$LD_LIBRARY_PATH\ SDKROOT=$(xcrun --sdk macosx --show-sdk-path)\ - PIP_PRE=1\ - PIP_NO_BUILD_ISOLATION=false\ - PKG_CONFIG_PATH=$LIB_PATH/pkgconfig\ - PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" + PKG_CONFIG_PATH=${{ github.workspace }}" echo "CIBW_ENVIRONMENT_MACOS=$CIBW" >> "$GITHUB_ENV" echo "REPAIR_PATH=$LIB_PATH" >> "$GITHUB_ENV" - GFORTRAN_LIB="\$(dirname \$(gfortran --print-file-name libgfortran.dylib))" - CIBW="DYLD_LIBRARY_PATH=$GFORTRAN_LIB:$LIB_PATH delocate-listdeps {wheel} &&\ - DYLD_LIBRARY_PATH=$GFORTRAN_LIB:$LIB_PATH delocate-wheel --require-archs \ - {delocate_archs} -w {dest_dir} {wheel}" + + PREFIX=DYLD_LIBRARY_PATH="\$(dirname \$(gfortran --print-file-name libgfortran.dylib))" + # remove libgfortran from location used for linking (if any), to + # check wheel has bundled things correctly and all tests pass without + # needing installed gfortran + POSTFIX=" sudo rm -rf /opt/gfortran-darwin-x86_64-native &&\ + sudo rm -rf /usr/local/gfortran/lib" + CIBW="$PREFIX delocate-listdeps -d {wheel} && echo "-----------" &&\ + $PREFIX delocate-wheel -v $EXCLUDE --require-archs \ + {delocate_archs} -w {dest_dir} {wheel} && echo "-----------" &&\ + delocate-listdeps -d {dest_dir}/*.whl && echo "-----------" &&\ + $POSTFIX" + # Rename x86 Accelerate wheel to test on macOS 13 runner if [[ ${{ matrix.buildplat[0] }} == 'macos-13' && ${{ matrix.buildplat[4] }} == '14.0' ]]; then CIBW+=" && mv {dest_dir}/\$(basename {wheel}) \ @@ -169,26 +177,8 @@ jobs: env: CIBW_BUILD: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}* CIBW_ARCHS: ${{ matrix.buildplat[2] }} - CIBW_ENVIRONMENT_PASS_LINUX: RUNNER_OS CIBW_PRERELEASE_PYTHONS: True - # TODO remove the CIBW_BEFORE_BUILD_* lines once there are - # numpy2.0 wheels available on PyPI. Also remove/comment out the - # PIP_NO_BUILD_ISOLATION and PIP_EXTRA_INDEX_URL from CIBW_ENVIRONMENT - # (also for _MACOS and _WINDOWS below) - CIBW_BEFORE_BUILD_LINUX: "pip install numpy>=2.0.0.dev0 meson-python cython pythran pybind11 ninja; bash {project}/tools/wheels/cibw_before_build_linux.sh {project}" - CIBW_BEFORE_BUILD_WINDOWS: "pip install numpy>=2.0.0.dev0 meson-python cython pythran pybind11 ninja && bash {project}/tools/wheels/cibw_before_build_win.sh {project}" - CIBW_BEFORE_BUILD_MACOS: "pip install numpy>=2.0.0.dev0 meson-python cython pythran pybind11 ninja; bash {project}/tools/wheels/cibw_before_build_macos.sh {project}" - # Allow pip to find install nightly wheels if necessary - # Setting PIP_NO_BUILD_ISOLATION=false makes pip use build-isolation. - CIBW_ENVIRONMENT: "PIP_NO_BUILD_ISOLATION=false PIP_PRE=1 PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" - - CIBW_ENVIRONMENT_WINDOWS: > - PKG_CONFIG_PATH=c:/opt/64/lib/pkgconfig - PIP_PRE=1 - PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - PIP_NO_BUILD_ISOLATION=false - - name: Rename after test (macOS x86 Accelerate only) # Rename x86 Accelerate wheel back so it targets macOS >= 14 if: matrix.buildplat[0] == 'macos-13' && matrix.buildplat[4] == '14.0' diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f055d4861273..bd27819346bc 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -49,7 +49,8 @@ jobs: - name: pip-packages run: | - pip install numpy cython pybind11 pythran meson ninja pytest pytest-xdist pytest-timeout pytest-fail-slow pooch rich_click click doit pydevtool hypothesis "scipy-openblas32<=0.3.23.293.2" + pip install numpy cython pybind11 pythran meson ninja pytest pytest-xdist pytest-timeout pytest-fail-slow pooch rich_click click doit pydevtool hypothesis + python -m pip install -r requirements/openblas.txt - name: Build run: | @@ -57,6 +58,8 @@ jobs: - name: Test run: | + # test runner parallel clashes with OpenBLAS multithreading + $env:OPENBLAS_NUM_THREADS=1 python dev.py test -j2 -- --durations=0 --durations-min=0.25 --fail-slow=1.0 @@ -89,7 +92,8 @@ jobs: run: | # 1.23.5 is currently the oldest numpy usable on cp3.10 according # to pyproject.toml - python -m pip install numpy==1.23.5 cython pybind11 pythran meson-python meson ninja pytest pytest-xdist pytest-timeout pytest-fail-slow pooch rich_click click doit pydevtool hypothesis "scipy-openblas32<=0.3.23.293.2" + python -m pip install numpy==1.23.5 cython pybind11 pythran meson-python meson ninja pytest pytest-xdist pytest-timeout pytest-fail-slow pooch rich_click click doit pydevtool hypothesis + python -m pip install -r requirements/openblas.txt - name: Build run: | @@ -97,6 +101,8 @@ jobs: - name: Test run: | + # test runner parallel clashes with OpenBLAS multithreading + $env:OPENBLAS_NUM_THREADS=1 python dev.py test -j2 --mode full -- --durations=0 --durations-min=1.0 --timeout=60 --fail-slow=5.0 @@ -130,10 +136,10 @@ jobs: - name: Install OpenBLAS shell: bash run: | - # Keep this using the OpenBLAS tarballs for now, as long as we use those for wheel builds set -xe + python -m pip install -r requirements/openblas.txt bash tools/wheels/cibw_before_build_win.sh . - echo "PKG_CONFIG_PATH=c:\opt\64\lib\pkgconfig;" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=${{ github.workspace }}" >> $GITHUB_ENV - name: pip-packages run: | @@ -141,19 +147,23 @@ jobs: python -m pip install --pre --upgrade --timeout=60 -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy - name: Build + shell: bash run: | python -m build --no-isolation -x -Csetup-args="-Duse-pythran=false" # Vendor openblas.dll and the DLL's it depends on into the wheel # Ignore `libsf_error_state.dll` for special function error handling; # it will be loaded using ctypes in scipy/special/__init__.py. - $env:wheel_name=Get-ChildItem -Path dist/* -Include *.whl - delvewheel repair --add-path c:\opt\openblas\openblas_dll --no-dll libsf_error_state.dll -w dist $env:wheel_name + wheel_name=$(ls dist/*.whl) + openblas_dir=$(python -c"import scipy_openblas32 as sop; print(sop.get_lib_dir())") + delvewheel repair --add-path $openblas_dir --no-dll libsf_error_state.dll -w wheelhouse $wheel_name - python -m pip install $env:wheel_name + python -m pip install wheelhouse/* - name: Test run: | cd $RUNNER_TEMP # run full test suite + # test runner parallel clashes with OpenBLAS multithreading + $env:OPENBLAS_NUM_THREADS=1 pytest --pyargs scipy diff --git a/.gitignore b/.gitignore index 547eff51a193..5f9c22fbfca9 100644 --- a/.gitignore +++ b/.gitignore @@ -138,6 +138,7 @@ Thumbs.db doc/frontpage/build doc/source/reference/generated **/.ipynb_checkpoints +doc/source/_contents # Things specific to this project # ################################### diff --git a/.mailmap b/.mailmap index cfd90864871a..095139b4620e 100644 --- a/.mailmap +++ b/.mailmap @@ -80,6 +80,7 @@ Arno Onken Arno Onken Arthur Volant Arthur <37664438+V0lantis@users.noreply.github.com> Ashwin Pathak ashwinpathak20 Ashwin Pathak ashwinpathak20nov1996 +Ataf Fazledin Ahamed fazledyn Atsushi Sakai Atsushi Sakai Aviv Yaish Aviv Balint Pato balopat @@ -235,6 +236,7 @@ Ilhan Polat ilayn Irvin Probst I--P Irwin Zaid izaid Jacob Carey Jacob Carey +Jacob Ogle jacobogle Jacob Vanderplas Jake VanderPlas Jacob Vanderplas Jake Vanderplas Jacob Vanderplas Jake Vanderplas @@ -310,6 +312,7 @@ Kai Striega kai Kai Striega kai-striega Kai Striega Kai Striega Kat Huang kat +Kenji S Emerson Sparrow Kentaro Yamamoto <38549987+yamaken1343@users.noreply.github.com> yamaken <38549987+yamaken1343@users.noreply.github.com> Kevin Richard Green kevinrichardgreen Klaus Sembritzki klaus @@ -375,6 +378,7 @@ Michael Hirsch michael Michael Hirsch Michael James Bedford Michael Michael Marien michaelmarien +Miguel A. Batalla mabatalla Mikhail Pak mp4096 Milad Sadeghi DM ELNS <57490926+EverLookNeverSee@users.noreply.github.com> Muhammad Firmansyah Kasim mfkasim91 @@ -479,6 +483,7 @@ Stefan Peterson Stefan Peterson Stefan van der Walt Stefan van der Walt Stefan van der Walt Steve Richardson arichar6 +Steven Adams <166521727+hugehope@users.noreply.github.com> hugehope <166521727+hugehope@users.noreply.github.com> Sturla Molden sturlamolden Sturla Molden Sturla Molden Sturla Molden unknown diff --git a/LICENSES_bundled.txt b/LICENSES_bundled.txt index 4828b64e30aa..e12ec9c16e4b 100644 --- a/LICENSES_bundled.txt +++ b/LICENSES_bundled.txt @@ -286,3 +286,8 @@ Name: Tempita Files: scipy/_build_utils/tempita/* License: MIT For details, see scipy/_build_utils/tempita/LICENCE.txt + +Name: mdspan +Files: scipy/special/special/third_party/kokkos/mdspan.hpp +License: Apache License v2.0 with LLVM Exceptions + For details, see scipy/special/special/third_party/kokkos/mdspan.hpp \ No newline at end of file diff --git a/README.rst b/README.rst index 7d5cc292de48..01ec8a053f9b 100644 --- a/README.rst +++ b/README.rst @@ -66,8 +66,9 @@ Writing code isn’t the only way to contribute to SciPy. You can also: - write grant proposals and help with other fundraising efforts If you’re unsure where to start or how your skills fit in, reach out! You can -ask on the mailing list or here, on GitHub, by leaving a -comment on a relevant issue that is already open. +ask on the `forum `__ +or here, on GitHub, by leaving a comment on a relevant issue that is already +open. If you are new to contributing to open source, `this guide `__ helps explain why, what, diff --git a/benchmarks/benchmarks/sparse_linalg_spsolve_triangular.py b/benchmarks/benchmarks/sparse_linalg_spsolve_triangular.py new file mode 100644 index 000000000000..be5feb66388c --- /dev/null +++ b/benchmarks/benchmarks/sparse_linalg_spsolve_triangular.py @@ -0,0 +1,47 @@ +""" +Check the speed of the sparse triangular solve function. +""" +import numpy as np +from numpy.testing import assert_equal + +from .common import Benchmark, safe_import + +with safe_import(): + from scipy import sparse + from scipy.sparse.linalg import spsolve, spsolve_triangular + +def _create_sparse_poisson1d(n): + # Make Gilbert Strang's favorite matrix + # http://www-math.mit.edu/~gs/PIX/cupcakematrix.jpg + # and take the lower triangular half + P1d = sparse.diags([[-1]*(n-1), [2]*n, [-1]*(n-1)], [-1, 0, 1]) + assert_equal(P1d.shape, (n, n)) + return P1d + + +def _create_sparse_poisson2d_half(n): + P1d = _create_sparse_poisson1d(n) + P2d = sparse.kronsum(P1d, P1d) + assert_equal(P2d.shape, (n*n, n*n)) + return sparse.tril(P2d).tocsr() + + +class Bench(Benchmark): + params = [ + [100,1000], + ["spsolve", "spsolve_triangular"], + ] + param_names = ['(n,n)',"method"] + + def setup(self, n, method): + self.b = np.ones(n*n) + self.P_sparse = _create_sparse_poisson2d_half(n) + + def time_solve(self, n, method): + if method == "spsolve": + spsolve(self.P_sparse, self.b) + elif method == "spsolve_triangular": + spsolve_triangular(self.P_sparse, self.b) + else: + raise NotImplementedError() + diff --git a/ci/cirrus_wheels.yml b/ci/cirrus_wheels.yml index ed7683f66daf..1dc44e153433 100644 --- a/ci/cirrus_wheels.yml +++ b/ci/cirrus_wheels.yml @@ -36,6 +36,7 @@ cirrus_wheels_linux_aarch64_task: PIP_PRE=1 PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple PIP_NO_BUILD_ISOLATION=false + PKG_CONFIG_PATH=/project build_script: | apt install -y python3-venv python-is-python3 diff --git a/dev.py b/dev.py index c4d957e96cf4..3cd5b7afb7e5 100644 --- a/dev.py +++ b/dev.py @@ -498,7 +498,7 @@ def setup_build(cls, dirs, args): elif args.with_scipy_openblas: cls.configure_scipy_openblas() env['PKG_CONFIG_PATH'] = os.pathsep.join([ - os.path.join(os.getcwd(), '.openblas'), + os.getcwd(), env.get('PKG_CONFIG_PATH', '') ]) @@ -607,13 +607,12 @@ def install_project(cls, dirs, args): @classmethod def configure_scipy_openblas(self, blas_variant='32'): - """Create .openblas/scipy-openblas.pc and scipy/_distributor_init_local.py + """Create scipy-openblas.pc and scipy/_distributor_init_local.py Requires a pre-installed scipy-openblas32 wheel from PyPI. """ basedir = os.getcwd() - openblas_dir = os.path.join(basedir, ".openblas") - pkg_config_fname = os.path.join(openblas_dir, "scipy-openblas.pc") + pkg_config_fname = os.path.join(basedir, "scipy-openblas.pc") if os.path.exists(pkg_config_fname): return None @@ -631,9 +630,8 @@ def configure_scipy_openblas(self, blas_variant='32'): with open(local, "w", encoding="utf8") as fid: fid.write(f"import {module_name}\n") - os.makedirs(openblas_dir, exist_ok=True) with open(pkg_config_fname, "w", encoding="utf8") as fid: - fid.write(openblas.get_pkg_config().replace("\\", "/")) + fid.write(openblas.get_pkg_config()) @classmethod def run(cls, add_path=False, **kwargs): diff --git a/doc/Makefile b/doc/Makefile index 0909ad02df7a..93a166c20dcc 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -108,11 +108,20 @@ upload: # Basic Sphinx generation rules for different formats #------------------------------------------------------------------------------ -html: version-check html-build +html: version-check convert-notebooks html-build html-build: mkdir -p build/html build/doctrees $(SPHINXBUILD) -WT --keep-going $(VERSIONWARNING) -b html $(ALLSPHINXOPTS) build/html $(FILES) +convert-notebooks: + mkdir -p source/_contents + for file in source/tutorial/stats/*.md; do \ + basename=$$(basename $$file .md); \ + output_name="source/_contents/$${basename}.ipynb"; \ + $(PYTHON) -m jupytext --output "$$output_name" $$file; \ + done + + coverage: build version-check mkdir -p build/coverage build/doctrees $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) build/coverage $(FILES) diff --git a/doc/source/_static/scipy.css b/doc/source/_static/scipy.css index a597689b29ce..b924c7cc0a24 100644 --- a/doc/source/_static/scipy.css +++ b/doc/source/_static/scipy.css @@ -78,3 +78,22 @@ div.admonition>.admonition-title { gap: 10px; margin-bottom: 20px; } + +/* Wrap long titles in pages */ +h1 { + word-wrap: break-word; +} + +/* Monospace titles for API docs */ +div.empty + section>h1 { + font-family: var(--pst-font-family-monospace); +} + +.prename { + font-family: var(--pst-font-family-monospace); + font-size: var(--pst-font-size-h4); +} + +.sig-prename { + display: none; +} diff --git a/doc/source/_templates/autosummary/attribute.rst b/doc/source/_templates/autosummary/attribute.rst index a176e99affe3..6c334f9d95db 100644 --- a/doc/source/_templates/autosummary/attribute.rst +++ b/doc/source/_templates/autosummary/attribute.rst @@ -1,8 +1,13 @@ :orphan: +.. raw:: html + +
{{ module }}.{{ class }}.
+
+ {{ fullname }} {{ underline }} .. currentmodule:: {{ module }} -.. autoattribute:: {{ objname }} \ No newline at end of file +.. autoattribute:: {{ objname }} diff --git a/doc/source/_templates/autosummary/class.rst b/doc/source/_templates/autosummary/class.rst index de8a20bb0768..e7637016a652 100644 --- a/doc/source/_templates/autosummary/class.rst +++ b/doc/source/_templates/autosummary/class.rst @@ -1,4 +1,9 @@ -{{ fullname }} +.. raw:: html + +
{{ module }}.
+
+ +{{ name }} {{ underline }} .. currentmodule:: {{ module }} diff --git a/doc/source/_templates/autosummary/function.rst b/doc/source/_templates/autosummary/function.rst new file mode 100644 index 000000000000..a0965e8bf1cb --- /dev/null +++ b/doc/source/_templates/autosummary/function.rst @@ -0,0 +1,11 @@ +.. raw:: html + +
{{ module }}.
+
+ +{{ name }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autofunction:: {{ objname }} \ No newline at end of file diff --git a/doc/source/_templates/autosummary/method.rst b/doc/source/_templates/autosummary/method.rst index 6dce2229ed12..1e7e0c286c6f 100644 --- a/doc/source/_templates/autosummary/method.rst +++ b/doc/source/_templates/autosummary/method.rst @@ -1,8 +1,13 @@ :orphan: -{{ fullname }} +.. raw:: html + +
{{ module }}.{{ class }}.
+
+ +{{ name }} {{ underline }} .. currentmodule:: {{ module }} -.. automethod:: {{ objname }} \ No newline at end of file +.. automethod:: {{ objname }} diff --git a/doc/source/_templates/autosummary/ndarray_subclass.rst b/doc/source/_templates/autosummary/ndarray_subclass.rst index 1403e7eceecc..90bbcc2c10c9 100644 --- a/doc/source/_templates/autosummary/ndarray_subclass.rst +++ b/doc/source/_templates/autosummary/ndarray_subclass.rst @@ -1,4 +1,9 @@ -{{ fullname }} +.. raw:: html + +
{{ module }}.
+
+ +{{ name }} {{ underline }} .. currentmodule:: {{ module }} diff --git a/doc/source/_templates/autosummary/property.rst b/doc/source/_templates/autosummary/property.rst index 8c3d07350bde..d08c9de3d705 100644 --- a/doc/source/_templates/autosummary/property.rst +++ b/doc/source/_templates/autosummary/property.rst @@ -1,8 +1,13 @@ :orphan: +.. raw:: html + +
{{ module }}.{{ class }}.
+
+ {{ fullname }} {{ underline }} .. currentmodule:: {{ module }} -.. autoproperty:: {{ objname }} \ No newline at end of file +.. autoproperty:: {{ objname }} diff --git a/doc/source/conf.py b/doc/source/conf.py index ce309446eec2..ba383f566ec7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -8,6 +8,7 @@ from docutils import nodes from docutils.parsers.rst import Directive +from intersphinx_registry import get_intersphinx_mapping import matplotlib import matplotlib.pyplot as plt from numpydoc.docscrape_sphinx import SphinxDocString @@ -101,6 +102,9 @@ # List of directories, relative to source directories, that shouldn't be searched # for source files. exclude_dirs = [] +exclude_patterns = [ # glob-style + "**.ipynb", +] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = False @@ -133,10 +137,6 @@ ("py:class", "v, remove specified key and return the corresponding value."), ] -exclude_patterns = [ # glob-style - -] - # be strict about warnings in our examples, we should write clean code # (exceptions permitted for pedagogical purposes below) warnings.resetwarnings() @@ -273,15 +273,9 @@ # ----------------------------------------------------------------------------- # Intersphinx configuration # ----------------------------------------------------------------------------- -intersphinx_mapping = { - 'python': ('https://docs.python.org/3', None), - 'numpy': ('https://numpy.org/devdocs', None), - 'neps': ('https://numpy.org/neps', None), - 'matplotlib': ('https://matplotlib.org/stable', None), - 'asv': ('https://asv.readthedocs.io/en/stable/', None), - 'statsmodels': ('https://www.statsmodels.org/stable', None), -} - +intersphinx_mapping = get_intersphinx_mapping( + packages={"python", "numpy", "neps", "matplotlib", "asv", "statsmodels", "mpmath"} +) # ----------------------------------------------------------------------------- # Numpy extensions @@ -350,14 +344,7 @@ for key in ( 'interp2d` is deprecated', # Deprecation of scipy.interpolate.interp2d 'scipy.misc', # scipy.misc deprecated in v1.10.0; use scipy.datasets - '`kurtosistest` p-value may be', # intentionally "bad" excample in docstring - 'scipy.signal.daub is deprecated', - 'scipy.signal.qmf is deprecated', - 'scipy.signal.cascade is deprecated', - 'scipy.signal.morlet is deprecated', - 'scipy.signal.morlet2 is deprecated', - 'scipy.signal.ricker is deprecated', - 'scipy.signal.cwt is deprecated', + '`kurtosistest` p-value may be', # intentionally "bad" example in docstring ): warnings.filterwarnings(action='ignore', message='.*' + key + '.*') @@ -397,6 +384,18 @@ nb_execution_mode = "auto" # Ignore notebooks generated by jupyterlite-sphinx for interactive examples. nb_execution_excludepatterns = ["_contents/*.ipynb"] +# Prevent creation of transition syntax when adding footnotes +# See https://github.com/executablebooks/MyST-Parser/issues/352 +myst_footnote_transition = False +myst_enable_extensions = [ + "colon_fence", + "dollarmath", + "substitution", +] +nb_render_markdown_format = "myst" +render_markdown_format = "myst" +# Fix rendering of MathJax objects in Jupyter notebooks +myst_update_mathjax = False #------------------------------------------------------------------------------ # Interactive examples with jupyterlite-sphinx diff --git a/doc/source/dev/api-dev/array_api.rst b/doc/source/dev/api-dev/array_api.rst index f46cf3e92550..326e819c4e35 100644 --- a/doc/source/dev/api-dev/array_api.rst +++ b/doc/source/dev/api-dev/array_api.rst @@ -1,3 +1,5 @@ +.. _dev-arrayapi: + Support for the array API standard ================================== diff --git a/doc/source/dev/contributor/development_workflow.rst b/doc/source/dev/contributor/development_workflow.rst index 3f84bf4b269a..4e7da3421277 100644 --- a/doc/source/dev/contributor/development_workflow.rst +++ b/doc/source/dev/contributor/development_workflow.rst @@ -259,7 +259,7 @@ has a nice help page that outlines the process for `filing pull requests`_. If your changes involve modifications to the API or addition/modification of a function, you should initiate a code review. This involves sending an email to -the `SciPy mailing list`_ with a link to your PR along with a description of +the `SciPy forum`_ with a link to your PR along with a description of and a motivation for your changes. .. _pr-checklist: diff --git a/doc/source/dev/contributor/devpy_test.rst b/doc/source/dev/contributor/devpy_test.rst index 544a9daf954b..d98b9f2f7677 100644 --- a/doc/source/dev/contributor/devpy_test.rst +++ b/doc/source/dev/contributor/devpy_test.rst @@ -87,6 +87,9 @@ Other useful options include: - ``-v`` or ``--verbose``, which activates the verbose option for more detailed output. +- ``-b`` or ``--array-api-backend`` *backend* to include alternative + array backends in array-api-compatible tests. See :ref:`dev-arrayapi` + for details. - ``--coverage`` to generate a test coverage report in ``scipy/build/coverage/index.html``. *Note:* |pytest-cov|_ *must be installed.* diff --git a/doc/source/dev/contributor/rendering_documentation.rst b/doc/source/dev/contributor/rendering_documentation.rst index 4d952e998177..e712c715468c 100644 --- a/doc/source/dev/contributor/rendering_documentation.rst +++ b/doc/source/dev/contributor/rendering_documentation.rst @@ -69,6 +69,31 @@ To render the documentation on your own machine: with ``index.html`` and browse, or you can jump straight to the file you’re interested in. +**Interactive Examples** + +Examples within docstrings can be made interactive using ``jupyterlite-sphinx``. +The buttons for converting examples sections into embedded interactive +notebooks are hidden by default on clean docs builds. To enable interactive +examples after building the documentation locally, edit the +``ignore_patterns`` list in the runtime configuration file ``try_examples.json`` +within ``scipy/doc/build/html/``. The initial version of this file in a clean +documentation build is + +.. code-block:: json + + { + "global_min_height": "400px", + "ignore_patterns": [".*"] + } + +The buttons that turn docstring examples into embedded notebooks will be hidden +for all url paths matching the JavaScript Regex patterns in the +``ignore_patterns`` list. ``[".*"]`` includes a pattern which matches all url +paths. Removing this pattern from the list will enable interactivity for all +examples. See the documentation for the ``jupyterlite-sphinx`` +`TryExamples directive `_ +for more information. + .. note:: - Changes to certain documents do not take effect when Sphinx documentation @@ -84,6 +109,15 @@ To render the documentation on your own machine: This indicates that you're likely picking up the wrong SciPy install, check with ``python -c "import scipy; print(scipy.__file__)"``. + - The interactive examples are not available in CI builds. To see the + interactive examples locally in a JupyterLab environment, you can use + + .. code-block:: bash + + python -m http.server --directory doc/build/html + + The documentation pages should then be available at `http://localhost:8000/`. + .. _rendering-documentation-cloud: Checking Documentation on the Cloud @@ -102,11 +136,11 @@ on the cloud. Adding or editing tutorials as Jupyter notebooks ------------------------------------------------ -Under the ``doc/source/notebooks/`` folder of the SciPy tree you can find a few +Under the ``doc/source/`` folder of the SciPy tree you can find a few documents written in MyST-NB_ format. These files are executable, meaning that their content is executed when the SciPy documentation is built (locally or on CI) and any outputs generated by the execution are rendered in the final HTML -files, which you can see listed in :ref:`the user guide `. +files. If you have a document written in Jupyter notebook format (an ``.ipynb`` file) and would like to submit it as part of the SciPy documentation, there are two diff --git a/doc/source/dev/core-dev/decisions.rst.inc b/doc/source/dev/core-dev/decisions.rst.inc index 78fd941cdf22..7a7c457245d7 100644 --- a/doc/source/dev/core-dev/decisions.rst.inc +++ b/doc/source/dev/core-dev/decisions.rst.inc @@ -11,14 +11,14 @@ Code Any significant decisions on adding (or not adding) new features, breaking backwards compatibility or making other significant changes to the codebase -should be made on the scipy-dev mailing list after a discussion (preferably +should be made on the scipy-dev forum after a discussion (preferably with full consensus). Any non-trivial change (where trivial means a typo, or a one-liner maintenance commit) has to go in through a pull request (PR). It has to be reviewed by another developer. In case review doesn't happen quickly enough and it is important that the PR is merged quickly, the submitter of the PR should send a -message to mailing list saying they intend to merge that PR without review +message to the forum saying they intend to merge that PR without review at time X for reason Y unless someone reviews it before then. Changes and new additions should be tested. Untested code is broken code. @@ -27,4 +27,4 @@ Commit rights ------------- Who gets commit rights is decided by the SciPy Steering Council; changes in -commit rights will then be announced on the scipy-dev mailing list. +commit rights will then be announced on the scipy-dev forum. diff --git a/doc/source/dev/core-dev/deprecations.rst.inc b/doc/source/dev/core-dev/deprecations.rst.inc index 07dbe6b503d7..de146d79335a 100644 --- a/doc/source/dev/core-dev/deprecations.rst.inc +++ b/doc/source/dev/core-dev/deprecations.rst.inc @@ -11,7 +11,7 @@ In general, it's not a good idea to remove something without warning users about that removal first. Therefore, this is what should be done before removing something from the public API: -#. Propose to deprecate the functionality on the scipy-dev mailing list and get +#. Propose to deprecate the functionality on the scipy-dev forum and get agreement that that's OK. #. Add a ``DeprecationWarning`` for it, which states that the functionality was deprecated, and in which release. For Cython APIs, see @@ -27,4 +27,4 @@ when running the test suite so they don't pollute the output. It's possible that there is reason to want to ignore this deprecation policy for a particular deprecation; this can always be discussed on the scipy-dev -mailing list. +forum. diff --git a/doc/source/dev/core-dev/distributing.rst.inc b/doc/source/dev/core-dev/distributing.rst.inc index ba717d3210db..be31b009b5d5 100644 --- a/doc/source/dev/core-dev/distributing.rst.inc +++ b/doc/source/dev/core-dev/distributing.rst.inc @@ -77,7 +77,7 @@ classifiers in ``setup.py``, and mentioned in the release notes for each release. All newly released Python versions will be supported as soon as possible. For the general policy on dropping support for a Python or NumPy version, see :ref:`NEP 29 `. The final decision on dropping support is -always taken on the scipy-dev mailing list. +always taken on the scipy-dev forum. The lowest supported Numpy_ version for a SciPy version is mentioned in the release notes and is encoded in ``pyproject.toml``, ``scipy/__init__.py`` and the @@ -90,7 +90,7 @@ Supported versions of optional dependencies and compilers is documented in :ref:`toolchain-roadmap`. Note that not all versions of optional dependencies that are supported are tested well or at all by SciPy's Continuous Integration setup. Issues regarding this are dealt with as they come up in the -issue tracker or mailing list. +issue tracker or forum. Building binary installers diff --git a/doc/source/dev/core-dev/github.rst.inc b/doc/source/dev/core-dev/github.rst.inc index 9e4f7a81bdf8..6152131d4fd7 100644 --- a/doc/source/dev/core-dev/github.rst.inc +++ b/doc/source/dev/core-dev/github.rst.inc @@ -43,7 +43,7 @@ Dealing with pull requests - When merging contributions, a committer is responsible for ensuring that those meet the requirements outlined in :ref:`Contributing to SciPy `. Also check that new features and backwards compatibility breaks were discussed - on the scipy-dev mailing list. + on the scipy-dev forum. - New code goes in via a pull request (PR). - Merge new code with the green button. In case of merge conflicts, ask the PR submitter to rebase (this may require providing some ``git`` instructions). diff --git a/doc/source/dev/core-dev/licensing.rst.inc b/doc/source/dev/core-dev/licensing.rst.inc index 5d251cc74eda..b44a805a602c 100644 --- a/doc/source/dev/core-dev/licensing.rst.inc +++ b/doc/source/dev/core-dev/licensing.rst.inc @@ -21,7 +21,7 @@ These contributions cannot be accepted for inclusion in SciPy unless the original code author is willing to (re)license their code under the modified BSD (or compatible) license. If the original author agrees, add a comment saying so to the source files and forward the relevant -communication to the scipy-dev mailing list. +communication to the scipy-dev forum. Another common occurrence is for code to be translated or derived from code in R, Octave (both GPL-licensed) or a commercial application. Such code also diff --git a/doc/source/dev/core-dev/releasing.rst.inc b/doc/source/dev/core-dev/releasing.rst.inc index 8ef9e0883355..33e58cf92cdf 100644 --- a/doc/source/dev/core-dev/releasing.rst.inc +++ b/doc/source/dev/core-dev/releasing.rst.inc @@ -238,3 +238,14 @@ and test against their own code) and report issues on Github or Discourse. After the final release is done, port relevant changes to release notes, build scripts, author name mapping in ``tools/authors.py`` and any other changes that were only made on the maintenance branch to main. + +Enable interactive examples by editing the runtime configuration file, +``try_examples.json``, in the root folder of the uploaded documentation on the +release server. One must remove the regular expression pattern ``".*"`` from +the ``ignore_patterns`` list. + +.. code-block:: console + + $ ssh your-username@docs.scipy.org + $ cd /srv/docs_scipy_org/doc/scipy-1.13.1 + $ vim try_examples.json # edit the ignore list to remove: ".*" diff --git a/doc/source/dev/gitwash/git_links.inc b/doc/source/dev/gitwash/git_links.inc index eafb3011e262..a4fb28f5a036 100755 --- a/doc/source/dev/gitwash/git_links.inc +++ b/doc/source/dev/gitwash/git_links.inc @@ -64,5 +64,5 @@ .. _`NumPy mailing list`: https://numpy.org/community/ .. _SciPy: https://www.scipy.org .. _`SciPy github`: https://github.com/scipy/scipy -.. _`SciPy mailing list`: https://mail.python.org/mailman3/lists/scipy-dev.python.org/ +.. _`SciPy forum`: https://discuss.scientific-python.org/c/contributor/scipy .. _`SciPy repository`: https://github.com/scipy/scipy diff --git a/doc/source/dev/governance.rst b/doc/source/dev/governance.rst index ebd93c95050f..a2d7556abc21 100644 --- a/doc/source/dev/governance.rst +++ b/doc/source/dev/governance.rst @@ -28,7 +28,7 @@ documentation, designs, or other work to the Project. Anyone can be a Contributor. Contributors can be affiliated with any legal entity or none. Contributors participate in the project by submitting, reviewing, and discussing GitHub Pull Requests and Issues and participating in open -and public Project discussions on GitHub, mailing lists, and other +and public Project discussions on GitHub, forums, and other channels. The foundation of Project participation is openness and transparency. @@ -146,7 +146,7 @@ active over the last two years. When considering potential Members, the Council will look at candidates with a comprehensive view of their contributions. This will include, but is not limited -to, code, code review, infrastructure work, mailing list and chat participation, +to, code, code review, infrastructure work, forum and chat participation, community help/building, education and outreach, design work, etc. We are deliberately not setting arbitrary quantitative metrics (like “100 commits in this repo”) to avoid encouraging behavior that plays to the metrics rather than @@ -186,10 +186,10 @@ responsible for: the :ref:`scipy-roadmap`) bi-yearly, around mid-April and mid-October. - At the same times of the year, summarizing any relevant organizational updates and issues in the preceding period, and asking for - feedback/suggestions on the mailing list. + feedback/suggestions on the forum. - Ensuring the composition of the Steering Council stays current. - Ensuring matters discussed in private by the Steering Council get - summarized on the mailing list to keep the Community informed. + summarized on the forum to keep the Community informed. - Ensuring other important organizational documents (e.g., Code of Conduct, Fiscal Sponsorship Agreement) stay current after they are added. diff --git a/doc/source/dev/hacking.rst b/doc/source/dev/hacking.rst index 6b233420222c..60075ce5d4cb 100644 --- a/doc/source/dev/hacking.rst +++ b/doc/source/dev/hacking.rst @@ -17,8 +17,7 @@ There are a lot of ways you can contribute: - Reviewing open pull requests - Triaging issues - Working on the `scipy.org`_ website -- Answering questions and participating on the scipy-dev and scipy-user - `mailing lists`_. +- Answering questions and participating on the `forum`_. Contributing new code ===================== @@ -41,14 +40,14 @@ more domain-specific code than SciPy. Now if you have code that you would like to see included in SciPy, how do you go about it? After checking that your code can be distributed in SciPy under a compatible license (see :ref:`license-considerations`), the first step is to -discuss it on the scipy-dev mailing list. All new features, as well as changes to +discuss it on the scipy-dev `forum`_. All new features, as well as changes to existing code, are discussed and decided on there. You can, and probably should already start this discussion before your code is finished. Remember that in order to be added to SciPy your code will need to be reviewed by someone else, so try to find someone willing to review your work while you're at it. -Assuming the outcome of the discussion on the mailing list is positive and you +Assuming the outcome of the discussion on the `forum`_ is positive and you have a function or piece of code that does what you need it to do, what next? Before code is added to SciPy, it at least has to have good documentation, unit tests, benchmarks, and correct code style. @@ -120,13 +119,13 @@ Once you think your code is ready for inclusion in SciPy, you can send a pull request (PR) on Github. We won't go into the details of how to work with git here, this is described well in :ref:`git-development` and on the `Github help pages`_. When you send the PR for a new -feature, be sure to also mention this on the scipy-dev mailing list. This can +feature, be sure to also mention this on the scipy-dev `forum`_. This can prompt interested people to help review your PR. Assuming that you already got positive feedback before on the general idea of your code/feature, the purpose of the code review is to ensure that the code is correct, efficient and meets the requirements outlined above. In many cases, the code review happens relatively quickly, but it's possible that it stalls. If you have addressed -all feedback already given, it's perfectly fine to ask on the mailing list +all feedback already given, it's perfectly fine to ask on the `forum`_ again for review (after a reasonable amount of time, say a couple of weeks, has passed). Once the review is completed, the PR is merged into the "main" branch of SciPy. @@ -134,7 +133,7 @@ branch of SciPy. The above describes the requirements and process for adding code to SciPy. It doesn't yet answer the question though how decisions are made exactly. The basic answer is: decisions are made by consensus, by everyone who chooses to -participate in the discussion on the mailing list. This includes developers, +participate in the discussion on the `forum`_. This includes developers, other users and yourself. Aiming for consensus in the discussion is important -- SciPy is a project by and for the scientific Python community. In those rare cases that agreement cannot be reached, the maintainers of the module @@ -153,7 +152,7 @@ MIT, PSF) then it's OK. Code which is GPL or Apache licensed, has no clear license, requires citation or is free for academic use only can't be included in SciPy. Therefore if you copied existing code with such a license or made a direct translation to Python of it, your code can't be included. -If you're unsure, please ask on the scipy-dev `mailing list `_. +If you're unsure, please ask on the scipy-dev `forum`_. *Why is SciPy under the BSD license and not, say, the GPL?* @@ -183,7 +182,7 @@ The discussion on code style and unit testing above applies equally to bug fixes. It is usually best to start by writing a unit test that shows the problem, i.e. it should pass but doesn't. Once you have that, you can fix the code so that the test does pass. That should be enough to send a PR for this -issue. Unlike when adding new code, discussing this on the mailing list may +issue. Unlike when adding new code, discussing this on the `forum`_ may not be necessary - if the old behavior of the code is clearly incorrect, no one will object to having it fixed. It may be necessary to add some warning or deprecation message for the changed behavior. This should be part of the @@ -236,7 +235,7 @@ thoughts in a comment) allows prioritizing maintenance work and finding related issues easily when working on an existing function or subpackage. To read more about issue triage, see :ref:`triaging`. -Participating in discussions on the scipy-user and scipy-dev `mailing lists`_ is +Participating in discussions on the scipy-user and scipy-dev `forum`_ is a contribution in itself. Everyone who writes to those lists with a problem or an idea would like to get responses, and writing such responses makes the project and community function better and appear more welcoming. @@ -296,7 +295,7 @@ improvements, and submit your first PR! .. _Pytest: https://pytest.org/ -.. _mailing lists: https://scipy.org/community/#scipy-mailing-list +.. _forum: https://discuss.scientific-python.org/c/contributor/scipy .. _Spyder: https://www.spyder-ide.org/ diff --git a/doc/source/dev/missing-bits.rst b/doc/source/dev/missing-bits.rst index 903311066b51..a8d5b2483411 100644 --- a/doc/source/dev/missing-bits.rst +++ b/doc/source/dev/missing-bits.rst @@ -82,7 +82,7 @@ is simple enough to fully document all attributes immediately below its name. Some return classes are sufficiently complex to deserve their own rendered documentation. This is fairly standard if the return class is public, but return classes should only be public if 1) they are intended to be imported by -end-users and 2) if they have been approved by the mailing list. For complex, +end-users and 2) if they have been approved by the forum. For complex, private return classes, please see how `~scipy.stats.binomtest` summarizes `~scipy.stats._result_classes.BinomTestResult` and links to its documentation, and note that ``BinomTestResult`` cannot be imported from `~scipy.stats`. diff --git a/doc/source/dev/roadmap-detailed.rst b/doc/source/dev/roadmap-detailed.rst index f528872497b1..173e6f7511cb 100644 --- a/doc/source/dev/roadmap-detailed.rst +++ b/doc/source/dev/roadmap-detailed.rst @@ -18,9 +18,9 @@ going and where help is needed most. General ------- -This roadmap will be evolving together with SciPy. Updates can be submitted as -pull requests. For large or disruptive changes you may want to discuss -those first on the scipy-dev mailing list. +This roadmap will be evolving together with SciPy. Updates can be submitted as +pull requests. For large or disruptive changes you may want to discuss +those first on the scipy-dev forum. API changes @@ -316,11 +316,6 @@ stable conversions to and from other filter representations. SOS filters could be considered as the default filtering method for ltisys objects, for their numerical stability. -*Wavelets*: what's there now doesn't make much sense. Continuous wavelets -only at the moment - decide whether to completely rewrite or remove them. -Discrete wavelet transforms are out of scope (PyWavelets does a good job -for those). - sparse `````` diff --git a/doc/source/dev/toolchain.rst b/doc/source/dev/toolchain.rst index a7e15e53dcce..436f7f7b153a 100644 --- a/doc/source/dev/toolchain.rst +++ b/doc/source/dev/toolchain.rst @@ -467,17 +467,18 @@ asv (airspeed velocity) Recent https://asv.readthedocs.io/ Building the Documentation -------------------------- -==================== ================================================= - Tool Version -==================== ================================================= -Sphinx Whatever recent versions work. >= 5.0. -PyData Sphinx theme Whatever recent versions work. >= 0.15.2. -Sphinx-Design Whatever recent versions work. >= 0.4.0. -numpydoc Whatever recent versions work. >= 1.5.0. -matplotlib Generally suggest >= 3.5. -MyST-NB Whatever recent versions work. >= 0.17.1 -jupyterlite-sphinx Whatever recent versions work. >= 0.12.0 -==================== ================================================= +============================ ================================================= + Tool Version +============================ ================================================= +Sphinx Whatever recent versions work. >= 5.0. +PyData Sphinx theme Whatever recent versions work. >= 0.15.2. +Sphinx-Design Whatever recent versions work. >= 0.4.0. +numpydoc Whatever recent versions work. >= 1.5.0. +matplotlib Generally suggest >= 3.5. +MyST-NB Whatever recent versions work. >= 0.17.1 +jupyterlite-sphinx Whatever recent versions work. >= 0.13.1 +jupyterlite-pyodide-kernel Whatever recent versions work. >= 0.1.0 +============================ ================================================= .. note:: diff --git a/doc/source/index.rst b/doc/source/index.rst index fda5aa05c79a..3dde480d9dcf 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -17,7 +17,7 @@ SciPy documentation `Source Repository `__ | `Issues & Ideas `__ | `Q&A Support `__ | -`Mailing List `__ +`Forum `__ **SciPy** (pronounced "Sigh Pie") is an open-source software for mathematics, science, and engineering. diff --git a/doc/source/release.rst b/doc/source/release.rst index 1bde64f973b2..a14c7a9eba96 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -8,6 +8,7 @@ see the `commit logs `_. .. toctree:: :maxdepth: 1 + release/1.15.0-notes release/1.14.0-notes release/1.13.1-notes release/1.13.0-notes diff --git a/doc/source/release/1.14.0-notes.rst b/doc/source/release/1.14.0-notes.rst index e46b4b172162..0d6b0c661fe9 100644 --- a/doc/source/release/1.14.0-notes.rst +++ b/doc/source/release/1.14.0-notes.rst @@ -6,7 +6,7 @@ SciPy 1.14.0 Release Notes .. contents:: -SciPy 1.14.0 is the culmination of X months of hard work. It contains +SciPy 1.14.0 is the culmination of 3 months of hard work. It contains many new features, numerous bug-fixes, improved test coverage and better documentation. There have been a number of deprecations and API changes in this release, which are documented below. All users are encouraged to @@ -17,7 +17,7 @@ run your code with ``python -Wd`` and check for ``DeprecationWarning`` s). Our development attention will now shift to bug-fix releases on the 1.14.x branch, and on adding new features on the main branch. -This release requires Python 3.10+ and NumPy 1.22.4 or greater. +This release requires Python 3.10+ and NumPy 1.23.5 or greater. For running on PyPy, PyPy3 6.0+ is required. @@ -25,96 +25,342 @@ For running on PyPy, PyPy3 6.0+ is required. ************************** Highlights of this release ************************** - +- SciPy now supports the new Accelerate library introduced in macOS 13.3, and + has wheels built against Accelerate for macOS >=14 resulting in significant + performance improvements for many linear algebra operations. +- A new method, ``cobyqa``, has been added to `scipy.optimize.minimize` - this + is an interface for COBYQA (Constrained Optimization BY Quadratic + Approximations), a derivative-free optimization solver, designed to + supersede COBYLA, developed by the Department of Applied Mathematics, The + Hong Kong Polytechnic University. +- `scipy.sparse.linalg.spsolve_triangular` is now more than an order of + magnitude faster in many cases. ************ New features ************ -`scipy.cluster` improvements -============================ - - -`scipy.interpolate` improvements -================================ +`scipy.fft` improvements +======================== +- A new function, `scipy.fft.prev_fast_len`, has been added. This function + finds the largest composite of FFT radices that is less than the target + length. It is useful for discarding a minimal number of samples before FFT. +`scipy.io` improvements +======================= +- ``wavfile`` now supports reading and writing of ``wav`` files in the RF64 + format, allowing files greater than 4 GB in size to be handled. -`scipy.linalg` improvements -=========================== - +`scipy.constants` improvements +============================== +- Experimental support for the array API standard has been added. -`scipy.ndimage` improvements -============================ +`scipy.interpolate` improvements +================================ +- `scipy.interpolate.Akima1DInterpolator` now supports extrapolation via the + ``extrapolate`` argument. `scipy.optimize` improvements ============================= +- `scipy.optimize.HessianUpdateStrategy` now also accepts square arrays for + ``init_scale``. +- A new method, ``cobyqa``, has been added to `scipy.optimize.minimize` - this + is an interface for COBYQA (Constrained Optimization BY Quadratic + Approximations), a derivative-free optimization solver, designed to + supersede COBYLA, developed by the Department of Applied Mathematics, The + Hong Kong Polytechnic University. +- There are some performance improvements in + `scipy.optimize.differential_evolution`. +- `scipy.optimize.approx_fprime` now has linear space complexity. `scipy.signal` improvements =========================== +- `scipy.signal.minimum_phase` has a new argument ``half``, allowing the + provision of a filter of the same length as the linear-phase FIR filter + coefficients and with the same magnitude spectrum. `scipy.sparse` improvements =========================== - +- A special case has been added to handle multiplying a ``dia_array`` by a + scalar, which avoids a potentially costly conversion to CSR format. +- `scipy.sparse.csgraph.yen` has been added, allowing usage of Yen's K-Shortest + Paths algorithm on a directed on undirected graph. +- Addition between DIA-format sparse arrays and matrices is now faster. +- `scipy.sparse.linalg.spsolve_triangular` is now more than an order of + magnitude faster in many cases. `scipy.spatial` improvements ============================ - +- ``Rotation`` supports an alternative "scalar-first" convention of quaternion + component ordering. It is available via the keyword argument ``scalar_first`` + of ``from_quat`` and ``as_quat`` methods. +- Some minor performance improvements for inverting of ``Rotation`` objects. `scipy.special` improvements ============================ +- Added `scipy.special.log_wright_bessel`, for calculation of the logarithm of + Wright's Bessel function. +- The relative error in `scipy.special.hyp2f1` calculations has improved + substantially. +- Improved behavior of ``boxcox``, ``inv_boxcox``, ``boxcox1p``, and + ``inv_boxcox1p`` by preventing premature overflow. `scipy.stats` improvements ========================== - -Hypothesis Tests ----------------- - - -Sample statistics ------------------ - - -Statistical Distributions -------------------------- - - -Other ------ +- A new function `scipy.stats.power` can be used for simulating the power + of a hypothesis test with respect to a specified alternative. +- The Irwin-Hall (AKA Uniform Sum) distribution has been added as + `scipy.stats.irwinhall`. +- Exact p-value calculations of `scipy.stats.mannwhitneyu` are much faster + and use less memory. +- `scipy.stats.pearsonr` now accepts n-D arrays and computes the statistic + along a specified ``axis``. +- `scipy.stats.kstat`, `scipy.stats.kstatvar`, and `scipy.stats.bartlett` + are faster at performing calculations along an axis of a large n-D array. +************************** +Array API Standard Support +************************** +*Experimental* support for array libraries other than NumPy has been added to +existing sub-packages in recent versions of SciPy. Please consider testing +these features by setting an environment variable ``SCIPY_ARRAY_API=1`` and +providing PyTorch, JAX, or CuPy arrays as array arguments. + +As of 1.14.0, there is support for + +- `scipy.cluster` +- `scipy.fft` +- `scipy.constants` +- `scipy.special`: (select functions) + + - `scipy.special.log_ndtr` + - `scipy.special.ndtr` + - `scipy.special.ndtri` + - `scipy.special.erf` + - `scipy.special.erfc` + - `scipy.special.i0` + - `scipy.special.i0e` + - `scipy.special.i1` + - `scipy.special.i1e` + - `scipy.special.gammaln` + - `scipy.special.gammainc` + - `scipy.special.gammaincc` + - `scipy.special.logit` + - `scipy.special.expit` + - `scipy.special.entr` + - `scipy.special.rel_entr` + - `scipy.special.xlogy` + - `scipy.special.chdtrc` + +- `scipy.stats`: (select functions) + + - `scipy.stats.moment` + - `scipy.stats.skew` + - `scipy.stats.kurtosis` + - `scipy.stats.kstat` + - `scipy.stats.kstatvar` + - `scipy.stats.circmean` + - `scipy.stats.circvar` + - `scipy.stats.circstd` + - `scipy.stats.entropy` + - `scipy.stats.variation` + - `scipy.stats.sem` + - `scipy.stats.ttest_1samp` + - `scipy.stats.pearsonr` + - `scipy.stats.chisquare` + - `scipy.stats.skewtest` + - `scipy.stats.kurtosistest` + - `scipy.stats.normaltest` + - `scipy.stats.jarque_bera` + - `scipy.stats.bartlett` + - `scipy.stats.power_divergence` + - `scipy.stats.monte_carlo_test` ******************* Deprecated features ******************* - -`scipy.linalg` deprecations -=========================== - - -`scipy.spatial` deprecations -============================ - +- `scipy.stats.gstd`, `scipy.stats.chisquare`, and + `scipy.stats.power_divergence` have deprecated support for masked array + input. +- `scipy.stats.linregress` has deprecated support for specifying both samples + in one argument; ``x`` and ``y`` are to be provided as separate arguments. +- The ``conjtransp`` method for `scipy.sparse.dok_array` and + `scipy.sparse.dok_matrix` has been deprecated and will be removed in SciPy + 1.16.0. +- The option ``quadrature="trapz"`` in `scipy.integrate.quad_vec` has been + deprecated in favour of ``quadrature="trapezoid"`` and will be removed in + SciPy 1.16.0. +- `scipy.special.comb` has deprecated support for use of ``exact=True`` in + conjunction with non-integral ``N`` and/or ``k``. ****************************** Backwards incompatible changes ****************************** +- Many `scipy.stats` functions now produce a standardized warning message when + an input sample is too small (e.g. zero size). Previously, these functions + may have raised an error, emitted one or more less informative warnings, or + emitted no warnings. In most cases, returned results are unchanged; in almost + all cases the correct result is ``NaN``. + +Expired deprecations +==================== +There is an ongoing effort to follow through on long-standing deprecations. +The following previously deprecated features are affected: + +- Several previously deprecated methods for sparse arrays were removed: + ``asfptype``, ``getrow``, ``getcol``, ``get_shape``, ``getmaxprint``, + ``set_shape``, ``getnnz``, and ``getformat``. Additionally, the ``.A`` and + ``.H`` attributes were removed. +- ``scipy.integrate.{simps,trapz,cumtrapz}`` have been removed in favour of + ``simpson``, ``trapezoid``, and ``cumulative_trapezoid``. +- The ``tol`` argument of ``scipy.sparse.linalg.{bcg,bicstab,cg,cgs,gcrotmk, + mres,lgmres,minres,qmr,tfqmr}`` has been removed in favour of ``rtol``. + Furthermore, the default value of ``atol`` for these functions has changed + to ``0.0``. +- The ``restrt`` argument of `scipy.sparse.linalg.gmres` has been removed in + favour of ``restart``. +- The ``initial_lexsort`` argument of `scipy.stats.kendalltau` has been + removed. +- The ``cond`` and ``rcond`` arguments of `scipy.linalg.pinv` have been + removed. +- The ``even`` argument of `scipy.integrate.simpson` has been removed. +- The ``turbo`` and ``eigvals`` arguments from ``scipy.linalg.{eigh,eigvalsh}`` + have been removed. +- The ``legacy`` argument of `scipy.special.comb` has been removed. +- The ``hz``/``nyq`` argument of ``signal.{firls, firwin, firwin2, remez}`` has + been removed. +- Objects that weren't part of the public interface but were accessible through + deprecated submodules have been removed. +- ``float128``, ``float96``, and object arrays now raise an error in + `scipy.signal.medfilt` and `scipy.signal.order_filter`. +- ``scipy.interpolate.interp2d`` has been replaced by an empty stub (to be + removed completely in the future). +- Coinciding with changes to function signatures (e.g. removal of a deprecated + keyword), we had deprecated positional use of keyword arguments for the + affected functions, which will now raise an error. Affected functions are: + + - ``sparse.linalg.{bicg, bicgstab, cg, cgs, gcrotmk, gmres, lgmres, minres, + qmr, tfqmr}`` + - ``stats.kendalltau`` + - ``linalg.pinv`` + - ``integrate.simpson`` + - ``linalg.{eigh,eigvalsh}`` + - ``special.comb`` + - ``signal.{firls, firwin, firwin2, remez}`` + + ************* Other changes ************* - +- SciPy now uses C17 as the C standard to build with, instead of C99. The C++ + standard remains C++17. +- macOS Accelerate, which got a major upgrade in macOS 13.3, is now supported. + This results in significant performance improvements for linear algebra + operations, as well as smaller binary wheels. +- Cross-compilation should be smoother and QEMU or similar is no longer needed + to run the cross interpreter. +- Experimental array API support for the JAX backend has been added to several + parts of SciPy. ******* Authors ******* +* Name (commits) +* h-vetinari (30) +* Steven Adams (1) + +* Max Aehle (1) + +* Ataf Fazledin Ahamed (2) + +* Trinh Quoc Anh (1) + +* Miguel A. Batalla (7) + +* Tim Beyer (1) + +* Andrea Blengino (1) + +* boatwrong (1) +* Jake Bowhay (47) +* Dietrich Brunn (2) +* Evgeni Burovski (174) +* Tim Butters (7) + +* CJ Carey (5) +* Sean Cheah (46) +* Lucas Colley (72) +* Giuseppe "Peppe" Dilillo (1) + +* DWesl (2) +* Pieter Eendebak (5) +* Kenji S Emerson (1) + +* Jonas Eschle (1) +* fancidev (2) +* Anthony Frazier (1) + +* Ilan Gold (1) + +* Ralf Gommers (122) +* Rohit Goswami (28) +* Ben Greiner (1) + +* Lorenzo Gualniera (1) + +* Matt Haberland (250) +* Shawn Hsu (1) + +* Budjen Jovan (3) + +* Jozsef Kutas (1) +* Eric Larson (3) +* Gregory R. Lee (4) +* Philip Loche (1) + +* Christian Lorentzen (5) +* Sijo Valayakkad Manikandan (2) + +* marinelay (2) + +* Nikolay Mayorov (1) +* Nicholas McKibben (2) +* Melissa Weber Mendonça (6) +* João Mendes (1) + +* Tomiță Militaru (2) + +* Andrew Nelson (32) +* Lysandros Nikolaou (1) +* Nick ODell (5) + +* Jacob Ogle (1) + +* Pearu Peterson (1) +* Matti Picus (4) +* Ilhan Polat (8) +* pwcnorthrop (3) + +* Bharat Raghunathan (1) +* Tom M. Ragonneau (2) + +* Tyler Reddy (47) +* Pamphile Roy (17) +* Atsushi Sakai (9) +* Daniel Schmitz (5) +* Julien Schueller (2) + +* Dan Schult (12) +* Tomer Sery (7) +* Scott Shambaugh (4) +* Tuhin Sharma (1) + +* Sheila-nk (4) +* Skylake (1) + +* Albert Steppi (214) +* Kai Striega (6) +* Zhibing Sun (2) + +* Nimish Telang (1) + +* toofooboo (1) + +* tpl2go (1) + +* Edgar Andrés Margffoy Tuay (44) +* Valerix (1) + +* Christian Veenhuis (1) +* void (2) + +* Warren Weckesser (3) +* Xuefeng Xu (1) +* Rory Yorke (1) +* Xiao Yuan (1) +* Irwin Zaid (35) +* Elmar Zander (1) + +* ਗਗਨਦੀਪ ਸਿੰਘ (Gagandeep Singh) (2) + + +A total of 81 people contributed to this release. +People with a "+" by their names contributed a patch for the first time. +This list of names is automatically generated, and may not be fully complete. @@ -122,9 +368,322 @@ Authors Issues closed for 1.14.0 ************************ +* `#5369 `__: fsolve & root incorrect function-call count +* `#7203 `__: vtk incompatibility with scipy.interpolate (and mvpoly.rbf) +* `#8056 `__: cho_factor and cho_solve don't support (0,0)-shape matrices +* `#8083 `__: special.hyp2f1 returns the wrong values when c-a-b is an integer... +* `#8510 `__: ValueError: failed to create intent(cache|hide)|optional array--... +* `#8856 `__: LinearNDInterpolator not thread safe +* `#9307 `__: feature request: make \`scipy.stats.pearsonr\` accept 2-D arrays +* `#9459 `__: BUG: linalg: lu and decompositions don't support (0, 1) or (0,... +* `#12515 `__: scipy.linalg.pinvh gives incorrect results +* `#14244 `__: ValueError: On entry to DGESDD parameter number 10 had an illegal... +* `#14389 `__: \`linalg.inv\` fails for arrays of shape (0, 0) +* `#14806 `__: ENH: Add the Irwin-Hall (Uniform Sum) and Bates (Uniform Mean)... +* `#15722 `__: DEP: special.comb: deprecate \`exact=True\` for non-integers +* `#16131 `__: BUG: spsolve_triangular is way slower than spsolve +* `#16583 `__: Combining extensions in \`stats._boost\` into one +* `#16748 `__: None of the \`cython_\*\` APIs have any tests using Cython +* `#16926 `__: TEST/BUG: Tolerance violation in test_solvers::test_solve_discrete_are +* `#17084 `__: ENH: Exporting the removed component of detrend() +* `#17559 `__: ENH: _mannwhitneyu.py computation of exact MWU statistics may... +* `#17658 `__: Inconsistent support for empty matrices in linalg +* `#19322 `__: BUG: \`rv_discrete.expect\` fails when duplicate positions +* `#19348 `__: BUG: stats.nct.pdf inconsistent behavior when compared to MATLAB... +* `#19586 `__: BUG: scipy.signal.group_delay not correct for complex coefficients +* `#19598 `__: BUG: Bug in \`scipy.sparse.linalg.svds\` for large sparse matrices... +* `#19649 `__: ENH: as_quat() and from_quat() seams to be reverse x,y,z,w vs... +* `#19734 `__: Build warnings from HiGHS +* `#19872 `__: BUG: error in calculation of p-values in sp.stats.wilcoxon when... +* `#19905 `__: DEP: remove deprecated imports from privatized modules +* `#19918 `__: ENH: Adding COBYQA to \`scipy.optimize\`? +* `#19921 `__: BUG: Inconsistent Output from BenchGlobal Compared to BenchLeastSquares... +* `#19964 `__: MAINT:BLD:special:Overhaul _ufuncs and cython_special machinery +* `#20124 `__: BUG: stats.skewnorm.ppf returns wrong values with moderately... +* `#20128 `__: BUG: \`csr_array(int())\` errors +* `#20208 `__: BUG: Test failures due to \`invalid value encountered in _beta_ppf\`... +* `#20247 `__: ENH: Akima1DInterpolator Extrapolation +* `#20277 `__: Very noisy doc builds after jupyterlite-sphinx integration +* `#20296 `__: CI: jupyterlite-shpinx pin breaks recent doc builds +* `#20324 `__: MAINT, BUG (?): pearsonr statistic return type change +* `#20357 `__: BUG: Memory usage in griddata function in version 1.12 +* `#20377 `__: ENH: sparse: Update str dunder to handle 1D (and 2D better) +* `#20378 `__: ENH: sparse: Update repr dunder to handle 1D (and maybe 2D better) +* `#20385 `__: MAINT: special version hex cleanup +* `#20386 `__: BUG: scipy.stats.kstest returns NaN starting in scipy 1.12 +* `#20388 `__: DOC: Version switcher is not vertically centred on mobile +* `#20394 `__: BUG: unnecessary computations in iirpeak/iirnotch/iircomb filter... +* `#20399 `__: BUG: scipy.special.logsumexp raises ValueError for a zero-size... +* `#20419 `__: BUG: nightly: .special.jv now promotes float32 inputs to float64 +* `#20434 `__: BUG: sparse dia_array changes to csr after multiplication +* `#20455 `__: BUG: signal.iirfilter: overflow for integer input +* `#20458 `__: MAINT: more potential cleanups related to version bumps +* `#20461 `__: DOC: some likely changes to release process docs +* `#20466 `__: BUG: scipy.linalg.bandwidth returns incorrect upper bandwidth +* `#20488 `__: BUG: When given invalid bounds, \`_minimize_neldermead\` raises... +* `#20492 `__: DOC: linalg.solve_discrete_lyapunov: dead reference link +* `#20502 `__: BUG: special.hyp2f1: local test failure +* `#20509 `__: DOC: Clarify behavior of \`sparse.csgraph.dijkstra\` for \`directed=False\` +* `#20523 `__: CI/BLD: Nightly wheel builds failing for macOS x86_64 +* `#20535 `__: BUG: generate_f2py mod is called by the wrong interpreter +* `#20540 `__: BUG: pytest scipy/linalg/tests/test_extending.py fails with Cython... +* `#20551 `__: DOC/DEV: clearly document which code has an active upstream repo +* `#20562 `__: BUG: Invalid default bracket selection in _bracket_minimum. +* `#20564 `__: TST: stats array API failure for test_skew_constant_value[torch]... +* `#20584 `__: BUG: \`optimize.linprog\` fails with \`list\` type \`integrality\`... +* `#20598 `__: ENH: special: add log of wright_bessel +* `#20614 `__: DOC: dual_annealing optimizer does not pass bounds to minimizer... +* `#20618 `__: BUG: scipy 'minimize' with method='trust-constr' with equality... +* `#20620 `__: DOC: Suggested improvement to interp2d transition guide +* `#20641 `__: BUG: stats: Two new XSLOW test failures +* `#20661 `__: MAINT, TST: failure in test_axis_nan_policy_decorated_positional_args... +* `#20662 `__: DOC: Missing blankspace in error message raised by cont2discrete() +* `#20674 `__: DOC: A typo in authors name in signal.ellipap reference +* `#20683 `__: DOC: A typo in ValueError raised by signal.iirdesign +* `#20691 `__: ENH: Reintroduce Apple Accelerate support +* `#20697 `__: BUG: special: algorithmic Error in \`ratevl\` in \`cephes/polevl.h\` +* `#20740 `__: BLD/DEV: special: build warnings +* `#20755 `__: BUG: stats: Two new test failures +* `#20768 `__: BUG: optimize.minimize: garbage collection in \`lbfgs\` +* `#20783 `__: BUG: Build failure on PyPy3.10 7.3.16: \`error: ‘Py_Initialize’... +* `#20797 `__: BUG: special.hyp1f1: broken for complex argument +* `#20802 `__: MAINT, TST: pytest-fail-slow and local concurrent runs/variability ************************ Pull requests for 1.14.0 ************************ - +* `#13534 `__: ENH: Add more initialization methods for HessianUpdateStrategy +* `#15321 `__: ENH: fft: Add \`prev_fast_len\` to complement \`next_fast_len\` +* `#17924 `__: ENH: sparse.linalg: speed up \`spsolve_triangular\` +* `#18926 `__: ENH: Move symiirorder1/2, cspline2d, qspline2d and spline_filter... +* `#19561 `__: ENH: stats.power: add function to simulate hypothesis test power +* `#19627 `__: FIX: correctly compute group_delay for complex-coefficient TFs +* `#19673 `__: DEP: signal: raise error using medfilt and order_filter with... +* `#19706 `__: ENH: Add half=True kwarg to minimum_phase +* `#19816 `__: BLD: Add Accelerate support for macOS 13.3+ +* `#19900 `__: MAINT/TST: fft: remove xp backend skips, test \`fftfreq\` \`device\` +* `#19904 `__: MAINT: remove incidental imports from private modules +* `#19923 `__: ENH: stats.mannwhitneyu: replace exact p-value calculation +* `#19954 `__: MAINT: Translate wright_bessel function to C++ +* `#19960 `__: DOC: Add examples to \`scipy.interpolate.spalde\` +* `#19994 `__: ENH: add cobyqa to scipy.optimize. +* `#20073 `__: ENH: special: fix premature overflow in \`boxcox\` +* `#20079 `__: ENH: io: Read and write wav files of size > 4GB +* `#20085 `__: ENH: array types: add JAX support +* `#20089 `__: ENH: Translate complex valued hyp2f1 to C++ and make improvements +* `#20127 `__: ENH/TST: Refactor refguide-check, take 3 +* `#20137 `__: ENH: stats.pearsonr: add support for \`axis\` argument +* `#20187 `__: ENH: sparse.csgraph: Yen K-shortest paths +* `#20199 `__: DOC/DEV/MAINT: update core-dev guide +* `#20202 `__: DOC: Reorganize contents of stats User Guide section +* `#20255 `__: TST: linalg: reenable gges[float32] tests +* `#20257 `__: BUG: prevent file descriptor leak in \`openblas_support.py\`... +* `#20260 `__: ENH: Begin overhaul of ufunc machinery +* `#20265 `__: ENH: optimize: const qualify Cython array arguments +* `#20269 `__: REL: set version to 1.14.0dev0 +* `#20273 `__: MAINT/DEV: enforce minimum \`ruff\` version +* `#20275 `__: MAINT/DEV: add auto-fix to \`dev.py lint\` +* `#20278 `__: DEP: integrate: remove simps,trapz,cumtrapz +* `#20281 `__: BUG: optimize: correct \`nfev\` values +* `#20283 `__: DEP: sparse: deprecate conjtransp() method for dok_array/matrix... +* `#20284 `__: ENH: stats.pearsonr: add array API support +* `#20289 `__: DOC: Pin Jupyterlite Sphinx to avoid noisy doc builds +* `#20292 `__: ENH: stats.moment: add array API support +* `#20295 `__: BUG: linalg: support empty arrays +* `#20297 `__: BUG: linalg: use SYEV not SYEVR for pinvh +* `#20298 `__: DOC: linalg: mention that eigenvalues_only=True/False may change... +* `#20304 `__: ENH: interpolate: allow Akima extrapolation +* `#20310 `__: MAINT: Pin jupyterlite-sphinx to >=0.13.1 +* `#20315 `__: DOC: add docs on how to debug linear algebra related issues +* `#20317 `__: MAINT/DEV: rename \`skip_if_array_api\` to \`skip_xp_backends\` +* `#20320 `__: ENH: Generalised ufuncs in special +* `#20321 `__: BUG: Fix for scipy.special seterr, geterr, errstate +* `#20325 `__: MAINT: Improve performance of ndimage.binary_erosion +* `#20326 `__: MAINT: Replace usage of np.prod +* `#20328 `__: DOC: fix small typo in odds_ratio +* `#20329 `__: MAINT: update \`array_api_compat\` to v1.5.1 +* `#20331 `__: MAINT: Fix Cythonize bug in optimize with const view +* `#20335 `__: TST: linalg: undo xfails of QZ and DARE +* `#20342 `__: BLD: linalg: fix rebuild dependencies for .pyf.src files +* `#20354 `__: MAINT: unpin pytest for wheels +* `#20355 `__: TST: signal: bump tolerance for new \`signal.group_delay\` test +* `#20356 `__: BLD: update numpy build dependency in pyproject.toml for numpy... +* `#20367 `__: STY: always \`import numpy as np\` +* `#20373 `__: MAINT: drop Python 3.9 and NumPy 1.22.x +* `#20380 `__: MAINT: forward port 1.13.0 relnotes +* `#20382 `__: MAINT: lint: enforce \`numpy as np\` alias +* `#20384 `__: ENH:special:Re-rewrite cdflib in C +* `#20390 `__: MAINT:Translate the entirety of cephes into C++ +* `#20393 `__: MAINT/BLD: Remove \`stats._boost\` and add the distribution related... +* `#20397 `__: ENH: Support scalar-first order of quaternion components in Rotation +* `#20403 `__: ENH: special: add ufuncs for amos +* `#20404 `__: BUG: interpolate: fix high memory usage for 2 classes +* `#20405 `__: BUG: Fix pair of bugs in Amos and Cephes yv which masked each... +* `#20413 `__: MAINT: Vendor npyrandom instead of using static library +* `#20416 `__: ENH: optimize._chandrupatla: allow infinite function value at... +* `#20417 `__: ENH: Make cython_special actual code, not autogenerated +* `#20418 `__: BUG: signal: corrections to \`iir{peak,notch,comb}\` filter gain +* `#20420 `__: DOC: stats: speed up the very slow \`bootstrap\` examples +* `#20421 `__: Added float32 overloads for amos functions +* `#20422 `__: TST: Test cimporting Cython APIs +* `#20424 `__: MAINT:special: Add license to cdflib and remove old pxd file +* `#20425 `__: MAINT: Fix DOI visibility badge in README +* `#20426 `__: DOC: add hints on how to debug linalg issues with gdb +* `#20427 `__: DOC: speed up some examples +* `#20438 `__: ENH: Translate \`sph_harm\` Cython->C++, add \`sph_harm_all\`... +* `#20441 `__: BLD: Install cython_special.pxd +* `#20443 `__: MAINT: sparse: Update EfficiencyWarning message to reflect array/matrix +* `#20445 `__: ENH: sparse: special-case DIA \* scalar +* `#20446 `__: MAINT: remove repetitive word typos +* `#20450 `__: BLD: avoid setting an environment variable in a meson.build file +* `#20453 `__: DOC: special: add examples for pdtrc, pdtri, pdtrik +* `#20454 `__: DOC: Update toolchain roadmap (1/N) +* `#20456 `__: BUG: signal.iirfilter: avoid integer overflow +* `#20457 `__: ENH: Add \`scipy.special._ufuncs._iv_ratio\` +* `#20460 `__: DOC: Remove extra css colors and settings +* `#20462 `__: DOC: update readme with link to new forum +* `#20463 `__: MAINT: Refactor special function ufunc generation and consolidate... +* `#20465 `__: MAINT: special: fix compiler warning for unused variable +* `#20467 `__: MAINT: stats._contains_nan: fix bug when -inf and inf are in... +* `#20468 `__: TST: stats: mark tests slow/xslow +* `#20469 `__: MAINT/CI: Remove doctesting from refguide-check +* `#20477 `__: BLD: ensure all static libraries use hidden visibility +* `#20478 `__: CI/MAINT: Increase minimum required compiler versions to GCC... +* `#20480 `__: CI: fail slow tests +* `#20481 `__: ENH: stats: Add the Irwin-Hall distribution +* `#20482 `__: CI: standardize job names +* `#20483 `__: ENH: special: translate \`sph_bessel\` to C++, refactor \`cyl_bessel\` +* `#20487 `__: TST: adjust other very slow tests +* `#20490 `__: BUG: sparse: raise error for array classes, document/test old... +* `#20494 `__: BUG: _qmc.py::_random_oa_lhs produces correlated samples +* `#20495 `__: BUG: Remove keyword argument from ValueError in SciPy.optimize +* `#20497 `__: DEP: interpolate: replace interp2d by stub +* `#20498 `__: DEP: switch sparse methods to kwarg-only; remove tol/restrt kwargs +* `#20499 `__: DEP: execute sparse array API deprecations +* `#20500 `__: DOC: Update dead reference link in \`Scipy.linalg._solvers.py\`:... +* `#20501 `__: MAINT: optimize._chandrupatla: reduce xatol +* `#20503 `__: MAINT: spatial: Fix type annotation of \`query_ball_point\` +* `#20508 `__: DOC: Fix legacy admonition styling +* `#20510 `__: BLD: Accelerate wheels for macOS 14+ +* `#20511 `__: BUG: Fix raising ValueError on a zero-size array for SciPy.special.logsumexp +* `#20515 `__: BLD: default to C17 rather than C99 +* `#20522 `__: TST: Skip or fix some failing tests on certain macOS builds +* `#20526 `__: BLD: adjust lower bound on Clang/LLVM from 14.0 to 12.0 +* `#20529 `__: MAINT: remove repeated "is" typos +* `#20534 `__: BUG: Fixes incorrect upper_band value for scipy.linalg.bandwidth +* `#20536 `__: CI: Check whether Python.h is included first in a file +* `#20538 `__: TST: _lib: remove redundant test for missing \`stacklevel\` +* `#20541 `__: ENH: stats.skew: add array-API support +* `#20542 `__: BLD: Accelerate builds should not define \`NO_APPEND_FORTRAN\` +* `#20545 `__: ENH: stats.ttest_1samp: add array-API support +* `#20546 `__: DOC: use more correct and inclusive pronouns +* `#20547 `__: DOC: stats.linregress: split stats/mstats documentation +* `#20548 `__: TST: Skip Cython tests for editable installs +* `#20550 `__: DEP: stats: switch kendalltau to kwarg-only, remove initial_lexsort... +* `#20554 `__: DEP: integrate: switch simpson to kwarg-only, remove even kwarg +* `#20556 `__: DOC: release process updates +* `#20559 `__: DOC/DEV: add core-dev page on vendored code +* `#20560 `__: DEP: linalg: remove turbo / eigvals kwargs from linalg.{eigh,eigvalsh}... +* `#20563 `__: BUG: Fix invalid default bracket selection in _bracket_minimum +* `#20565 `__: DEP: linalg: remove cond / rcond kwargs from linalg.pinv and... +* `#20568 `__: DOC: change approx_fprime doctest +* `#20572 `__: MAINT: vendor Tempita in \`scipy/_build_utils\` +* `#20575 `__: TST: stats.skew: assert_equal -> xp_assert_equal as appropriate +* `#20577 `__: DEV: add unicode check to pre-commit-hook +* `#20578 `__: DEP: signal: remove nyq / Hz kwargs in firwin\* and switch to... +* `#20582 `__: MAINT: optimize.isotonic_regression: remove unnecessary copies +* `#20583 `__: TST: stats.rv_continuous.fit: adjust fit XSLOW/XFAIL/skip sets +* `#20585 `__: CI/BLD: use scipy-openblas wheel when building +* `#20588 `__: DEP: special: remove legacy kwarg from special.comb and switch... +* `#20590 `__: Revert "ENH: Use \`highspy\` in \`linprog\`" +* `#20593 `__: ENH: constants: add array api support +* `#20595 `__: ENH: stats.circ___: add array-API support +* `#20597 `__: ENH: stats.skewtest: add array-API support +* `#20600 `__: TYP: update supported Mypy version from 1.0.0 to 1.10.0 +* `#20604 `__: ENH: stats.monte_carlo_test: add array API support +* `#20612 `__: BLD: fix use of non-default interpreter, improve f2py handling +* `#20615 `__: ENH: stats: Implement _isf for burr12 +* `#20616 `__: DOC: integrate: remove references to deprecated and legacy functions +* `#20619 `__: ENH: spatial: serialize concurrent calls to QHull +* `#20621 `__: TYP: add type annotations to \`scipy/_lib/_array_api.py\` +* `#20625 `__: TST: add dtype dependent default rtol to xp_assert_close +* `#20627 `__: MAINT: special: Drop unused function_calls variable in kolmogorov.h +* `#20628 `__: TST: integrate.tanhsinh: make test case XSLOW +* `#20630 `__: ENH: optimize._jacobian: use _differentiate to compute accurate... +* `#20631 `__: ENH: stats.sem: add array-API support +* `#20634 `__: ENH: stats: add array-API support to kstat/kstatvar +* `#20637 `__: MAINT: Fix broken links in \`datasets._fetchers\` module +* `#20640 `__: TST: adjust new array API test, slow tests +* `#20642 `__: TST: stats.ttest_1samp: fix xslow test +* `#20643 `__: MAINT:update boost to fix \`skewnorm.ppf\` +* `#20645 `__: ENH: optimize.approx_fprime: avoid quadratic memory usage +* `#20646 `__: ENH: special: add \`log_wright_bessel\` +* `#20647 `__: ENH: stats.variation: add array-API support +* `#20649 `__: MAINT: sparse: reformat str and repr for sparse arrays, correct... +* `#20651 `__: ENH: stats.kstat/kstatvar: add native support for \`axis\` +* `#20656 `__: ENH: Micro-optimizations for spatial.transform.Rotation methods +* `#20657 `__: MAINT: remove unused variable in special +* `#20658 `__: ENH: stats.kurtosis: add array API support +* `#20663 `__: MAINT: stats.kruskal: fix no-arg behavior w/ SCIPY_ARRAY_API=1 +* `#20664 `__: Fix typo in cont2discrete +* `#20665 `__: trust-constr make origin of error message clearer when there... +* `#20667 `__: ENH: stats.describe: add array API support +* `#20673 `__: ENH: stats.entropy, special.{entr, rel_entr}: add array API support +* `#20675 `__: DOC: Fixed typo in signal.ellipap +* `#20676 `__: MAINT: clarify dual_annealing-minimizer_kwargs docstring. Closes... +* `#20677 `__: TST: test__differential_evolution tweaks for speed +* `#20679 `__: MAINT: special.wright_bessel: add comment about reference text +* `#20684 `__: MAINT: Fix missing whitespace in signal.iirdesign, spacing consistency... +* `#20685 `__: MAINT: Add graceful handling of invalid initial brackets to elementwise... +* `#20689 `__: ENH: optimize._chandrupatla: add array API support +* `#20694 `__: MAINT: stats: make reducing functions emit consistent warning... +* `#20696 `__: MAINT: stats.gstd: return result rather than raising +* `#20698 `__: DEV/BLD: add --with-accelerate flag to \`dev.py build\` +* `#20705 `__: MAINT: Add missing whitespace +* `#20711 `__: MAINT: numpy cleanup version bumps: fixes issue #20458 +* `#20712 `__: ENH/BLD: Add install tags for \`tests\` +* `#20715 `__: ENH: stats.kurtosistest: add array API support +* `#20716 `__: DEP: integrate.quad_vec: deprecate \`quadrature="trapz"\` +* `#20722 `__: ENH: sparse: Speed up \`_add_sparse\` for DIA format +* `#20726 `__: DOC: stats.{circmean, circvar, circstd}: improve accuracy/clarity +* `#20730 `__: BUG: special: fix algorithmic error in \`ratevl\` in \`cephes/polevl.h\` +* `#20732 `__: BUG: interpolate: do not segfault on bad boundary conditions +* `#20736 `__: ENH: stats.normaltest/jarque_bera: add array-API support +* `#20737 `__: TST, MAINT: run optimize array API tests and fix \`chandrupatla\` +* `#20738 `__: DOC: sparse.csgraph.dijkstra: add warning for \`directed=False\`... +* `#20741 `__: MAINT: optimize: another fail_slow exception for COBYQA +* `#20744 `__: MAINT: use PyTorch 2.3 in CI, fix CuPy failures, more type annotations... +* `#20745 `__: BUG: Fix incorrect brackets in cephes hyperg.h +* `#20746 `__: DOC: stats: update formulas given for kstat/kstatvar to reflect... +* `#20748 `__: TST: bump tolerance to address local \`test_axis_nan_policy\`... +* `#20750 `__: ENH: some micro-optimisations for differential_evolution +* `#20751 `__: ENH: stats.bartlett: add native \`axis\` and array API support +* `#20753 `__: ENH: stats.chisquare/power_divergence: add array API support +* `#20756 `__: TST: stats: refactor tests of normality tests +* `#20764 `__: TST: stats.fit: address xslow test failures +* `#20765 `__: MAINT: stats.wilcoxon: make \`method='exact'\` symmetric w/ ties +* `#20769 `__: MAINT: stats: move \`multiscale_graphcorr\` tests to save time +* `#20770 `__: MAINT: optimize: remove circular reference in \`ScalarFunction\` +* `#20775 `__: MAINT: forward port 1.13.1 relnotes +* `#20777 `__: ENH: stats: end-to-end array-API support for normality tests +* `#20778 `__: DOC: signal: Documentation improvements of \`detrend\` function +* `#20780 `__: DEP: special.comb: deprecate \`exact=True\` for non-integer intputs +* `#20781 `__: TST: stats: remove overhead of array_namespace in calls to _get_pvalue +* `#20782 `__: ENH: stats: end-to-end array-API support for NHSTs with chi-squared... +* `#20787 `__: DOC: interpolate: mention default kinds in interp2d transition... +* `#20788 `__: ENH: optimize: improve \`cobyqa\` performance by reducing overhead... +* `#20789 `__: DEP: stats.linregress: deprecate one-arg use +* `#20790 `__: BUG: special: remove redundant \`Py_Initialize\` +* `#20791 `__: TST: optimize: fix failing tests for \`_bracket_minimum\` +* `#20792 `__: BUG: sparse: Fix argmin/max shape diff between axis 0/1. And... +* `#20795 `__: MAINT: fix warnings about \`noexcept\` and \`except \*\` in Cython... +* `#20796 `__: BLD: optimize: silence build warnings coming from HiGHS +* `#20798 `__: MAINT: special: fix numpy initialization, avoid build warnings +* `#20799 `__: DOC: ndimage: improve grayscale morphology docstrings +* `#20804 `__: MAINT: remove pytest-fail-slow from pyproject.toml +* `#20805 `__: BUG: special: Restore missing line of code in the function cchg(). +* `#20807 `__: TST: stats.nbinom: adjust cdf-ppf roundtrip test +* `#20812 `__: DOC: extend "building reproducible binaries" page +* `#20815 `__: DOC: integrate: odeint user functions must not modify y. +* `#20819 `__: REV: revert accidental \`cobyqa\` update in gh-17924 diff --git a/doc/source/release/1.15.0-notes.rst b/doc/source/release/1.15.0-notes.rst new file mode 100644 index 000000000000..5bd01eb81508 --- /dev/null +++ b/doc/source/release/1.15.0-notes.rst @@ -0,0 +1,112 @@ +========================== +SciPy 1.15.0 Release Notes +========================== + +.. note:: SciPy 1.15.0 is not released yet! + +.. contents:: + +SciPy 1.15.0 is the culmination of X months of hard work. It contains +many new features, numerous bug-fixes, improved test coverage and better +documentation. There have been a number of deprecations and API changes +in this release, which are documented below. All users are encouraged to +upgrade to this release, as there are a large number of bug-fixes and +optimizations. Before upgrading, we recommend that users check that +their own code does not use deprecated SciPy functionality (to do so, +run your code with ``python -Wd`` and check for ``DeprecationWarning`` s). +Our development attention will now shift to bug-fix releases on the +1.15.x branch, and on adding new features on the main branch. + +This release requires Python 3.10+ and NumPy 1.23.5 or greater. + + +************************** +Highlights of this release +************************** + + +************ +New features +************ + +`scipy.cluster` improvements +============================ + + +`scipy.interpolate` improvements +================================ + + +`scipy.linalg` improvements +=========================== + + +`scipy.ndimage` improvements +============================ + + +`scipy.optimize` improvements +============================= + + +`scipy.signal` improvements +=========================== + + +`scipy.sparse` improvements +=========================== + + + +`scipy.spatial` improvements +============================ + + +`scipy.special` improvements +============================ + + +`scipy.stats` improvements +========================== + + + +******************* +Deprecated features +******************* + +`scipy.linalg` deprecations +=========================== + + +`scipy.spatial` deprecations +============================ + + + +****************************** +Backwards incompatible changes +****************************** + +************* +Other changes +************* + + + +******* +Authors +******* + + + +************************ +Issues closed for 1.15.0 +************************ + + +************************ +Pull requests for 1.15.0 +************************ + + diff --git a/doc/source/try_examples.json b/doc/source/try_examples.json index f5429194157b..7e1b894774df 100644 --- a/doc/source/try_examples.json +++ b/doc/source/try_examples.json @@ -1,4 +1,4 @@ { - "min_height": "400px", + "global_min_height": "400px", "ignore_patterns": [".*"] } diff --git a/doc/source/tutorial/index.rst b/doc/source/tutorial/index.rst index c20ab5da69c7..d089843ec190 100644 --- a/doc/source/tutorial/index.rst +++ b/doc/source/tutorial/index.rst @@ -68,21 +68,6 @@ Below, you can find the complete user guide organized by subpackages. ndimage io - -.. _executable-tutorials: - -Executable tutorials --------------------- - -Below you can also find tutorials in -`MyST Markdown `_ format. -These can be opened as Jupyter Notebooks with the help of the -`Jupytext `_ extension. - -.. toctree:: - :caption: Executable tutorials - :maxdepth: 1 - .. raw:: latex \addtocontents{toc}{\protect\setcounter{tocdepth}{1}} diff --git a/doc/source/tutorial/interpolate/smoothing_splines.rst b/doc/source/tutorial/interpolate/smoothing_splines.rst index 4e5d6e4a42b7..7bd2f5dad85d 100644 --- a/doc/source/tutorial/interpolate/smoothing_splines.rst +++ b/doc/source/tutorial/interpolate/smoothing_splines.rst @@ -231,9 +231,9 @@ example that follows. Notice that `sproot` may fail to find an obvious solution at the edge of the approximation interval, :math:`x = 0`. If we define the spline on a slightly - larger interval, we recover both roots :math:`x = 0` and :math:`x = 2\pi`: + larger interval, we recover both roots :math:`x = 0` and :math:`x = \pi`: - >>> x = np.linspace(-np.pi/4, 2.*np.pi + np.pi/4, 21) + >>> x = np.linspace(-np.pi/4, np.pi + np.pi/4, 51) >>> y = np.sin(x) >>> tck = interpolate.splrep(x, y, s=0) >>> interpolate.sproot(tck) diff --git a/doc/source/tutorial/stats/sampling.md b/doc/source/tutorial/stats/sampling.md new file mode 100644 index 000000000000..62f614fb7965 --- /dev/null +++ b/doc/source/tutorial/stats/sampling.md @@ -0,0 +1,340 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.1 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +```{eval-rst} +.. jupyterlite:: ../../_contents/sampling.ipynb + :new_tab: True +``` +(non-uniform-random-number-sampling)= +# Universal Non-Uniform Random Number Sampling in SciPy + +SciPy provides an interface to many universal non-uniform random number +generators to sample random variates from a wide variety of univariate +continuous and discrete distributions. Implementations of a fast C library +called [UNU.RAN](http://statmath.wu.ac.at/software/unuran/) are used +for speed and performance. Please look at [UNU.RAN's +documentation](http://statmath.wu.ac.at/software/unuran/doc/unuran.html) +for an in-depth explanation of these methods. It is heavily referred to +for writing this tutorial and the documentation of all the generators. + +## Introduction + +Random variate generation is the small field of research that deals with +algorithms to generate random variates from various distributions. It is +common to assume that a uniform random number generator is available. +This is a program that produces a sequence of independent and identically +distributed continuous U(0,1) random variates (i.e. uniform random variates +on the interval (0,1)). Of course, real-world computers can never generate +ideal random numbers and they cannot produce numbers of arbitrary precision +but state-of-the-art uniform random number generators come close to this +aim. Thus random variate generation deals with the problem of transforming +such a sequence of U(0,1) random numbers into non-uniform random variates. +These methods are universal and work in a black-box fashion. + +Some methods to do that are: + +* The Inversion method: When the inverse $F^{-1}$ of the cumulative + distribution function is known, then random variate generation is easy. + We just generate a uniformly U(0,1) distributed random number U and + return $X = F^{-1}(U)$. As closed form solutions for the inverse + are rarely available, one usually needs to rely on approximations of + the inverse (e.g. {class}`scipy.special.ndtri`, + {class}`scipy.special.stdtrit`). In general, the implementation of special + functions is quite slow compared to the inversion methods in UNU.RAN. +* The Rejection Method: The rejection method, often called + acceptance-rejection method, has been suggested by John von Neumann in + 1951[^1]. It involves computing an upper bound to the PDF (also called the + hat function) and using the inversion method to generate a random + variate, say Y, from this bound. Then a uniform random number can be + drawn between 0 to the value of the upper bound at Y. If this number + is less than the PDF at Y, return the sample otherwise reject it. See + {class}`scipy.stats.sampling.TransformedDensityRejection`. +* The Ratio-of-Uniforms Method: This is a type of acceptance-rejection + method which is uses minimal bounding rectangles to construct the hat + function. See {class}`scipy.stats.sampling.RatioUniforms`. +* Inversion for Discrete Distributions: The difference compared to the + continuous case is that $F$ is now a step-function. To realize + this in a computer, a search algorithm is used, the simplest of which + is *sequential search*. A uniform random number is generated from + U(0, 1) and probabilities are summed until the cumulative probability + exceeds the uniform random number. The index at which this happens is + the required random variate and is returned. + + +More details on these algorithms can be found in the [appendix of the UNU.RAN +user manual](http://statmath.wu.ac.at/software/unuran/doc/unuran.html#RVG). + +When generating random variates of a distribution, two factors are important +to determine the speed of a generator: the setup step and the actual sampling. +Depending on the situation, different generators can be optimal. For example, +if one repeatedly needs to draw large samples from a given distribution with +a fixed shape parameter, a slow setup is acceptable if the sampling is fast. +This is called the fixed parameter case. If one aims to generate samples of +a distribution for different shape parameters (the varying parameter case), +an expensive setup that needs to be repeated for each parameter would lead +to very poor performance. In such a situation, a fast setup is crucial to +achieve good performance. An overview of the setup and sampling speed of the +different methods is shown in the table below. + +(unuran-methods-summary)= + +Methods for continuous distributions | Required Inputs | Optional Inputs | Setup Speed | Sampling Speed +------------------------------------- | --------------- | --------------- | ----------- | -------------- +{class}`~.stats.sampling.TransformedDensityRejection` | pdf, dpdf | none | slow | fast +{class}`scipy.stats.sampling.NumericalInverseHermite` | cdf | pdf, dpdf | (very) slow | (very) fast +{class}`scipy.stats.sampling.NumericalInversePolynomial` | pdf | cdf | (very) slow | (very) fast +{class}`scipy.stats.sampling.SimpleRatioUniforms` | pdf | none | fast | slow + +where + +- pdf: probability density function +- dpdf: derivative of the pdf +- cdf: cumulative distribution function + +To apply the numerical inversion method NumericalInversePolynomial to a large +number of continuous distributions in SciPy with minimal effort, take a look +at {class}`scipy.stats.sampling.FastGeneratorInversion`. + +Methods for discrete distributions | Required Inputs | Optional Inputs | Setup Speed | Sampling Speed +----------------------------------- | --------------- | --------------- | ----------- | -------------- +{class}`scipy.stats.sampling.DiscreteAliasUrn` | pv | pmf | slow | very fast +{class}`scipy.stats.sampling.DiscreteGuideTable` | pv | pmf | slow | very fast + + +where + +- pv: probability vector +- pmf: probability mass function + + +For more details on the generators implemented in UNU.RAN, please refer to [^2] and [^3]. + +## Basic concepts of the Interface + +Every generator needs to be set up before one can start sampling from it. +This can be done by instantiating an object of that class. Most of the +generators take a distribution object as input which contains the implementation +of required methods like PDF, CDF, etc. In addition to the distribution +object, one can also pass parameters used to set up the generator. It is also +possible to truncate the distributions using a `domain` parameter. All +generators need a stream of uniform random numbers that are transformed into +random variates of the given distribution. This is done by passing a `random_state` +parameter with a NumPy BitGenerator as the uniform random number generator. +`random_state` can either be a integer, {class}`numpy.random.Generator`, +or {class}`numpy.random.RandomState`. + +```{warning} + Use of NumPy < 1.19.0 is discouraged as it doesn't have a fast + Cython API for generating uniform random numbers and might be + too slow for practical use. +``` + +All the generators have a common `rvs` method that can be used to draw +samples from the given distribution. + +An example of this interface is shown below: + +```{code-cell} ipython3 +from scipy.stats.sampling import TransformedDensityRejection +from math import exp +import numpy as np + +class StandardNormal: + def pdf(self, x: float) -> float: + # note that the normalization constant isn't required + return exp(-0.5 * x*x) + def dpdf(self, x: float) -> float: + return -x * exp(-0.5 * x*x) + +dist = StandardNormal() +urng = np.random.default_rng() +rng = TransformedDensityRejection(dist, random_state=urng) +``` + +As shown in the example, we first initialize a distribution object that +contains an implementation of the methods required by the generator. In +our case, we use the {class}`~.stats.sampling.TransformedDensityRejection` (TDR) +method which requires a PDF and its derivative w.r.t. `x` (i.e. the variate). + +```{note} + + Note that the methods of the distribution (i.e. `pdf`, `dpdf`, etc) need not + be vectorized. They should accept and return floats. +``` + +```{note} + + One can also pass the SciPy distributions as arguments. However, note that the + object doesn't always have all the information required by some generators + like the derivative of PDF for the TDR method. Relying on SciPy distributions + might also reduce performance due to the vectorization of the methods like + `pdf` and `cdf`. In both cases, one can implement a custom distribution object + that contains all the required methods and that is not vectorized as shown in + the example above. If one wants to apply a numerical inversion method to a + distribution defined in SciPy, please also take a look at + {class}`scipy.stats.sampling.FastGeneratorInversion`. +``` + +In the above example, we have set up an object of the +{class}`~.stats.sampling.TransformedDensityRejection` method to sample from a +standard normal distribution. Now, we can start sampling from our +distribution by calling the `rvs` method: + +```{code-cell} ipython3 +rng.rvs() +``` + +```{code-cell} ipython3 +rng.rvs((5, 3)) +``` + +We can also check that the samples are drawn from the correct distribution +by visualizing the histogram of the samples: + +```{code-cell} ipython3 +--- +mystnb: + image: + alt: This code generates an X-Y plot with the probability distribution function + of X on the Y axis and values of X on the X axis. A red trace showing the true + distribution is a typical normal distribution with tails near zero at the edges + and a smooth peak around the center near 0.4. A blue bar graph of random variates + is shown below the red trace with a distribution similar to the truth, but with + clear imperfections. +--- +import numpy as np +import matplotlib.pyplot as plt +from scipy.stats import norm +from scipy.stats.sampling import TransformedDensityRejection +from math import exp + +class StandardNormal: + def pdf(self, x: float) -> float: + # note that the normalization constant isn't required + return exp(-0.5 * x*x) + def dpdf(self, x: float) -> float: + return -x * exp(-0.5 * x*x) + +dist = StandardNormal() +urng = np.random.default_rng() +rng = TransformedDensityRejection(dist, random_state=urng) +rvs = rng.rvs(size=1000) +x = np.linspace(rvs.min()-0.1, rvs.max()+0.1, num=1000) +fx = norm.pdf(x) +plt.plot(x, fx, 'r-', lw=2, label='true distribution') +plt.hist(rvs, bins=20, density=True, alpha=0.8, label='random variates') +plt.xlabel('x') +plt.ylabel('PDF(x)') +plt.title('Transformed Density Rejection Samples') +plt.legend() +plt.show() +``` + +````{note} + + Please note the difference between the `rvs` method of the distributions + present in {mod}`scipy.stats` and the one provided by these generators. + UNU.RAN generators must be considered independent in a sense that they will + generally produce a different stream of random numbers than the one produced + by the equivalent distribution in {mod}`scipy.stats` for any seed. The + implementation of `rvs` in {class}`scipy.stats.rv_continuous` usually relies + on the NumPy module {mod}`numpy.random` for well-known distributions (e.g., + for the normal distribution, the beta distribution) and transformations of + other distributions (e.g., normal inverse Gaussian + {class}`scipy.stats.norminvgauss` and the lognormal + {class}`scipy.stats.lognorm` distribution). If no specific method is + implemented, {class}`scipy.stats.rv_continuous` defaults to a numerical + inversion method of the CDF that is very slow. As UNU.RAN transforms uniform + random numbers differently than SciPy or NumPy, the resulting stream of RVs is + different even for the same stream of uniform random numbers. For example, the + random number stream of SciPy's {class}`scipy.stats.norm` and UNU.RAN's + {class}`~.stats.sampling.TransformedDensityRejection` would not be the same + even for the same `random_state`: + + ```python + from scipy.stats.sampling import norm, TransformedDensityRejection + from copy import copy + dist = StandardNormal() + urng1 = np.random.default_rng() + urng1_copy = copy(urng1) + rng = TransformedDensityRejection(dist, random_state=urng1) + rng.rvs() + # -1.526829048388144 + norm.rvs(random_state=urng1_copy) + # 1.3194816698862635 + ``` +```` + +We can pass a `domain` parameter to truncate the distribution: + +```{code-cell} ipython3 +rng = TransformedDensityRejection(dist, domain=(-1, 1), random_state=urng) +rng.rvs((5, 3)) +``` + +Invalid and bad arguments are handled either by SciPy or by UNU.RAN. The +latter throws a {class}`~.stats.sampling.UNURANError` that follows a common format: + +``` +UNURANError: [objid: ] : => +``` + +where: + +- `` is the ID of the object given by UNU.RAN +- `` is an error code representing a type of error. +- `` is the reason why the error occurred. +- `` is a short description of the type of error. + +The `` shows what caused the error. This, by itself, should contain +enough information to help debug the error. In addition, `` and +`` can be used to investigate different classes of error in +UNU.RAN. A complete list of all the error codes and their descriptions can be +found in the [Section 8.4 of the UNU.RAN user +manual](http://statmath.wu.ac.at/software/unuran/doc/unuran.html#Errno). + +An example of an error generated by UNU.RAN is shown below: + +``` +UNURANError: [objid: TDR.003] 50 : PDF(x) < 0.! => (generator) (possible) invalid data +``` + +This shows that UNU.RAN failed to initialize an object with ID `TDR.003` +because the PDF was < 0. i.e. negative. This falls under the type +"possible invalid data for the generator" and has error code 50. + +Warnings thrown by UNU.RAN also follow the same format. + +## Generators in {mod}`scipy.stats.sampling` + +```{toctree} +:maxdepth: 1 + +sampling_tdr +sampling_dau +sampling_pinv +sampling_dgt +sampling_hinv +sampling_srou +``` + +## References + +[^1]: Von Neumann, John. "13. various techniques used in connection with + random digits." Appl. Math Ser 12.36-38 (1951): 3. + +[^2]: UNU.RAN User Manual, + +[^3]: Leydold, Josef, Wolfgang Hörmann, and Halis Sak. "An R Interface to + the UNU.RAN Library for Universal Random Variate Generators.", + diff --git a/doc/source/tutorial/stats/sampling.rst b/doc/source/tutorial/stats/sampling.rst deleted file mode 100644 index 1635a980d0e6..000000000000 --- a/doc/source/tutorial/stats/sampling.rst +++ /dev/null @@ -1,317 +0,0 @@ -.. _non-uniform-random-number-sampling: - -===================================================== -Universal Non-Uniform Random Number Sampling in SciPy -===================================================== - -.. currentmodule:: scipy.stats.sampling - -SciPy provides an interface to many universal non-uniform random number -generators to sample random variates from a wide variety of univariate -continuous and discrete distributions. Implementations of a fast C library -called `UNU.RAN `__ are used -for speed and performance. Please look at -`UNU.RAN's documentation `__ -for an in-depth explanation of these methods. It is heavily referred to -for writing this tutorial and the documentation of all the generators. - - -Introduction ------------- - -Random variate generation is the small field of research that deals with -algorithms to generate random variates from various distributions. It is -common to assume that a uniform random number generator is available. -This is a program that produces a sequence of independent and identically -distributed continuous U(0,1) random variates (i.e. uniform random variates -on the interval (0,1)). Of course, real-world computers can never generate -ideal random numbers and they cannot produce numbers of arbitrary precision -but state-of-the-art uniform random number generators come close to this -aim. Thus random variate generation deals with the problem of transforming -such a sequence of U(0,1) random numbers into non-uniform random variates. -These methods are universal and work in a black-box fashion. - -Some methods to do that are: - -* The Inversion method: When the inverse :math:`F^{-1}` of the cumulative - distribution function is known, then random variate generation is easy. - We just generate a uniformly U(0,1) distributed random number U and - return :math:`X = F^{-1}(U)`. As closed form solutions for the inverse - are rarely available, one usually needs to rely on approximations of - the inverse (e.g. :class:`~scipy.special.ndtri`, - :class:`~scipy.special.stdtrit`). In general, the implementation of special - functions is quite slow compared to the inversion methods in UNU.RAN. -* The Rejection Method: The rejection method, often called - acceptance-rejection method, has been suggested by John von Neumann in - 1951 [1]_. It involves computing an upper bound to the PDF (also called the - hat function) and using the inversion method to generate a random - variate, say Y, from this bound. Then a uniform random number can be - drawn between 0 to the value of the upper bound at Y. If this number - is less than the PDF at Y, return the sample otherwise reject it. See - :class:`~TransformedDensityRejection`. -* The Ratio-of-Uniforms Method: This is a type of acceptance-rejection - method which is uses minimal bounding rectangles to construct the hat - function. See `scipy.stats.rvs_ratio_uniforms`. -* Inversion for Discrete Distributions: The difference compared to the - continuous case is that :math:`F` is now a step-function. To realize - this in a computer, a search algorithm is used, the simplest of which - is *sequential search*. A uniform random number is generated from - U(0, 1) and probabilities are summed until the cumulative probability - exceeds the uniform random number. The index at which this happens is - the required random variate and is returned. - - -More details on these algorithms can be found in the `appendix of the UNU.RAN -user manual `__. - -When generating random variates of a distribution, two factors are important -to determine the speed of a generator: the setup step and the actual sampling. -Depending on the situation, different generators can be optimal. For example, -if one repeatedly needs to draw large samples from a given distribution with -a fixed shape parameter, a slow setup is acceptable if the sampling is fast. -This is called the fixed parameter case. If one aims to generate samples of -a distribution for different shape parameters (the varying parameter case), -an expensive setup that needs to be repeated for each parameter would lead -to very poor performance. In such a situation, a fast setup is crucial to -achieve good performance. An overview of the setup and sampling speed of the -different methods is shown in the table below. - -.. _unuran-methods-summary: - -===================================== =============== =============== =========== ============== -Methods for continuous distributions Required Inputs Optional Inputs Setup Speed Sampling Speed -===================================== =============== =============== =========== ============== -:class:`~TransformedDensityRejection` pdf, dpdf none slow fast -:class:`~NumericalInverseHermite` cdf pdf, dpdf (very) slow (very) fast -:class:`~NumericalInversePolynomial` pdf cdf (very) slow (very) fast -:class:`~SimpleRatioUniforms` pdf none fast slow -===================================== =============== =============== =========== ============== - -where - -* pdf: probability density function -* dpdf: derivative of the pdf -* cdf: cumulative distribution function - -To apply the numerical inversion method NumericalInversePolynomial to a large -number of continuous distributions in SciPy with minimal effort, take a look -at `scipy.stats.sampling.FastGeneratorInversion`. - -===================================== =============== =============== =========== ============== -Methods for discrete distributions Required Inputs Optional Inputs Setup Speed Sampling Speed -===================================== =============== =============== =========== ============== -:class:`~DiscreteAliasUrn` pv pmf slow very fast -:class:`~DiscreteGuideTable` pv pmf slow very fast -===================================== =============== =============== =========== ============== - -where - -* pv: probability vector -* pmf: probability mass function - - -For more details on the generators implemented in UNU.RAN, please refer to [2]_ and [3]_. - -Basic concepts of the Interface -------------------------------- - -Every generator needs to be set up before one can start sampling from it. -This can be done by instantiating an object of that class. Most of the -generators take a distribution object as input which contains the implementation -of required methods like PDF, CDF, etc. In addition to the distribution -object, one can also pass parameters used to set up the generator. It is also -possible to truncate the distributions using a ``domain`` parameter. All -generators need a stream of uniform random numbers that are transformed into -random variates of the given distribution. This is done by passing a ``random_state`` -parameter with a NumPy BitGenerator as the uniform random number generator. -``random_state`` can either be a integer, `numpy.random.Generator`, -or `numpy.random.RandomState`. - -.. warning:: Use of NumPy < 1.19.0 is discouraged as it doesn't have a fast - Cython API for generating uniform random numbers and might be - too slow for practical use. - -All the generators have a common ``rvs`` method that can be used to draw -samples from the given distribution. - -An example of this interface is shown below: - - >>> from scipy.stats.sampling import TransformedDensityRejection - >>> from math import exp - >>> - >>> class StandardNormal: - ... def pdf(self, x: float) -> float: - ... # note that the normalization constant isn't required - ... return exp(-0.5 * x*x) - ... def dpdf(self, x: float) -> float: - ... return -x * exp(-0.5 * x*x) - ... - >>> dist = StandardNormal() - >>> - >>> import numpy as np - >>> urng = np.random.default_rng() - >>> rng = TransformedDensityRejection(dist, random_state=urng) - -As shown in the example, we first initialize a distribution object that -contains an implementation of the methods required by the generator. In -our case, we use the :class:`~TransformedDensityRejection` (TDR) method -which requires a PDF and its derivative w.r.t. ``x`` (i.e. the variate). - -.. note:: Note that the methods of the distribution (i.e. ``pdf``, - ``dpdf``, etc) need not be vectorized. They should - accept and return floats. - -.. note:: One can also pass the SciPy distributions as arguments. However, - note that the object doesn't always have all the information - required by some generators like the derivative of PDF for the - TDR method. Relying on SciPy distributions might also reduce - performance due to the vectorization of the methods like - ``pdf`` and ``cdf``. In both cases, one can implement a - custom distribution object that contains all the required - methods and that is not vectorized as shown in the example - above. If one wants to apply a numerical inversion method to - a distribution defined in SciPy, please also take a look at - `scipy.stats.sampling.FastGeneratorInversion`. - -In the above example, we have set up an object of the -:class:`~TransformedDensityRejection` method to sample from a -standard normal distribution. Now, we can start sampling from our -distribution by calling the ``rvs`` method: - - >>> rng.rvs() - -1.526829048388144 - >>> rng.rvs((5, 3)) - array([[ 2.06206883, 0.15205036, 1.11587367], - [-0.30775562, 0.29879802, -0.61858268], - [-1.01049115, 0.78853694, -0.23060766], - [-0.60954752, 0.29071797, -0.57167182], - [ 0.9331694 , -0.95605208, 1.72195199]]) - -We can also check that the samples are drawn from the correct distribution -by visualizing the histogram of the samples: - -.. plot:: - :alt: "This code generates an X-Y plot with the probability distribution function of X on the Y axis and values of X on the X axis. A red trace showing the true distribution is a typical normal distribution with tails near zero at the edges and a smooth peak around the center near 0.4. A blue bar graph of random variates is shown below the red trace with a distribution similar to the truth, but with clear imperfections." - - >>> import matplotlib.pyplot as plt - >>> from scipy.stats import norm - >>> from scipy.stats.sampling import TransformedDensityRejection - >>> from math import exp - >>> - >>> class StandardNormal: - ... def pdf(self, x: float) -> float: - ... # note that the normalization constant isn't required - ... return exp(-0.5 * x*x) - ... def dpdf(self, x: float) -> float: - ... return -x * exp(-0.5 * x*x) - ... - >>> - >>> dist = StandardNormal() - >>> urng = np.random.default_rng() - >>> rng = TransformedDensityRejection(dist, random_state=urng) - >>> rvs = rng.rvs(size=1000) - >>> x = np.linspace(rvs.min()-0.1, rvs.max()+0.1, num=1000) - >>> fx = norm.pdf(x) - >>> plt.plot(x, fx, 'r-', lw=2, label='true distribution') - >>> plt.hist(rvs, bins=20, density=True, alpha=0.8, label='random variates') - >>> plt.xlabel('x') - >>> plt.ylabel('PDF(x)') - >>> plt.title('Transformed Density Rejection Samples') - >>> plt.legend() - >>> plt.show() - -.. note:: Please note the difference between the `rvs` method of the - distributions present in :mod:`scipy.stats` and the one provided - by these generators. UNU.RAN generators must be considered - independent in a sense that they will generally produce a different - stream of random numbers than the one produced by the equivalent - distribution in :mod:`scipy.stats` for any seed. The implementation - of `rvs` in `scipy.stats.rv_continuous` usually relies on the NumPy - module `numpy.random` for well-known distributions (e.g., for the normal - distribution, the beta distribution) and transformations of other - distributions (e.g., normal inverse Gaussian `scipy.stats.norminvgauss` and the - lognormal `scipy.stats.lognorm` distribution). If no specific method is implemented, - `scipy.stats.rv_continuous` defaults to a numerical inversion method of the CDF - that is very slow. As UNU.RAN transforms uniform random numbers - differently than SciPy or NumPy, the resulting stream of RVs is - different even for the same stream of uniform random numbers. For - example, the random number stream of SciPy's ``scipy.stats.norm`` and UNU.RAN's - :class:`~TransformedDensityRejection` would not be the same even for - the same ``random_state``: - - >>> from scipy.stats import norm - >>> from scipy.stats.sampling import TransformedDensityRejection - >>> from copy import copy - >>> dist = StandardNormal() - >>> urng1 = np.random.default_rng() - >>> urng1_copy = copy(urng1) - >>> rng = TransformedDensityRejection(dist, random_state=urng1) - >>> rng.rvs() - -1.526829048388144 - >>> norm.rvs(random_state=urng1_copy) - 1.3194816698862635 - -We can pass a ``domain`` parameter to truncate the distribution: - - >>> rng = TransformedDensityRejection(dist, domain=(-1, 1), random_state=urng) - >>> rng.rvs((5, 3)) - array([[-0.99865691, 0.38104014, 0.31633526], # may vary - [ 0.88433909, -0.45181849, 0.78574461], - [ 0.3337244 , 0.12924307, 0.40499404], - [-0.51865761, 0.43252222, -0.6514866 ], - [-0.82666174, 0.71525582, 0.49006743]]) - -Invalid and bad arguments are handled either by SciPy or by UNU.RAN. The -latter throws a :class:`~UNURANError` that follows a common format: - -``UNURANError: [objid: ] : => `` - -where: - -* ```` is the ID of the object given by UNU.RAN -* ```` is an error code representing a type of error. -* ```` is the reason why the error occurred. -* ```` is a short description of the type of error. - -The ```` shows what caused the error. This, by itself, should contain -enough information to help debug the error. In addition, ```` and -```` can be used to investigate different classes of error in -UNU.RAN. A complete list of all the error codes and their descriptions can be -found in the `Section 8.4 of the UNU.RAN user manual -`__. - -An example of an error generated by UNU.RAN is shown below: - -``UNURANError: [objid: TDR.003] 50 : PDF(x) < 0.! => (generator) (possible) invalid data`` - -This shows that UNU.RAN failed to initialize an object with ID ``TDR.003`` -because the PDF was < 0. i.e. negative. This falls under the type -"possible invalid data for the generator" and has error code 50. - -Warnings thrown by UNU.RAN also follow the same format. - - -Generators in :mod:`scipy.stats.sampling` ------------------------------------------ -.. toctree:: - :maxdepth: 1 - - sampling_tdr - sampling_dau - sampling_pinv - sampling_dgt - sampling_hinv - sampling_srou - - -References -~~~~~~~~~~ - -.. [1] Von Neumann, John. "13. various techniques used in connection with - random digits." Appl. Math Ser 12.36-38 (1951): 3. - -.. [2] UNU.RAN User Manual, https://statmath.wu.ac.at/unuran/doc/unuran.html - -.. [3] Leydold, Josef, Wolfgang Hörmann, and Halis Sak. "An R Interface to - the UNU.RAN Library for Universal Random Variate Generators.", - https://cran.r-project.org/web/packages/Runuran/vignettes/Runuran.pdf diff --git a/environment.yml b/environment.yml index 84c31d1c4a22..0d3b7eeaad0e 100644 --- a/environment.yml +++ b/environment.yml @@ -36,6 +36,7 @@ dependencies: - types-psutil # For building docs - sphinx + - intersphinx-registry - numpydoc - ipython - setuptools<67.3 # avoid pkg_resources deprecation warnings from MPL/scikit-umfpack @@ -45,6 +46,7 @@ dependencies: - jupytext - myst-nb - jupyterlite-sphinx>=0.13.1 + - jupyterlite-pyodide-kernel # Some optional test dependencies - mpmath - gmpy2 diff --git a/meson.build b/meson.build index ed92bdde6d36..1d57ef8bb709 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project( # Note that the git commit hash cannot be added dynamically here (it is added # in the dynamically generated and installed `scipy/version.py` though - see # tools/version_utils.py - version: '1.14.0.dev0', + version: '1.15.0.dev0', license: 'BSD-3', meson_version: '>= 1.1.0', default_options: [ diff --git a/pyproject.toml b/pyproject.toml index 5dacd58974ea..d4e47db6f403 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ requires = [ [project] name = "scipy" -version = "1.14.0.dev0" +version = "1.15.0.dev0" # TODO: add `license-files` once PEP 639 is accepted (see meson-python#88) # at that point, no longer include them in `py3.install_sources()` license = { file = "LICENSE.txt" } @@ -87,6 +87,7 @@ test = [ ] doc = [ "sphinx>=5.0.0", + "intersphinx_registry", "pydata-sphinx-theme>=0.15.2", "sphinx-design>=0.4.0", "matplotlib>=3.5", @@ -122,7 +123,6 @@ dodoFile = "dev.py" [tool.cibuildwheel] skip = "cp36-* cp37-* cp38-* pp* *_ppc64le *_i686 *_s390x" -build-verbosity = "3" # gmpy2 and scikit-umfpack are usually added for testing. However, there are # currently wheels missing that make the test script fail. test-requires = [ @@ -134,6 +134,7 @@ test-requires = [ "pooch", "hypothesis", ] +before-test = "bash {project}/tools/wheels/cibw_before_test.sh {project}" test-command = "bash {project}/tools/wheels/cibw_test_command.sh {project}" [tool.cibuildwheel.linux] @@ -141,22 +142,22 @@ manylinux-x86_64-image = "manylinux2014" manylinux-aarch64-image = "manylinux2014" before-build = "bash {project}/tools/wheels/cibw_before_build_linux.sh {project}" +[tool.cibuildwheel.linux.environment] +# /project will be the $PWD equivalent inside the docker used to build the wheel +PKG_CONFIG_PATH="/project/" + [tool.cibuildwheel.macos] before-build = "bash {project}/tools/wheels/cibw_before_build_macos.sh {project}" +[tool.cibuildwheel.macos.environment] +PKG_CONFIG_PATH="{project}" + [tool.cibuildwheel.windows] before-build = "bash {project}/tools/wheels/cibw_before_build_win.sh {project}" repair-wheel-command = "bash ./tools/wheels/repair_windows.sh {wheel} {dest_dir}" -[[tool.cibuildwheel.overrides]] -select = "*-win32" - -[[tool.cibuildwheel.overrides]] -select = "*-win_amd64" -# can use pkg-config detection for win_amd64 because the installed rtools -# provide a working pkg-config. -# An alternative is to set CMAKE_PREFIX_PATH="c:/opt/openblas/if_32/32" -# Don't use double backslash for path separators, they don't get passed -# to the build correctly -# environment = { CMAKE_PREFIX_PATH="c:/opt/64" } -environment = { PKG_CONFIG_PATH = "c:/opt/64/lib/pkgconfig" } +[tool.cibuildwheel.windows.environment] +# This does not work because pkg-config does not like backslashes, +PKG_CONFIG_PATH="{project}" +# do this instead (which will override this setting) +# set CIBW_ENVIRONMENT_WINDOWS=PKG_CONFIG_PATH=PWD.replace('\\', '/') diff --git a/requirements/doc.txt b/requirements/doc.txt index fa160cc5d384..0df108072d84 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -1,6 +1,7 @@ # Generated via tools/generate_requirements.py. # Do not edit this file; modify `pyproject.toml` instead and run `python tools/generate_requirements.py`. sphinx>=5.0.0 +intersphinx_registry pydata-sphinx-theme>=0.15.2 sphinx-design>=0.4.0 matplotlib>=3.5 diff --git a/requirements/openblas.txt b/requirements/openblas.txt new file mode 100644 index 000000000000..db99a76438b7 --- /dev/null +++ b/requirements/openblas.txt @@ -0,0 +1 @@ +scipy-openblas32==0.3.27.63.1 diff --git a/scipy/_lib/cobyqa b/scipy/_lib/cobyqa index c516987f947c..7f40b6dd5452 160000 --- a/scipy/_lib/cobyqa +++ b/scipy/_lib/cobyqa @@ -1 +1 @@ -Subproject commit c516987f947ccef9a16869b9633c20f9b8e0f4fe +Subproject commit 7f40b6dd54525d7aa722f9558c186d39d163af94 diff --git a/scipy/_lib/deprecation.py b/scipy/_lib/deprecation.py index 01a1dfa73695..0823c78c579a 100644 --- a/scipy/_lib/deprecation.py +++ b/scipy/_lib/deprecation.py @@ -213,14 +213,10 @@ def inner_f(*args, **kwargs): return f(*args, **kwargs) # extra_args > 0 - args_msg = [ - f"{name}={arg}" - for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:]) - ] - args_msg = ", ".join(args_msg) + args_msg = ", ".join(kwonly_args[:extra_args]) warnings.warn( ( - f"You are passing {args_msg} as a positional argument. " + f"You are passing as positional arguments: {args_msg}. " "Please change your invocation to use keyword arguments. " f"From SciPy {version}, passing these as positional " "arguments will result in an error." diff --git a/scipy/_lib/tests/test__util.py b/scipy/_lib/tests/test__util.py index b1f58acf3dc9..163c0ba201c4 100644 --- a/scipy/_lib/tests/test__util.py +++ b/scipy/_lib/tests/test__util.py @@ -395,7 +395,7 @@ class TestLazywhere: p = strategies.floats(min_value=0, max_value=1) data = strategies.data() - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) @pytest.mark.filterwarnings('ignore::RuntimeWarning') # overflows, etc. @skip_xp_backends('jax.numpy', reasons=["JAX arrays do not support item assignment"]) diff --git a/scipy/_lib/tests/test_import_cycles.py b/scipy/_lib/tests/test_import_cycles.py index 02177fec255e..20822b1aba42 100644 --- a/scipy/_lib/tests/test_import_cycles.py +++ b/scipy/_lib/tests/test_import_cycles.py @@ -8,7 +8,7 @@ # Check that all modules are importable in a new Python process. # This is not necessarily true if there are import cycles present. -@pytest.mark.fail_slow(20) +@pytest.mark.fail_slow(40) @pytest.mark.slow def test_public_modules_importable(): pids = [subprocess.Popen([sys.executable, '-c', f'import {module}']) diff --git a/scipy/_lib/tests/test_warnings.py b/scipy/_lib/tests/test_warnings.py index aad199d4bc10..b76971868f47 100644 --- a/scipy/_lib/tests/test_warnings.py +++ b/scipy/_lib/tests/test_warnings.py @@ -95,7 +95,7 @@ def warning_calls(): return bad_filters, bad_stacklevels -@pytest.mark.fail_slow(20) +@pytest.mark.fail_slow(40) @pytest.mark.slow def test_warning_calls_filters(warning_calls): bad_filters, bad_stacklevels = warning_calls @@ -118,7 +118,9 @@ def test_warning_calls_filters(warning_calls): os.path.join('stats', '_discrete_distns.py'), # gh-14901 os.path.join('stats', '_continuous_distns.py'), os.path.join('stats', '_binned_statistic.py'), # gh-19345 + os.path.join('stats', 'tests', 'test_axis_nan_policy.py'), # gh-20694 os.path.join('_lib', '_util.py'), # gh-19341 + os.path.join('sparse', 'linalg', '_dsolve', 'linsolve.py'), # gh-17924 "conftest.py", ) bad_filters = [item for item in bad_filters if item.split(':')[0] not in diff --git a/scipy/conftest.py b/scipy/conftest.py index 1f17618ec551..c9bd9bf99afb 100644 --- a/scipy/conftest.py +++ b/scipy/conftest.py @@ -267,7 +267,7 @@ def skip_xp_backends(xp, request): # FIXME: populate the dict once @contextmanager - def warnings_errors_and_rng(test): + def warnings_errors_and_rng(test=None): """Temporarily turn (almost) all warnings to errors. Filter out known warnings which we allow. @@ -337,11 +337,11 @@ def warnings_errors_and_rng(test): with _fixed_default_rng(): np.random.seed(None) with warnings.catch_warnings(): - if test.name in known_warnings: + if test and test.name in known_warnings: warnings.filterwarnings('ignore', **known_warnings[test.name]) yield - elif test.name in legit: + elif test and test.name in legit: yield else: warnings.simplefilter('error', Warning) diff --git a/scipy/datasets/tests/test_data.py b/scipy/datasets/tests/test_data.py index d29feb72f55f..6474fd27cc75 100644 --- a/scipy/datasets/tests/test_data.py +++ b/scipy/datasets/tests/test_data.py @@ -35,7 +35,7 @@ def test_download_all(self): yield - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_existence_all(self): assert len(os.listdir(data_dir)) >= len(registry) diff --git a/scipy/integrate/__init__.py b/scipy/integrate/__init__.py index 039234777d35..b452d5fe87a8 100644 --- a/scipy/integrate/__init__.py +++ b/scipy/integrate/__init__.py @@ -17,12 +17,9 @@ tplquad -- General purpose triple integration nquad -- General purpose N-D integration fixed_quad -- Integrate func(x) using Gaussian quadrature of order n - quadrature -- Integrate with given tolerance using Gaussian quadrature - romberg -- Integrate func using Romberg integration newton_cotes -- Weights and error coefficient for Newton-Cotes integration qmc_quad -- N-D integration using Quasi-Monte Carlo quadrature IntegrationWarning -- Warning on issues during integration - AccuracyWarning -- Warning on issues during quadrature integration Integrating functions, given fixed samples ========================================== diff --git a/scipy/integrate/_ivp/bdf.py b/scipy/integrate/_ivp/bdf.py index 5b78060cc3a2..29bd94615192 100644 --- a/scipy/integrate/_ivp/bdf.py +++ b/scipy/integrate/_ivp/bdf.py @@ -204,7 +204,8 @@ def __init__(self, fun, t0, y0, t_bound, max_step=np.inf, self.rtol, self.atol = validate_tol(rtol, atol, self.n) f = self.fun(self.t, self.y) if first_step is None: - self.h_abs = select_initial_step(self.fun, self.t, self.y, f, + self.h_abs = select_initial_step(self.fun, self.t, self.y, + t_bound, max_step, f, self.direction, 1, self.rtol, self.atol) else: diff --git a/scipy/integrate/_ivp/common.py b/scipy/integrate/_ivp/common.py index eccf91f9d5ef..4ff0b7056a0e 100644 --- a/scipy/integrate/_ivp/common.py +++ b/scipy/integrate/_ivp/common.py @@ -65,7 +65,8 @@ def norm(x): return np.linalg.norm(x) / x.size ** 0.5 -def select_initial_step(fun, t0, y0, f0, direction, order, rtol, atol): +def select_initial_step(fun, t0, y0, t_bound, + max_step, f0, direction, order, rtol, atol): """Empirically select a good initial step. The algorithm is described in [1]_. @@ -78,6 +79,11 @@ def select_initial_step(fun, t0, y0, f0, direction, order, rtol, atol): Initial value of the independent variable. y0 : ndarray, shape (n,) Initial value of the dependent variable. + t_bound : float + End-point of integration interval; used to ensure that t0+step<=tbound + and that fun is only evaluated in the interval [t0,tbound] + max_step : float + Maximum allowable step size. f0 : ndarray, shape (n,) Initial value of the derivative, i.e., ``fun(t0, y0)``. direction : float @@ -103,6 +109,10 @@ def select_initial_step(fun, t0, y0, f0, direction, order, rtol, atol): if y0.size == 0: return np.inf + interval_length = abs(t_bound - t0) + if interval_length == 0.0: + return 0.0 + scale = atol + np.abs(y0) * rtol d0 = norm(y0 / scale) d1 = norm(f0 / scale) @@ -110,7 +120,8 @@ def select_initial_step(fun, t0, y0, f0, direction, order, rtol, atol): h0 = 1e-6 else: h0 = 0.01 * d0 / d1 - + # Check t0+h0*direction doesn't take us beyond t_bound + h0 = min(h0, interval_length) y1 = y0 + h0 * direction * f0 f1 = fun(t0 + h0 * direction, y1) d2 = norm((f1 - f0) / scale) / h0 @@ -120,7 +131,7 @@ def select_initial_step(fun, t0, y0, f0, direction, order, rtol, atol): else: h1 = (0.01 / max(d1, d2)) ** (1 / (order + 1)) - return min(100 * h0, h1) + return min(100 * h0, h1, interval_length, max_step) class OdeSolution: diff --git a/scipy/integrate/_ivp/radau.py b/scipy/integrate/_ivp/radau.py index 0d9109e3564e..e13cb0f14c3c 100644 --- a/scipy/integrate/_ivp/radau.py +++ b/scipy/integrate/_ivp/radau.py @@ -305,7 +305,7 @@ def __init__(self, fun, t0, y0, t_bound, max_step=np.inf, # the error. if first_step is None: self.h_abs = select_initial_step( - self.fun, self.t, self.y, self.f, self.direction, + self.fun, self.t, self.y, t_bound, max_step, self.f, self.direction, 3, self.rtol, self.atol) else: self.h_abs = validate_first_step(first_step, t0, t_bound) diff --git a/scipy/integrate/_ivp/rk.py b/scipy/integrate/_ivp/rk.py index b6076f950156..62a5347ffe91 100644 --- a/scipy/integrate/_ivp/rk.py +++ b/scipy/integrate/_ivp/rk.py @@ -94,7 +94,7 @@ def __init__(self, fun, t0, y0, t_bound, max_step=np.inf, self.f = self.fun(self.t, self.y) if first_step is None: self.h_abs = select_initial_step( - self.fun, self.t, self.y, self.f, self.direction, + self.fun, self.t, self.y, t_bound, max_step, self.f, self.direction, self.error_estimator_order, self.rtol, self.atol) else: self.h_abs = validate_first_step(first_step, t0, t_bound) diff --git a/scipy/integrate/_ivp/tests/test_ivp.py b/scipy/integrate/_ivp/tests/test_ivp.py index 2443553f65a9..2c20e2e300b6 100644 --- a/scipy/integrate/_ivp/tests/test_ivp.py +++ b/scipy/integrate/_ivp/tests/test_ivp.py @@ -7,7 +7,7 @@ from scipy.optimize._numdiff import group_columns from scipy.integrate import solve_ivp, RK23, RK45, DOP853, Radau, BDF, LSODA from scipy.integrate import OdeSolution -from scipy.integrate._ivp.common import num_jac +from scipy.integrate._ivp.common import num_jac, select_initial_step from scipy.integrate._ivp.base import ConstantDenseOutput from scipy.sparse import coo_matrix, csc_matrix @@ -260,7 +260,7 @@ def test_integration_complex(): assert np.all(e < 5) -@pytest.mark.fail_slow(2) +@pytest.mark.fail_slow(5) def test_integration_sparse_difference(): n = 200 t_span = [0, 20] @@ -598,7 +598,7 @@ def test_max_step(): solver = method(fun_rational, t_span[0], y0, t_span[1], rtol=rtol, atol=atol, max_step=1e-20) message = solver.step() - + message = solver.step() # First step succeeds but second step fails. assert_equal(solver.status, 'failed') assert_("step size is less" in message) assert_raises(RuntimeError, solver.step) @@ -1128,9 +1128,117 @@ def fun_with_arg(t, y, a): sol = solve_ivp(fun_with_arg, (0, 0.1), [1], args=(-1,)) assert_allclose(sol.y[0, -1], np.exp(-0.1)) + @pytest.mark.parametrize("f0_fill", [np.nan, np.inf]) def test_initial_state_finiteness(f0_fill): # regression test for gh-17846 msg = "All components of the initial state `y0` must be finite." with pytest.raises(ValueError, match=msg): solve_ivp(fun_zero, [0, 10], np.full(3, f0_fill)) + + +@pytest.mark.parametrize('method', ['RK23', 'RK45', 'DOP853', 'Radau', 'BDF']) +def test_zero_interval(method): + # Case where upper and lower limits of integration are the same + # Result of integration should match initial state. + # f[y(t)] = 2y(t) + def f(t, y): + return 2 * y + res = solve_ivp(f, (0.0, 0.0), np.array([1.0]), method=method) + assert res.success + assert_allclose(res.y[0, -1], 1.0) + + +@pytest.mark.parametrize('method', ['RK23', 'RK45', 'DOP853', 'Radau', 'BDF']) +def test_tbound_respected_small_interval(method): + """Regression test for gh-17341""" + SMALL = 1e-4 + + # f[y(t)] = 2y(t) on t in [0,SMALL] + # undefined otherwise + def f(t, y): + if t > SMALL: + raise ValueError("Function was evaluated outside interval") + return 2 * y + res = solve_ivp(f, (0.0, SMALL), np.array([1]), method=method) + assert res.success + + +@pytest.mark.parametrize('method', ['RK23', 'RK45', 'DOP853', 'Radau', 'BDF']) +def test_tbound_respected_larger_interval(method): + """Regression test for gh-8848""" + def V(r): + return -11/r + 10 * r / (0.05 + r**2) + + def func(t, p): + if t < -17 or t > 2: + raise ValueError("Function was evaluated outside interval") + P = p[0] + Q = p[1] + r = np.exp(t) + dPdr = r * Q + dQdr = -2.0 * r * ((-0.2 - V(r)) * P + 1 / r * Q) + return np.array([dPdr, dQdr]) + + result = solve_ivp(func, + (-17, 2), + y0=np.array([1, -11]), + max_step=0.03, + vectorized=False, + t_eval=None, + atol=1e-8, + rtol=1e-5) + assert result.success + + +@pytest.mark.parametrize('method', ['RK23', 'RK45', 'DOP853', 'Radau', 'BDF']) +def test_tbound_respected_oscillator(method): + "Regression test for gh-9198" + def reactions_func(t, y): + if (t > 205): + raise ValueError("Called outside interval") + yprime = np.array([1.73307544e-02, + 6.49376470e-06, + 0.00000000e+00, + 0.00000000e+00]) + return yprime + + def run_sim2(t_end, n_timepoints=10, shortest_delay_line=10000000): + init_state = np.array([134.08298555, 138.82348612, 100., 0.]) + t0 = 100.0 + t1 = 200.0 + return solve_ivp(reactions_func, + (t0, t1), + init_state.copy(), + dense_output=True, + max_step=t1 - t0) + result = run_sim2(1000, 100, 100) + assert result.success + + +def test_inital_maxstep(): + """Verify that select_inital_step respects max_step""" + rtol = 1e-3 + atol = 1e-6 + y0 = np.array([1/3, 2/9]) + for (t0, t_bound) in ((5, 9), (5, 1)): + for method_order in [RK23.error_estimator_order, + RK45.error_estimator_order, + DOP853.error_estimator_order, + 3, #RADAU + 1 #BDF + ]: + step_no_max = select_initial_step(fun_rational, t0, y0, t_bound, + np.inf, + fun_rational(t0,y0), + np.sign(t_bound - t0), + method_order, + rtol, atol) + max_step = step_no_max/2 + step_with_max = select_initial_step(fun_rational, t0, y0, t_bound, + max_step, + fun_rational(t0, y0), + np.sign(t_bound - t0), + method_order, + rtol, atol) + assert_equal(max_step, step_with_max) diff --git a/scipy/integrate/_odepack_py.py b/scipy/integrate/_odepack_py.py index b868c0ced8c2..20993e5bb516 100644 --- a/scipy/integrate/_odepack_py.py +++ b/scipy/integrate/_odepack_py.py @@ -59,6 +59,8 @@ def odeint(func, y0, t, args=(), Dfun=None, col_deriv=0, full_output=0, Computes the derivative of y at t. If the signature is ``callable(t, y, ...)``, then the argument `tfirst` must be set ``True``. + `func` must not modify the data in `y`, as it is a + view of the data used internally by the ODE solver. y0 : array Initial condition on y (can be a vector). t : array @@ -72,6 +74,8 @@ def odeint(func, y0, t, args=(), Dfun=None, col_deriv=0, full_output=0, Gradient (Jacobian) of `func`. If the signature is ``callable(t, y, ...)``, then the argument `tfirst` must be set ``True``. + `Dfun` must not modify the data in `y`, as it is a + view of the data used internally by the ODE solver. col_deriv : bool, optional True if `Dfun` defines derivatives down columns (faster), otherwise `Dfun` should define derivatives across rows. diff --git a/scipy/integrate/_quadrature.py b/scipy/integrate/_quadrature.py index 7fe4ef9424eb..9da7b85329ff 100644 --- a/scipy/integrate/_quadrature.py +++ b/scipy/integrate/_quadrature.py @@ -9,13 +9,12 @@ from scipy.special import roots_legendre from scipy.special import gammaln, logsumexp from scipy._lib._util import _rng_spawn -from scipy._lib.deprecation import _deprecated -__all__ = ['fixed_quad', 'quadrature', 'romberg', 'romb', +__all__ = ['fixed_quad', 'romb', 'trapezoid', 'simpson', 'cumulative_trapezoid', 'newton_cotes', - 'qmc_quad', 'AccuracyWarning', 'cumulative_simpson'] + 'qmc_quad', 'cumulative_simpson'] def trapezoid(y, x=None, dx=1.0, axis=-1): @@ -148,10 +147,6 @@ def trapezoid(y, x=None, dx=1.0, axis=-1): return ret -class AccuracyWarning(Warning): - pass - - if TYPE_CHECKING: # workaround for mypy function attributes see: # https://github.com/python/mypy/issues/2087#issuecomment-462726600 @@ -250,142 +245,6 @@ def fixed_quad(func, a, b, args=(), n=5): return (b-a)/2.0 * np.sum(w*func(y, *args), axis=-1), None -def vectorize1(func, args=(), vec_func=False): - """Vectorize the call to a function. - - This is an internal utility function used by `romberg` and - `quadrature` to create a vectorized version of a function. - - If `vec_func` is True, the function `func` is assumed to take vector - arguments. - - Parameters - ---------- - func : callable - User defined function. - args : tuple, optional - Extra arguments for the function. - vec_func : bool, optional - True if the function func takes vector arguments. - - Returns - ------- - vfunc : callable - A function that will take a vector argument and return the - result. - - """ - if vec_func: - def vfunc(x): - return func(x, *args) - else: - def vfunc(x): - if np.isscalar(x): - return func(x, *args) - x = np.asarray(x) - # call with first point to get output type - y0 = func(x[0], *args) - n = len(x) - dtype = getattr(y0, 'dtype', type(y0)) - output = np.empty((n,), dtype=dtype) - output[0] = y0 - for i in range(1, n): - output[i] = func(x[i], *args) - return output - return vfunc - - -@_deprecated("`scipy.integrate.quadrature` is deprecated as of SciPy 1.12.0" - "and will be removed in SciPy 1.15.0. Please use" - "`scipy.integrate.quad` instead.") -def quadrature(func, a, b, args=(), tol=1.49e-8, rtol=1.49e-8, maxiter=50, - vec_func=True, miniter=1): - """ - Compute a definite integral using fixed-tolerance Gaussian quadrature. - - .. deprecated:: 1.12.0 - - This function is deprecated as of SciPy 1.12.0 and will be removed - in SciPy 1.15.0. Please use `scipy.integrate.quad` instead. - - Integrate `func` from `a` to `b` using Gaussian quadrature - with absolute tolerance `tol`. - - Parameters - ---------- - func : function - A Python function or method to integrate. - a : float - Lower limit of integration. - b : float - Upper limit of integration. - args : tuple, optional - Extra arguments to pass to function. - tol, rtol : float, optional - Iteration stops when error between last two iterates is less than - `tol` OR the relative change is less than `rtol`. - maxiter : int, optional - Maximum order of Gaussian quadrature. - vec_func : bool, optional - True or False if func handles arrays as arguments (is - a "vector" function). Default is True. - miniter : int, optional - Minimum order of Gaussian quadrature. - - Returns - ------- - val : float - Gaussian quadrature approximation (within tolerance) to integral. - err : float - Difference between last two estimates of the integral. - - See Also - -------- - fixed_quad : fixed-order Gaussian quadrature - quad : adaptive quadrature using QUADPACK - dblquad : double integrals - tplquad : triple integrals - romb : integrator for sampled data - simpson : integrator for sampled data - cumulative_trapezoid : cumulative integration for sampled data - - Examples - -------- - >>> from scipy import integrate - >>> import numpy as np - >>> f = lambda x: x**8 - >>> integrate.quadrature(f, 0.0, 1.0) - (0.11111111111111106, 4.163336342344337e-17) - >>> print(1/9.0) # analytical result - 0.1111111111111111 - - >>> integrate.quadrature(np.cos, 0.0, np.pi/2) - (0.9999999999999536, 3.9611425250996035e-11) - >>> np.sin(np.pi/2)-np.sin(0) # analytical result - 1.0 - - """ - if not isinstance(args, tuple): - args = (args,) - vfunc = vectorize1(func, args, vec_func=vec_func) - val = np.inf - err = np.inf - maxiter = max(miniter+1, maxiter) - for n in range(miniter, maxiter+1): - newval = fixed_quad(vfunc, a, b, (), n)[0] - err = abs(newval-val) - val = newval - - if err < tol or err < rtol*abs(val): - break - else: - warnings.warn( - "maxiter (%d) exceeded. Latest difference = %e" % (maxiter, err), - AccuracyWarning, stacklevel=2 - ) - return val, err - - def tupleset(t, i, value): l = list(t) l[i] = value @@ -1064,196 +923,6 @@ def romb(y, dx=1.0, axis=-1, show=False): return R[(k, k)] -# Romberg quadratures for numeric integration. -# -# Written by Scott M. Ransom -# last revision: 14 Nov 98 -# -# Cosmetic changes by Konrad Hinsen -# last revision: 1999-7-21 -# -# Adapted to SciPy by Travis Oliphant -# last revision: Dec 2001 - - -def _difftrap(function, interval, numtraps): - """ - Perform part of the trapezoidal rule to integrate a function. - Assume that we had called difftrap with all lower powers-of-2 - starting with 1. Calling difftrap only returns the summation - of the new ordinates. It does _not_ multiply by the width - of the trapezoids. This must be performed by the caller. - 'function' is the function to evaluate (must accept vector arguments). - 'interval' is a sequence with lower and upper limits - of integration. - 'numtraps' is the number of trapezoids to use (must be a - power-of-2). - """ - if numtraps <= 0: - raise ValueError("numtraps must be > 0 in difftrap().") - elif numtraps == 1: - return 0.5*(function(interval[0])+function(interval[1])) - else: - numtosum = numtraps/2 - h = float(interval[1]-interval[0])/numtosum - lox = interval[0] + 0.5 * h - points = lox + h * np.arange(numtosum) - s = np.sum(function(points), axis=0) - return s - - -def _romberg_diff(b, c, k): - """ - Compute the differences for the Romberg quadrature corrections. - See Forman Acton's "Real Computing Made Real," p 143. - """ - tmp = 4.0**k - return (tmp * c - b)/(tmp - 1.0) - - -def _printresmat(function, interval, resmat): - # Print the Romberg result matrix. - i = j = 0 - print('Romberg integration of', repr(function), end=' ') - print('from', interval) - print('') - print('%6s %9s %9s' % ('Steps', 'StepSize', 'Results')) - for i in range(len(resmat)): - print('%6d %9f' % (2**i, (interval[1]-interval[0])/(2.**i)), end=' ') - for j in range(i+1): - print('%9f' % (resmat[i][j]), end=' ') - print('') - print('') - print('The final result is', resmat[i][j], end=' ') - print('after', 2**(len(resmat)-1)+1, 'function evaluations.') - - -@_deprecated("`scipy.integrate.romberg` is deprecated as of SciPy 1.12.0" - "and will be removed in SciPy 1.15.0. Please use" - "`scipy.integrate.quad` instead.") -def romberg(function, a, b, args=(), tol=1.48e-8, rtol=1.48e-8, show=False, - divmax=10, vec_func=False): - """ - Romberg integration of a callable function or method. - - .. deprecated:: 1.12.0 - - This function is deprecated as of SciPy 1.12.0 and will be removed - in SciPy 1.15.0. Please use `scipy.integrate.quad` instead. - - Returns the integral of `function` (a function of one variable) - over the interval (`a`, `b`). - - If `show` is 1, the triangular array of the intermediate results - will be printed. If `vec_func` is True (default is False), then - `function` is assumed to support vector arguments. - - Parameters - ---------- - function : callable - Function to be integrated. - a : float - Lower limit of integration. - b : float - Upper limit of integration. - - Returns - ------- - results : float - Result of the integration. - - Other Parameters - ---------------- - args : tuple, optional - Extra arguments to pass to function. Each element of `args` will - be passed as a single argument to `func`. Default is to pass no - extra arguments. - tol, rtol : float, optional - The desired absolute and relative tolerances. Defaults are 1.48e-8. - show : bool, optional - Whether to print the results. Default is False. - divmax : int, optional - Maximum order of extrapolation. Default is 10. - vec_func : bool, optional - Whether `func` handles arrays as arguments (i.e., whether it is a - "vector" function). Default is False. - - See Also - -------- - fixed_quad : Fixed-order Gaussian quadrature. - quad : Adaptive quadrature using QUADPACK. - dblquad : Double integrals. - tplquad : Triple integrals. - romb : Integrators for sampled data. - simpson : Integrators for sampled data. - cumulative_trapezoid : Cumulative integration for sampled data. - - References - ---------- - .. [1] 'Romberg's method' https://en.wikipedia.org/wiki/Romberg%27s_method - - Examples - -------- - Integrate a gaussian from 0 to 1 and compare to the error function. - - >>> from scipy import integrate - >>> from scipy.special import erf - >>> import numpy as np - >>> gaussian = lambda x: 1/np.sqrt(np.pi) * np.exp(-x**2) - >>> result = integrate.romberg(gaussian, 0, 1, show=True) - Romberg integration of from [0, 1] - - :: - - Steps StepSize Results - 1 1.000000 0.385872 - 2 0.500000 0.412631 0.421551 - 4 0.250000 0.419184 0.421368 0.421356 - 8 0.125000 0.420810 0.421352 0.421350 0.421350 - 16 0.062500 0.421215 0.421350 0.421350 0.421350 0.421350 - 32 0.031250 0.421317 0.421350 0.421350 0.421350 0.421350 0.421350 - - The final result is 0.421350396475 after 33 function evaluations. - - >>> print("%g %g" % (2*result, erf(1))) - 0.842701 0.842701 - - """ - if np.isinf(a) or np.isinf(b): - raise ValueError("Romberg integration only available " - "for finite limits.") - vfunc = vectorize1(function, args, vec_func=vec_func) - n = 1 - interval = [a, b] - intrange = b - a - ordsum = _difftrap(vfunc, interval, n) - result = intrange * ordsum - resmat = [[result]] - err = np.inf - last_row = resmat[0] - for i in range(1, divmax+1): - n *= 2 - ordsum += _difftrap(vfunc, interval, n) - row = [intrange * ordsum / n] - for k in range(i): - row.append(_romberg_diff(last_row[k], row[k], k+1)) - result = row[i] - lastresult = last_row[i-1] - if show: - resmat.append(row) - err = abs(result - lastresult) - if err < tol or err < rtol * abs(result): - break - last_row = row - else: - warnings.warn( - "divmax (%d) exceeded. Latest difference = %e" % (divmax, err), - AccuracyWarning, stacklevel=2) - - if show: - _printresmat(vfunc, interval, resmat) - return result - # Coefficients for Newton-Cotes quadrature # diff --git a/scipy/integrate/_tanhsinh.py b/scipy/integrate/_tanhsinh.py index 28f17cc65bc5..1f5fcecf7bc1 100644 --- a/scipy/integrate/_tanhsinh.py +++ b/scipy/integrate/_tanhsinh.py @@ -411,9 +411,10 @@ def check_termination(work): # Terminate before first iteration if integration limits are equal if work.nit == 0: i = (work.a == work.b).ravel() # ravel singleton dimension - zero = -np.inf if log else 0 - work.Sn[i] = zero - work.aerr[i] = zero + zero = np.full(work.Sn.shape, -np.inf if log else 0, dtype=Sn.dtype) + zero[np.isnan(Sn)] = np.nan + work.Sn[i] = zero[i] + work.aerr[i] = zero[i] work.status[i] = eim._ECONVERGED stop[i] = True else: diff --git a/scipy/integrate/tests/test__quad_vec.py b/scipy/integrate/tests/test__quad_vec.py index c88650ca1010..2ef0b54ff6d9 100644 --- a/scipy/integrate/tests/test__quad_vec.py +++ b/scipy/integrate/tests/test__quad_vec.py @@ -107,7 +107,7 @@ def _lorenzian(x): return 1 / (1 + x**2) -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) def test_quad_vec_pool(): f = _lorenzian res, err = quad_vec(f, -np.inf, np.inf, norm='max', epsabs=1e-4, workers=4) @@ -124,7 +124,7 @@ def _func_with_args(x, a): return x * (x + a) * np.arange(3) -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) @pytest.mark.parametrize('extra_args', [2, (2,)]) @pytest.mark.parametrize('workers', [1, 10]) def test_quad_vec_pool_args(extra_args, workers): diff --git a/scipy/integrate/tests/test_quadpack.py b/scipy/integrate/tests/test_quadpack.py index a503cb54918b..e61a69df40f9 100644 --- a/scipy/integrate/tests/test_quadpack.py +++ b/scipy/integrate/tests/test_quadpack.py @@ -541,7 +541,7 @@ def tfunc(x): class TestNQuad: - @pytest.mark.fail_slow(2) + @pytest.mark.fail_slow(5) def test_fixed_limits(self): def func1(x0, x1, x2, x3): val = (x0**2 + x1*x2 - x3**3 + np.sin(x0) + @@ -556,7 +556,7 @@ def opts_basic(*args): assert_quad(res[:-1], 1.5267454070738635) assert_(res[-1]['neval'] > 0 and res[-1]['neval'] < 4e5) - @pytest.mark.fail_slow(2) + @pytest.mark.fail_slow(5) def test_variable_limits(self): scale = .1 diff --git a/scipy/integrate/tests/test_quadrature.py b/scipy/integrate/tests/test_quadrature.py index 9006fb414152..25c28e342530 100644 --- a/scipy/integrate/tests/test_quadrature.py +++ b/scipy/integrate/tests/test_quadrature.py @@ -1,16 +1,14 @@ # mypy: disable-error-code="attr-defined" import pytest import numpy as np -from numpy import cos, sin, pi -from numpy.testing import (assert_equal, assert_almost_equal, assert_allclose, - assert_, suppress_warnings) +from numpy.testing import assert_equal, assert_almost_equal, assert_allclose from hypothesis import given import hypothesis.strategies as st import hypothesis.extra.numpy as hyp_num -from scipy.integrate import (quadrature, romberg, romb, newton_cotes, +from scipy.integrate import (romb, newton_cotes, cumulative_trapezoid, trapezoid, - quad, simpson, fixed_quad, AccuracyWarning, + quad, simpson, fixed_quad, qmc_quad, cumulative_simpson) from scipy.integrate._quadrature import _cumulative_simpson_unequal_intervals from scipy import stats, special @@ -32,59 +30,10 @@ def test_vector(self): assert_allclose(got, expected, rtol=1e-12) -@pytest.mark.filterwarnings('ignore::DeprecationWarning') class TestQuadrature: def quad(self, x, a, b, args): raise NotImplementedError - def test_quadrature(self): - # Typical function with two extra arguments: - def myfunc(x, n, z): # Bessel function integrand - return cos(n*x-z*sin(x))/pi - val, err = quadrature(myfunc, 0, pi, (2, 1.8)) - table_val = 0.30614353532540296487 - assert_almost_equal(val, table_val, decimal=7) - - def test_quadrature_rtol(self): - def myfunc(x, n, z): # Bessel function integrand - return 1e90 * cos(n*x-z*sin(x))/pi - val, err = quadrature(myfunc, 0, pi, (2, 1.8), rtol=1e-10) - table_val = 1e90 * 0.30614353532540296487 - assert_allclose(val, table_val, rtol=1e-10) - - def test_quadrature_miniter(self): - # Typical function with two extra arguments: - def myfunc(x, n, z): # Bessel function integrand - return cos(n*x-z*sin(x))/pi - table_val = 0.30614353532540296487 - for miniter in [5, 52]: - val, err = quadrature(myfunc, 0, pi, (2, 1.8), miniter=miniter) - assert_almost_equal(val, table_val, decimal=7) - assert_(err < 1.0) - - def test_quadrature_single_args(self): - def myfunc(x, n): - return 1e90 * cos(n*x-1.8*sin(x))/pi - val, err = quadrature(myfunc, 0, pi, args=2, rtol=1e-10) - table_val = 1e90 * 0.30614353532540296487 - assert_allclose(val, table_val, rtol=1e-10) - - def test_romberg(self): - # Typical function with two extra arguments: - def myfunc(x, n, z): # Bessel function integrand - return cos(n*x-z*sin(x))/pi - val = romberg(myfunc, 0, pi, args=(2, 1.8)) - table_val = 0.30614353532540296487 - assert_almost_equal(val, table_val, decimal=7) - - def test_romberg_rtol(self): - # Typical function with two extra arguments: - def myfunc(x, n, z): # Bessel function integrand - return 1e19*cos(n*x-z*sin(x))/pi - val = romberg(myfunc, 0, pi, args=(2, 1.8), rtol=1e-10) - table_val = 1e19*0.30614353532540296487 - assert_allclose(val, table_val, rtol=1e-10) - def test_romb(self): assert_equal(romb(np.arange(17)), 128) @@ -96,19 +45,6 @@ def test_romb_gh_3731(self): val2, err = quad(lambda x: np.cos(0.2*x), x.min(), x.max()) assert_allclose(val, val2, rtol=1e-8, atol=0) - # should be equal to romb with 2**k+1 samples - with suppress_warnings() as sup: - sup.filter(AccuracyWarning, "divmax .4. exceeded") - val3 = romberg(lambda x: np.cos(0.2*x), x.min(), x.max(), divmax=4) - assert_allclose(val, val3, rtol=1e-12, atol=0) - - def test_non_dtype(self): - # Check that we work fine with functions returning float - import math - valmath = romberg(math.sin, 0, 1) - expected_val = 0.45969769413185085 - assert_almost_equal(valmath, expected_val, decimal=7) - def test_newton_cotes(self): """Test the first few degrees, for evenly spaced points.""" n = 1 @@ -239,13 +175,6 @@ def test_simpson_2d_integer_no_x(self, droplast): assert_equal(result, expected) -@pytest.mark.parametrize('func', [romberg, quadrature]) -def test_deprecate_integrator(func): - message = f"`scipy.integrate.{func.__name__}` is deprecated..." - with pytest.deprecated_call(match=message): - func(np.exp, 0, 1) - - class TestCumulative_trapezoid: def test_1d(self): x = np.linspace(-2, 2, num=5) diff --git a/scipy/interpolate/_bspl.pyx b/scipy/interpolate/_bspl.pyx index 04f2ca88fbd3..c1de323ee847 100644 --- a/scipy/interpolate/_bspl.pyx +++ b/scipy/interpolate/_bspl.pyx @@ -264,7 +264,7 @@ def insert(double xval, if (interval + 1 <= 2*k) and (interval + 1 >= t.shape[0] - 2*k): # in case of a periodic spline (iopt.ne.0) there must be # either at least k interior knots t(j) satisfying t(k+1) 72: @@ -223,6 +221,8 @@ def __init__(self, title, key, if len(key) > 8: warnings.warn("key is > 8 characters (key is %s)" % key, LineOverflow, stacklevel=3) + self.title = title + self.key = key self.total_nlines = total_nlines self.pointer_nlines = pointer_nlines diff --git a/scipy/io/tests/test_mmio.py b/scipy/io/tests/test_mmio.py index b7178cf448c1..f8b5ccbb2b81 100644 --- a/scipy/io/tests/test_mmio.py +++ b/scipy/io/tests/test_mmio.py @@ -127,7 +127,7 @@ def test_random_rectangular_float(self): a = np.random.random(sz) self.check(a, (20, 15, 300, 'array', 'real', 'general')) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_bad_number_of_array_header_fields(self): s = """\ %%MatrixMarket matrix array real general diff --git a/scipy/linalg/_cythonized_array_utils.pyx b/scipy/linalg/_cythonized_array_utils.pyx index ad85421f5cd1..c38d8d795938 100644 --- a/scipy/linalg/_cythonized_array_utils.pyx +++ b/scipy/linalg/_cythonized_array_utils.pyx @@ -478,7 +478,7 @@ def is_sym_her_complex_c(const np_complex_numeric_t[:, ::1]A): return s @cython.initializedcheck(False) -def is_sym_her_complex_noncontig(np_complex_numeric_t[:, :]A): +def is_sym_her_complex_noncontig(const np_complex_numeric_t[:, :]A): cdef bint s with nogil: s = is_sym_her_complex_noncontig_internal(A) diff --git a/scipy/linalg/_decomp_schur.py b/scipy/linalg/_decomp_schur.py index 73bcaffeb700..54a5ce92dd58 100644 --- a/scipy/linalg/_decomp_schur.py +++ b/scipy/linalg/_decomp_schur.py @@ -42,6 +42,9 @@ def schur(a, output='real', lwork=None, overwrite_a=False, sort=None, Specifies whether the upper eigenvalues should be sorted. A callable may be passed that, given a eigenvalue, returns a boolean denoting whether the eigenvalue should be sorted to the top-left (True). + If output='real', the callable should have two arguments, the first + one being the real part of the eigenvalue, the second one being + the imaginary part. Alternatively, string parameters may be used:: 'lhp' Left-hand plane (x.real < 0.0) diff --git a/scipy/linalg/_matfuncs_sqrtm_triu.pyx b/scipy/linalg/_matfuncs_sqrtm_triu.pyx index 348199b6e007..237dc948cc51 100644 --- a/scipy/linalg/_matfuncs_sqrtm_triu.pyx +++ b/scipy/linalg/_matfuncs_sqrtm_triu.pyx @@ -9,7 +9,7 @@ cdef fused floating: complex128_t -def within_block_loop(floating[:,::1] R, floating[:,::1] T, start_stop_pairs, intp_t nblocks): +def within_block_loop(floating[:,::1] R, const floating[:,::1] T, start_stop_pairs, intp_t nblocks): cdef intp_t start, stop, i, j, k cdef floating s, denom, num diff --git a/scipy/linalg/_solve_toeplitz.pyx b/scipy/linalg/_solve_toeplitz.pyx index 59a8d32dc320..c75a28e3c8de 100644 --- a/scipy/linalg/_solve_toeplitz.pyx +++ b/scipy/linalg/_solve_toeplitz.pyx @@ -11,7 +11,7 @@ cdef fused dz: complex128_t -def levinson(dz[::1] a, dz[::1] b): +def levinson(const dz[::1] a, const dz[::1] b): """Solve a linear Toeplitz system using Levinson recursion. Parameters diff --git a/scipy/linalg/tests/test_decomp.py b/scipy/linalg/tests/test_decomp.py index 40904fea4c09..ab0133a37f3d 100644 --- a/scipy/linalg/tests/test_decomp.py +++ b/scipy/linalg/tests/test_decomp.py @@ -181,7 +181,8 @@ def test_gh_3054(self): assert_equal(w, np.inf) assert_allclose(vr, 1) - def _check_gen_eig(self, A, B, atol_homog=1e-13, rtol_homog=1e-13): + def _check_gen_eig(self, A, B, atol_homog=1e-13, rtol_homog=1e-13, + atol=1e-13, rtol=1e-13): if B is not None: A, B = asarray(A), asarray(B) B0 = B @@ -230,7 +231,7 @@ def _check_gen_eig(self, A, B, atol_homog=1e-13, rtol_homog=1e-13): for i in range(res.shape[1]): if np.all(isfinite(res[:, i])): assert_allclose(res[:, i], 0, - rtol=1e-13, atol=1e-13, err_msg=msg) + rtol=rtol, atol=atol, err_msg=msg) # try to consistently order eigenvalues, including complex conjugate pairs w_fin = w[isfinite(w)] @@ -269,7 +270,7 @@ def test_singular(self): [24, 35, 18, 21, 22]]) with np.errstate(all='ignore'): - self._check_gen_eig(A, B, atol_homog=5e-13) + self._check_gen_eig(A, B, atol_homog=5e-13, atol=5e-13) def test_falker(self): # Test matrices giving some Nan generalized eigenvalues. @@ -1178,7 +1179,7 @@ class TestSVD_GESVD(TestSVD_GESDD): lapack_driver = 'gesvd' -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) def test_svd_gesdd_nofegfault(): # svd(a) with {U,VT}.size > INT_MAX does not segfault # cf https://github.com/scipy/scipy/issues/14001 @@ -2601,7 +2602,7 @@ def test_sort_explicit(self): class TestOrdQZWorkspaceSize: - @pytest.mark.fail_slow(2) + @pytest.mark.fail_slow(5) def test_decompose(self): rng = np.random.RandomState(12345) N = 202 diff --git a/scipy/linalg/tests/test_extending.py b/scipy/linalg/tests/test_extending.py index e6ea695b33b7..4e581fa405a9 100644 --- a/scipy/linalg/tests/test_extending.py +++ b/scipy/linalg/tests/test_extending.py @@ -9,7 +9,7 @@ from scipy.linalg.lapack import dgtsv # type: ignore[attr-defined] -@pytest.mark.fail_slow(60) +@pytest.mark.fail_slow(120) # essential per https://github.com/scipy/scipy/pull/20487#discussion_r1567057247 @pytest.mark.skipif(IS_EDITABLE, reason='Editable install cannot find .pxd headers.') diff --git a/scipy/linalg/tests/test_matfuncs.py b/scipy/linalg/tests/test_matfuncs.py index 5f17a639eff7..c6614961a271 100644 --- a/scipy/linalg/tests/test_matfuncs.py +++ b/scipy/linalg/tests/test_matfuncs.py @@ -752,7 +752,7 @@ def test_readonly(self): a.flags.writeable = False expm(a) - @pytest.mark.fail_slow(2) + @pytest.mark.fail_slow(5) def test_gh18086(self): A = np.zeros((400, 400), dtype=float) rng = np.random.default_rng(100) diff --git a/scipy/meson.build b/scipy/meson.build index 988458d21a3b..79a16cd5b8ea 100644 --- a/scipy/meson.build +++ b/scipy/meson.build @@ -206,6 +206,7 @@ if blas_name == 'openblas' or blas_name == 'auto' blas = dependency('scipy-openblas', method: 'pkg-config', required: false) if blas.found() blas_name = 'scipy-openblas' + generate_blas_wrappers = true endif endif @@ -298,7 +299,8 @@ python_sources = [ 'special.pxd', ] -if blas_name == 'scipy-openblas' +fs = import('fs') +if fs.exists('_distributor_init_local.py') python_sources += ['_distributor_init_local.py'] endif @@ -309,7 +311,6 @@ py3.install_sources( # Copy the main __init__.py and pxd files to the build dir. # Needed to trick Cython, it won't do a relative import outside a package -fs = import('fs') #_cython_tree = declare_dependency(sources: [ _cython_tree = [ fs.copyfile('__init__.py'), diff --git a/scipy/misc/tests/test_doccer.py b/scipy/misc/tests/test_doccer.py index fa34228ddff7..2ba37becccec 100644 --- a/scipy/misc/tests/test_doccer.py +++ b/scipy/misc/tests/test_doccer.py @@ -72,6 +72,7 @@ def test_docformat(): @pytest.mark.skipif(DOCSTRINGS_STRIPPED, reason="docstrings stripped") +@pytest.mark.skipif(sys.version_info >= (3, 13), reason='it fails on Py3.13') def test_decorator(): with suppress_warnings() as sup: sup.filter(category=DeprecationWarning) diff --git a/scipy/ndimage/_filters.py b/scipy/ndimage/_filters.py index 635b2d336b34..ed9687c5c14d 100644 --- a/scipy/ndimage/_filters.py +++ b/scipy/ndimage/_filters.py @@ -883,11 +883,13 @@ def convolve(input, weights, output=None, mode='reflect', cval=0.0, cval : scalar, optional Value to fill past edges of input if `mode` is 'constant'. Default is 0.0 - origin : int, optional - Controls the origin of the input signal, which is where the - filter is centered to produce the first element of the output. - Positive values shift the filter to the right, and negative values - shift the filter to the left. Default is 0. + origin : int or sequence, optional + Controls the placement of the filter on the input array's pixels. + A value of 0 (the default) centers the filter over the pixel, with + positive values shifting the filter to the right, and negative ones + to the left. By passing a sequence of origins with length equal to + the number of dimensions of the input array, different shifts can + be specified along each axis. Returns ------- diff --git a/scipy/ndimage/_morphology.py b/scipy/ndimage/_morphology.py index fabf0926e013..22ada0b130f9 100644 --- a/scipy/ndimage/_morphology.py +++ b/scipy/ndimage/_morphology.py @@ -1142,7 +1142,8 @@ def grey_erosion(input, size=None, footprint=None, structure=None, neighbors of the center over which the minimum is chosen. structure : array of ints, optional Structuring element used for the grayscale erosion. `structure` - may be a non-flat structuring element. + may be a non-flat structuring element. The `structure` array applies a + subtractive offset for each pixel in the neighborhood. output : array, optional An array used for storing the output of the erosion may be provided. mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional @@ -1253,7 +1254,8 @@ def grey_dilation(input, size=None, footprint=None, structure=None, neighbors of the center over which the maximum is chosen. structure : array of ints, optional Structuring element used for the grayscale dilation. `structure` - may be a non-flat structuring element. + may be a non-flat structuring element. The `structure` array applies an + additive offset for each pixel in the neighborhood. output : array, optional An array used for storing the output of the dilation may be provided. mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional @@ -1399,7 +1401,9 @@ def grey_opening(input, size=None, footprint=None, structure=None, used for the grayscale opening. structure : array of ints, optional Structuring element used for the grayscale opening. `structure` - may be a non-flat structuring element. + may be a non-flat structuring element. The `structure` array applies + offsets to the pixels in a neighborhood (the offset is additive during + dilation and subtractive during erosion). output : array, optional An array used for storing the output of the opening may be provided. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional @@ -1484,7 +1488,9 @@ def grey_closing(input, size=None, footprint=None, structure=None, used for the grayscale closing. structure : array of ints, optional Structuring element used for the grayscale closing. `structure` - may be a non-flat structuring element. + may be a non-flat structuring element. The `structure` array applies + offsets to the pixels in a neighborhood (the offset is additive during + dilation and subtractive during erosion) output : array, optional An array used for storing the output of the closing may be provided. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional @@ -1570,8 +1576,10 @@ def morphological_gradient(input, size=None, footprint=None, structure=None, used for the morphology operations. Larger footprints give a more blurred morphological gradient. structure : array of ints, optional - Structuring element used for the morphology operations. - `structure` may be a non-flat structuring element. + Structuring element used for the morphology operations. `structure` may + be a non-flat structuring element. The `structure` array applies + offsets to the pixels in a neighborhood (the offset is additive during + dilation and subtractive during erosion) output : array, optional An array used for storing the output of the morphological gradient may be provided. @@ -1673,12 +1681,18 @@ def morphological_laplace(input, size=None, footprint=None, ---------- input : array_like Input. - size : int or sequence of ints, optional - See `structure`. - footprint : bool or ndarray, optional - See `structure`. - structure : structure, optional - Either `size`, `footprint`, or the `structure` must be provided. + size : tuple of ints + Shape of a flat and full structuring element used for the mathematical + morphology operations. Optional if `footprint` or `structure` is + provided. + footprint : array of ints, optional + Positions of non-infinite elements of a flat structuring element + used for the morphology operations. + structure : array of ints, optional + Structuring element used for the morphology operations. `structure` may + be a non-flat structuring element. The `structure` array applies + offsets to the pixels in a neighborhood (the offset is additive during + dilation and subtractive during erosion) output : ndarray, optional An output array can optionally be provided. mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional @@ -1730,8 +1744,10 @@ def white_tophat(input, size=None, footprint=None, structure=None, Positions of elements of a flat structuring element used for the white tophat filter. structure : array of ints, optional - Structuring element used for the filter. `structure` - may be a non-flat structuring element. + Structuring element used for the filter. `structure` may be a non-flat + structuring element. The `structure` array applies offsets to the + pixels in a neighborhood (the offset is additive during dilation and + subtractive during erosion) output : array, optional An array used for storing the output of the filter may be provided. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional @@ -1808,8 +1824,10 @@ def black_tophat(input, size=None, footprint=None, Positions of non-infinite elements of a flat structuring element used for the black tophat filter. structure : array of ints, optional - Structuring element used for the filter. `structure` - may be a non-flat structuring element. + Structuring element used for the filter. `structure` may be a non-flat + structuring element. The `structure` array applies offsets to the + pixels in a neighborhood (the offset is additive during dilation and + subtractive during erosion) output : array, optional An array used for storing the output of the filter may be provided. mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional diff --git a/scipy/optimize/_cobyqa_py.py b/scipy/optimize/_cobyqa_py.py index 5d164771a213..4928fca9c162 100644 --- a/scipy/optimize/_cobyqa_py.py +++ b/scipy/optimize/_cobyqa_py.py @@ -10,7 +10,7 @@ def _minimize_cobyqa(fun, x0, args=(), bounds=None, constraints=(), **unknown_options): """ Minimize a scalar function of one or more variables using the - Constrained Optimization BY Quadratic Approximations (COBYQA) algorithm. + Constrained Optimization BY Quadratic Approximations (COBYQA) algorithm [1]_. .. versionadded:: 1.14.0 @@ -40,6 +40,11 @@ def _minimize_cobyqa(fun, x0, args=(), bounds=None, constraints=(), if all the lower and upper bounds are finite, the variables are scaled to be within the range :math:`[-1, 1]`. If any of the lower or upper bounds is infinite, the variables are not scaled. + + References + ---------- + .. [1] COBYQA + https://www.cobyqa.com/stable/ """ from .._lib.cobyqa import minimize # import here to avoid circular imports diff --git a/scipy/optimize/_differentialevolution.py b/scipy/optimize/_differentialevolution.py index 815d27afd072..4d2c0d339055 100644 --- a/scipy/optimize/_differentialevolution.py +++ b/scipy/optimize/_differentialevolution.py @@ -105,7 +105,7 @@ def differential_evolution(func, bounds, args=(), strategy='best1bin', of 2 after ``popsize * (N - N_equal)``. tol : float, optional Relative tolerance for convergence, the solving stops when - ``np.std(pop) <= atol + tol * np.abs(np.mean(population_energies))``, + ``np.std(population_energies) <= atol + tol * np.abs(np.mean(population_energies))``, where and `atol` and `tol` are the absolute and relative tolerance respectively. mutation : float or tuple(float, float), optional @@ -481,7 +481,7 @@ def differential_evolution(func, bounds, args=(), strategy='best1bin', ... trial = np.where(crossovers, bprime, trial) ... return trial - """ + """# noqa: E501 # using a context manager means that any created Pool objects are # cleared up. @@ -575,7 +575,7 @@ class DifferentialEvolutionSolver: of 2 after ``popsize * (N - N_equal)``. tol : float, optional Relative tolerance for convergence, the solving stops when - ``np.std(pop) <= atol + tol * np.abs(np.mean(population_energies))``, + ``np.std(population_energies) <= atol + tol * np.abs(np.mean(population_energies))``, where and `atol` and `tol` are the absolute and relative tolerance respectively. mutation : float or tuple(float, float), optional @@ -723,7 +723,7 @@ class DifferentialEvolutionSolver: ignored if ``workers != 1``. This option will override the `updating` keyword to ``updating='deferred'``. - """ + """ # noqa: E501 # Dispatch of mutation strategy method (binomial or exponential). _binomial = {'best1bin': '_best1', diff --git a/scipy/optimize/_highs/meson.build b/scipy/optimize/_highs/meson.build index 7ef1cefc65dd..5dbd8b72fdca 100644 --- a/scipy/optimize/_highs/meson.build +++ b/scipy/optimize/_highs/meson.build @@ -51,7 +51,8 @@ basiclu_lib = static_library('basiclu', '../../_lib/highs/src', '../../_lib/highs/src/ipm/basiclu/include' ], - c_args: [Wno_unused_variable, highs_define_macros] + c_args: [Wno_unused_variable, highs_define_macros], + gnu_symbol_visibility: 'inlineshidden', ) highs_flags = [ @@ -109,7 +110,8 @@ ipx_lib = static_library('ipx', 'cython/src/' ], dependencies: thread_dep, - cpp_args: [highs_flags, highs_define_macros] + cpp_args: [highs_flags, highs_define_macros], + gnu_symbol_visibility: 'inlineshidden', ) highs_lib = static_library('highs', @@ -226,7 +228,8 @@ highs_lib = static_library('highs', '../../_lib/highs/src/util/', ], dependencies: thread_dep, - cpp_args: [highs_flags, highs_define_macros] + cpp_args: [highs_flags, highs_define_macros], + gnu_symbol_visibility: 'inlineshidden', ) _highs_wrapper = py3.extension_module('_highs_wrapper', diff --git a/scipy/optimize/_isotonic.py b/scipy/optimize/_isotonic.py index 929481e02261..bbbce625a7e5 100644 --- a/scipy/optimize/_isotonic.py +++ b/scipy/optimize/_isotonic.py @@ -121,11 +121,13 @@ class of strictly consistent scoring functions for the mean, see [2]_ input y of length 1000, the minimizer takes about 4 seconds, while ``isotonic_regression`` takes about 200 microseconds. """ - yarr = np.asarray(y) # Check yarr.ndim == 1 is implicit (pybind11) in pava. + yarr = np.atleast_1d(y) # Check yarr.ndim == 1 is implicit (pybind11) in pava. + order = slice(None) if increasing else slice(None, None, -1) + x = np.array(yarr[order], order="C", dtype=np.float64, copy=True) if weights is None: - warr = np.ones_like(yarr) + wx = np.ones_like(yarr, dtype=np.float64) else: - warr = np.asarray(weights) + warr = np.atleast_1d(weights) if not (yarr.ndim == warr.ndim == 1 and yarr.shape[0] == warr.shape[0]): raise ValueError( @@ -134,9 +136,7 @@ class of strictly consistent scoring functions for the mean, see [2]_ if np.any(warr <= 0): raise ValueError("Weights w must be strictly positive.") - order = slice(None) if increasing else slice(None, None, -1) - x = np.array(yarr[order], order="C", dtype=np.float64, copy=True) - wx = np.array(warr[order], order="C", dtype=np.float64, copy=True) + wx = np.array(warr[order], order="C", dtype=np.float64, copy=True) n = x.shape[0] r = np.full(shape=n + 1, fill_value=-1, dtype=np.intp) x, wx, r, b = pava(x, wx, r) diff --git a/scipy/optimize/tests/test__basinhopping.py b/scipy/optimize/tests/test__basinhopping.py index 4fbd376ac2c1..2e02dd670eac 100644 --- a/scipy/optimize/tests/test__basinhopping.py +++ b/scipy/optimize/tests/test__basinhopping.py @@ -199,7 +199,7 @@ def test_2d_nograd(self): niter=self.niter, disp=self.disp) assert_almost_equal(res.x, self.sol[i], self.tol) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_all_minimizers(self): # Test 2-D minimizations with gradient. Nelder-Mead, Powell, COBYLA, and # COBYQA don't accept jac=True, so aren't included here. @@ -213,7 +213,7 @@ def test_all_minimizers(self): niter=self.niter, disp=self.disp) assert_almost_equal(res.x, self.sol[i], self.tol) - @pytest.mark.fail_slow(10) + @pytest.mark.fail_slow(20) def test_all_nograd_minimizers(self): # Test 2-D minimizations without gradient. Newton-CG requires jac=True, # so not included here. diff --git a/scipy/optimize/tests/test__differential_evolution.py b/scipy/optimize/tests/test__differential_evolution.py index b6b1ba39a242..f3c7af51d161 100644 --- a/scipy/optimize/tests/test__differential_evolution.py +++ b/scipy/optimize/tests/test__differential_evolution.py @@ -722,7 +722,7 @@ def test_immediate_updating(self): pass assert s._updating == 'deferred' - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_parallel(self): # smoke test for parallelization with deferred updating bounds = [(0., 2.), (0., 2.)] @@ -864,7 +864,7 @@ def constr_f(x): assert constr_f(res.x) <= 1.9 assert res.success - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_impossible_constraint(self): def constr_f(x): return np.array([x[0] + x[1]]) @@ -1021,7 +1021,7 @@ def test_matrix_linear_constraint(self): xtrial = np.arange(4 * 5).reshape(4, 5) assert cw.violation(xtrial).shape == (2, 5) - @pytest.mark.fail_slow(10) + @pytest.mark.fail_slow(20) def test_L1(self): # Lampinen ([5]) test problem 1 @@ -1116,7 +1116,7 @@ def c2(x): assert_(np.all(res.x >= np.array(bounds)[:, 0])) assert_(np.all(res.x <= np.array(bounds)[:, 1])) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_L2(self): # Lampinen ([5]) test problem 2 @@ -1156,7 +1156,7 @@ def c1(x): assert_(np.all(res.x >= np.array(bounds)[:, 0])) assert_(np.all(res.x <= np.array(bounds)[:, 1])) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_L3(self): # Lampinen ([5]) test problem 3 @@ -1207,7 +1207,7 @@ def c1(x): assert_(np.all(res.x >= np.array(bounds)[:, 0])) assert_(np.all(res.x <= np.array(bounds)[:, 1])) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_L4(self): # Lampinen ([5]) test problem 4 def f(x): @@ -1260,7 +1260,7 @@ def c1(x): assert_(np.all(res.x >= np.array(bounds)[:, 0])) assert_(np.all(res.x <= np.array(bounds)[:, 1])) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_L5(self): # Lampinen ([5]) test problem 5 @@ -1291,7 +1291,7 @@ def c1(x): assert_(np.all(res.x >= np.array(bounds)[:, 0])) assert_(np.all(res.x <= np.array(bounds)[:, 1])) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_L6(self): # Lampinen ([5]) test problem 6 def f(x): @@ -1420,6 +1420,7 @@ def c1(x): assert_(np.all(res.x >= np.array(bounds)[:, 0])) assert_(np.all(res.x <= np.array(bounds)[:, 1])) + @pytest.mark.fail_slow(5) def test_L9(self): # Lampinen ([5]) test problem 9 @@ -1450,7 +1451,7 @@ def c1(x): assert_(np.all(res.x >= np.array(bounds)[:, 0])) assert_(np.all(res.x <= np.array(bounds)[:, 1])) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_integrality(self): # test fitting discrete distribution to data rng = np.random.default_rng(6519843218105) @@ -1540,7 +1541,7 @@ def f(x): DifferentialEvolutionSolver(f, bounds=bounds, polish=False, integrality=integrality) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_vectorized(self): def quadratic(x): return np.sum(x**2) @@ -1632,6 +1633,7 @@ def func(x): # "MAXCV = 0.". assert "MAXCV = 0.4" in result.message + @pytest.mark.fail_slow(20) # fail-slow exception by request - see gh-20806 def test_strategy_fn(self): # examines ability to customize strategy by mimicking one of the # in-built strategies diff --git a/scipy/optimize/tests/test__dual_annealing.py b/scipy/optimize/tests/test__dual_annealing.py index 6819d7f2be52..15b9307290ca 100644 --- a/scipy/optimize/tests/test__dual_annealing.py +++ b/scipy/optimize/tests/test__dual_annealing.py @@ -110,7 +110,7 @@ def test_low_dim(self): assert_allclose(ret.fun, 0., atol=1e-12) assert ret.success - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_high_dim(self): ret = dual_annealing(self.func, self.hd_bounds, seed=self.seed) assert_allclose(ret.fun, 0., atol=1e-12) @@ -121,7 +121,7 @@ def test_low_dim_no_ls(self): no_local_search=True, seed=self.seed) assert_allclose(ret.fun, 0., atol=1e-4) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_high_dim_no_ls(self): ret = dual_annealing(self.func, self.hd_bounds, no_local_search=True, seed=self.seed) @@ -140,7 +140,7 @@ def test_max_reinit(self): assert_raises(ValueError, dual_annealing, self.weirdfunc, self.ld_bounds) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_reproduce(self): res1 = dual_annealing(self.func, self.ld_bounds, seed=self.seed) res2 = dual_annealing(self.func, self.ld_bounds, seed=self.seed) @@ -282,7 +282,7 @@ def test_gradient_gnev(self): seed=self.seed) assert ret.njev == self.ngev - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_from_docstring(self): def func(x): return np.sum(x * x - 10 * np.cos(2 * np.pi * x)) + 10 * np.size(x) @@ -338,7 +338,7 @@ def test_accept_reject_probabilistic( assert_allclose(rate, accept_rate) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_bounds_class(self): # test that result does not depend on the bounds type def func(x): @@ -367,6 +367,7 @@ def func(x): assert_allclose(ret_bounds_list.fun, ret_bounds_class.fun, atol=1e-9) assert ret_bounds_list.nfev == ret_bounds_class.nfev + @pytest.mark.fail_slow(10) def test_callable_jac_hess_with_args_gh11052(self): # dual_annealing used to fail when `jac` was callable and `args` were # used; check that this is resolved. Example is from gh-11052. diff --git a/scipy/optimize/tests/test__shgo.py b/scipy/optimize/tests/test__shgo.py index fea1fb70fbda..ad8466beca5e 100644 --- a/scipy/optimize/tests/test__shgo.py +++ b/scipy/optimize/tests/test__shgo.py @@ -470,7 +470,7 @@ def test_f5_2_cons_symmetry(self): options=options, iters=1, sampling_method='simplicial') - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_f5_3_cons_symmetry(self): """Assymmetrically constrained test function""" options = {'symmetry': [0, 0, 0, 3], @@ -778,7 +778,7 @@ def f(x): np.testing.assert_allclose(res_new_bounds.x, x_opt) np.testing.assert_allclose(res_new_bounds.x, res_old_bounds.x) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_19_parallelization(self): """Test the functionality to add custom sampling methods to shgo""" diff --git a/scipy/optimize/tests/test_constraint_conversion.py b/scipy/optimize/tests/test_constraint_conversion.py index df5ab6dee478..8f2cc7c356a0 100644 --- a/scipy/optimize/tests/test_constraint_conversion.py +++ b/scipy/optimize/tests/test_constraint_conversion.py @@ -61,7 +61,7 @@ def fun(x): class TestNewToOld: - + @pytest.mark.fail_slow(2) def test_multiple_constraint_objects(self): def fun(x): return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2 + (x[2] - 0.75) ** 2 @@ -90,7 +90,7 @@ def fun(x): assert_allclose(funs['cobyla'], funs['trust-constr'], rtol=1e-4) assert_allclose(funs['cobyqa'], funs['trust-constr'], rtol=1e-4) - @pytest.mark.fail_slow(10) + @pytest.mark.fail_slow(20) def test_individual_constraint_objects(self): def fun(x): return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2 + (x[2] - 0.75) ** 2 diff --git a/scipy/optimize/tests/test_extending.py b/scipy/optimize/tests/test_extending.py index 80e25f28891c..48c91a5cf4cf 100644 --- a/scipy/optimize/tests/test_extending.py +++ b/scipy/optimize/tests/test_extending.py @@ -6,7 +6,7 @@ from scipy._lib._testutils import IS_EDITABLE, _test_cython_extension, cython -@pytest.mark.fail_slow(20) +@pytest.mark.fail_slow(40) # essential per https://github.com/scipy/scipy/pull/20487#discussion_r1567057247 @pytest.mark.skipif(IS_EDITABLE, reason='Editable install cannot find .pxd headers.') diff --git a/scipy/optimize/tests/test_isotonic_regression.py b/scipy/optimize/tests/test_isotonic_regression.py index ac574b34b1a1..b49c56db5b44 100644 --- a/scipy/optimize/tests/test_isotonic_regression.py +++ b/scipy/optimize/tests/test_isotonic_regression.py @@ -16,7 +16,9 @@ class TestIsotonicRegression: "Input arrays y and w must have one dimension of equal length"), ([0, 1], [1], "Input arrays y and w must have one dimension of equal length"), - (1, 2, + (1, [1, 2], + "Input arrays y and w must have one dimension of equal length"), + ([1, 2], 1, "Input arrays y and w must have one dimension of equal length"), ([0, 1], [0, 1], "Weights w must be strictly positive"), diff --git a/scipy/optimize/tests/test_least_squares.py b/scipy/optimize/tests/test_least_squares.py index 68cfc421c987..954fc8f2d941 100644 --- a/scipy/optimize/tests/test_least_squares.py +++ b/scipy/optimize/tests/test_least_squares.py @@ -468,7 +468,7 @@ def test_bounds_instances(self): bounds=Bounds(lb=[0.1, 0.1])) assert_allclose(res.x, [0.1, 0.1], atol=1e-5) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_rosenbrock_bounds(self): x0_1 = np.array([-2.0, 1.0]) x0_2 = np.array([2.0, 2.0]) @@ -554,7 +554,7 @@ def test_numerical_jac(self): assert_allclose(res_dense.cost, 0, atol=1e-20) assert_allclose(res_sparse.cost, 0, atol=1e-20) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_with_bounds(self): p = BroydenTridiagonal() for jac, jac_sparsity in product( diff --git a/scipy/optimize/tests/test_linprog.py b/scipy/optimize/tests/test_linprog.py index 1e304cd038ad..62b7018f9d01 100644 --- a/scipy/optimize/tests/test_linprog.py +++ b/scipy/optimize/tests/test_linprog.py @@ -1802,7 +1802,7 @@ def test_crossover(self): # there should be nonzero crossover iterations for IPM (only) assert_equal(res.crossover_nit == 0, self.method != "highs-ipm") - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_marginals(self): # Ensure lagrange multipliers are correct by comparing the derivative # w.r.t. b_ub/b_eq/ub/lb to the reported duals. @@ -2249,7 +2249,7 @@ class TestLinprogHiGHSMIP: method = "highs" options = {} - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) @pytest.mark.xfail(condition=(sys.maxsize < 2 ** 32 and platform.system() == "Linux"), run=False, diff --git a/scipy/optimize/tests/test_lsq_linear.py b/scipy/optimize/tests/test_lsq_linear.py index a2fdd1221851..5f00924524d6 100644 --- a/scipy/optimize/tests/test_lsq_linear.py +++ b/scipy/optimize/tests/test_lsq_linear.py @@ -207,7 +207,7 @@ def test_sparse_and_LinearOperator(self): res = lsq_linear(A, b) assert_allclose(res.optimality, 0, atol=1e-6) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_sparse_bounds(self): m = 5000 n = 1000 diff --git a/scipy/optimize/tests/test_optimize.py b/scipy/optimize/tests/test_optimize.py index 86c6ab268ee4..8a591156a80c 100644 --- a/scipy/optimize/tests/test_optimize.py +++ b/scipy/optimize/tests/test_optimize.py @@ -1261,7 +1261,7 @@ def dfunc(z): assert func(sol1.x) < func(sol2.x), \ f"{method}: {func(sol1.x)} vs. {func(sol2.x)}" - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) @pytest.mark.filterwarnings('ignore::UserWarning') @pytest.mark.filterwarnings('ignore::RuntimeWarning') # See gh-18547 @pytest.mark.parametrize('method', @@ -2495,6 +2495,7 @@ def setup_method(self): self.hessp = optimize.rosen_hess_prod self.bounds = [(0., 10.), (0., 10.)] + @pytest.mark.fail_slow(2) def test_attributes_present(self): attributes = ['nit', 'nfev', 'x', 'success', 'status', 'fun', 'message'] @@ -2584,7 +2585,7 @@ def f(x): optimize.brute(f, [(-1, 1)], Ns=3, finish=None) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_workers(self): # check that parallel evaluation works resbrute = optimize.brute(brute_func, self.rranges, args=self.params, @@ -2615,7 +2616,7 @@ def f(x, *args): assert_allclose(resbrute, 0) -@pytest.mark.fail_slow(10) +@pytest.mark.fail_slow(20) def test_cobyla_threadsafe(): # Verify that cobyla is threadsafe. Will segfault if it is not. @@ -2663,7 +2664,7 @@ def slow_func(self, v): r, t = np.sqrt(v[0]**2+v[1]**2), np.arctan2(v[0], v[1]) return np.sin(r*20 + t)+r*0.5 - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_neldermead_limit(self): self.check_limits("Nelder-Mead", 200) diff --git a/scipy/optimize/tests/test_trustregion_exact.py b/scipy/optimize/tests/test_trustregion_exact.py index 42c649218078..b48e4153d0cc 100644 --- a/scipy/optimize/tests/test_trustregion_exact.py +++ b/scipy/optimize/tests/test_trustregion_exact.py @@ -275,7 +275,7 @@ def test_for_jac_very_close_to_zero(self): -0.84954934]) assert_array_almost_equal(hits_boundary, True) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_for_random_entries(self): # Seed np.random.seed(1) diff --git a/scipy/signal/__init__.py b/scipy/signal/__init__.py index f56cf4ef1fd3..931d3ae4c005 100644 --- a/scipy/signal/__init__.py +++ b/scipy/signal/__init__.py @@ -125,7 +125,6 @@ buttap -- Return (z,p,k) for analog prototype of Butterworth filter. cheb1ap -- Return (z,p,k) for type I Chebyshev filter. cheb2ap -- Return (z,p,k) for type II Chebyshev filter. - cmplx_sort -- Sort roots based on magnitude. ellipap -- Return (z,p,k) for analog prototype of elliptic filter. lp2bp -- Transform a lowpass filter prototype to a bandpass filter. lp2bp_zpk -- Transform a lowpass filter prototype to a bandpass filter. @@ -236,20 +235,6 @@ get_window -- Return a window of a given length and type. -Wavelets -======== - -.. autosummary:: - :toctree: generated/ - - cascade -- Compute scaling function and wavelet from coefficients. - daub -- Return low-pass. - morlet -- Complex Morlet wavelet. - qmf -- Return quadrature mirror filter from low-pass. - ricker -- Return ricker wavelet. - morlet2 -- Return Morlet wavelet, compatible with cwt. - cwt -- Perform continuous wavelet transform. - Peak finding ============ @@ -323,7 +308,6 @@ from ._savitzky_golay import savgol_coeffs, savgol_filter from ._spectral_py import * from ._short_time_fft import * -from ._wavelets import * from ._peak_finding import * from ._czt import * from .windows import get_window # keep this one in signal namespace diff --git a/scipy/signal/_max_len_seq_inner.pyx b/scipy/signal/_max_len_seq_inner.pyx index 8ce5bc4e72a2..f9bb116cefa6 100644 --- a/scipy/signal/_max_len_seq_inner.pyx +++ b/scipy/signal/_max_len_seq_inner.pyx @@ -11,7 +11,7 @@ np.import_array() @cython.cdivision(True) # faster modulo @cython.boundscheck(False) # designed to stay within bounds @cython.wraparound(False) # we don't use negative indexing -def _max_len_seq_inner(Py_ssize_t[::1] taps, +def _max_len_seq_inner(const Py_ssize_t[::1] taps, np.int8_t[::1] state, Py_ssize_t nbits, Py_ssize_t length, np.int8_t[::1] seq): diff --git a/scipy/signal/_peak_finding.py b/scipy/signal/_peak_finding.py index 3dbb04126325..b5ba280afb3f 100644 --- a/scipy/signal/_peak_finding.py +++ b/scipy/signal/_peak_finding.py @@ -1255,8 +1255,6 @@ def find_peaks_cwt(vector, widths, wavelet=None, max_distances=None, See Also -------- - cwt - Continuous wavelet transform. find_peaks Find peaks inside a signal based on peak properties. diff --git a/scipy/signal/_peak_finding_utils.pyx b/scipy/signal/_peak_finding_utils.pyx index 997272cc60e7..16c67ecc7c14 100644 --- a/scipy/signal/_peak_finding_utils.pyx +++ b/scipy/signal/_peak_finding_utils.pyx @@ -88,8 +88,8 @@ def _local_maxima_1d(const np.float64_t[::1] x not None): return midpoints.base, left_edges.base, right_edges.base -def _select_by_peak_distance(np.intp_t[::1] peaks not None, - np.float64_t[::1] priority not None, +def _select_by_peak_distance(const np.intp_t[::1] peaks not None, + const np.float64_t[::1] priority not None, np.float64_t distance): """ Evaluate which peaks fulfill the distance condition. @@ -164,7 +164,7 @@ class PeakPropertyWarning(RuntimeWarning): def _peak_prominences(const np.float64_t[::1] x not None, - np.intp_t[::1] peaks not None, + const np.intp_t[::1] peaks not None, np.intp_t wlen): """ Calculate the prominence of each peak in a signal. @@ -262,11 +262,11 @@ def _peak_prominences(const np.float64_t[::1] x not None, def _peak_widths(const np.float64_t[::1] x not None, - np.intp_t[::1] peaks not None, + const np.intp_t[::1] peaks not None, np.float64_t rel_height, - np.float64_t[::1] prominences not None, - np.intp_t[::1] left_bases not None, - np.intp_t[::1] right_bases not None): + const np.float64_t[::1] prominences not None, + const np.intp_t[::1] left_bases not None, + const np.intp_t[::1] right_bases not None): """ Calculate the width of each each peak in a signal. diff --git a/scipy/signal/_signaltools.py b/scipy/signal/_signaltools.py index 620ae3f5ac45..ef1d50fdb74b 100644 --- a/scipy/signal/_signaltools.py +++ b/scipy/signal/_signaltools.py @@ -31,7 +31,7 @@ 'convolve', 'convolve2d', 'fftconvolve', 'oaconvolve', 'order_filter', 'medfilt', 'medfilt2d', 'wiener', 'lfilter', 'lfiltic', 'sosfilt', 'deconvolve', 'hilbert', 'hilbert2', - 'cmplx_sort', 'unique_roots', 'invres', 'invresz', 'residue', + 'unique_roots', 'invres', 'invresz', 'residue', 'residuez', 'resample', 'resample_poly', 'detrend', 'lfilter_zi', 'sosfilt_zi', 'sosfiltfilt', 'choose_conv_method', 'filtfilt', 'decimate', 'vectorstrength'] @@ -2452,18 +2452,6 @@ def hilbert2(x, N=None): return x -_msg_cplx_sort="""cmplx_sort was deprecated in SciPy 1.12 and will be removed -in SciPy 1.15. The exact equivalent for a numpy array argument is ->>> def cmplx_sort(p): -... idx = np.argsort(abs(p)) -... return np.take(p, idx, 0), idx -""" - -def cmplx_sort(p): - warnings.warn(_msg_cplx_sort, DeprecationWarning, stacklevel=2) - return _cmplx_sort(p) - - def _cmplx_sort(p): """Sort roots based on magnitude. diff --git a/scipy/signal/_upfirdn_apply.pyx b/scipy/signal/_upfirdn_apply.pyx index 6419816b1984..27b2361cc4f7 100644 --- a/scipy/signal/_upfirdn_apply.pyx +++ b/scipy/signal/_upfirdn_apply.pyx @@ -274,7 +274,7 @@ cpdef _pad_test(np.ndarray[DTYPE_t] data, np.intp_t npre=0, np.intp_t npost=0, return np.asarray(out) -def _apply(np.ndarray data, DTYPE_t [::1] h_trans_flip, np.ndarray out, +def _apply(np.ndarray data, const DTYPE_t [::1] h_trans_flip, np.ndarray out, np.intp_t up, np.intp_t down, np.intp_t axis, np.intp_t mode, DTYPE_t cval): cdef ArrayInfo data_info, output_info diff --git a/scipy/signal/_waveforms.py b/scipy/signal/_waveforms.py index 55bd045cdbf2..324e99c1c78c 100644 --- a/scipy/signal/_waveforms.py +++ b/scipy/signal/_waveforms.py @@ -203,10 +203,6 @@ def gausspulse(t, fc=1000, bw=0.5, bwr=-6, tpr=-60, retquad=False, yenv : ndarray Envelope of signal. Only returned if `retenv` is True. - See Also - -------- - scipy.signal.morlet - Examples -------- Plot real component, imaginary component, and envelope for a 5 Hz pulse, diff --git a/scipy/signal/_wavelets.py b/scipy/signal/_wavelets.py index 7cfc9f77d6f7..2b9f8fa32672 100644 --- a/scipy/signal/_wavelets.py +++ b/scipy/signal/_wavelets.py @@ -1,363 +1,6 @@ -import warnings - import numpy as np -from scipy.linalg import eig -from scipy.special import comb from scipy.signal import convolve -__all__ = ['daub', 'qmf', 'cascade', 'morlet', 'ricker', 'morlet2', 'cwt'] - - -_msg="""scipy.signal.%s is deprecated in SciPy 1.12 and will be removed -in SciPy 1.15. We recommend using PyWavelets instead. -""" - - -def daub(p): - """ - The coefficients for the FIR low-pass filter producing Daubechies wavelets. - - .. deprecated:: 1.12.0 - - scipy.signal.daub is deprecated in SciPy 1.12 and will be removed - in SciPy 1.15. We recommend using PyWavelets instead. - - p>=1 gives the order of the zero at f=1/2. - There are 2p filter coefficients. - - Parameters - ---------- - p : int - Order of the zero at f=1/2, can have values from 1 to 34. - - Returns - ------- - daub : ndarray - Return - - """ - warnings.warn(_msg % 'daub', DeprecationWarning, stacklevel=2) - - sqrt = np.sqrt - if p < 1: - raise ValueError("p must be at least 1.") - if p == 1: - c = 1 / sqrt(2) - return np.array([c, c]) - elif p == 2: - f = sqrt(2) / 8 - c = sqrt(3) - return f * np.array([1 + c, 3 + c, 3 - c, 1 - c]) - elif p == 3: - tmp = 12 * sqrt(10) - z1 = 1.5 + sqrt(15 + tmp) / 6 - 1j * (sqrt(15) + sqrt(tmp - 15)) / 6 - z1c = np.conj(z1) - f = sqrt(2) / 8 - d0 = np.real((1 - z1) * (1 - z1c)) - a0 = np.real(z1 * z1c) - a1 = 2 * np.real(z1) - return f / d0 * np.array([a0, 3 * a0 - a1, 3 * a0 - 3 * a1 + 1, - a0 - 3 * a1 + 3, 3 - a1, 1]) - elif p < 35: - # construct polynomial and factor it - if p < 35: - P = [comb(p - 1 + k, k, exact=1) for k in range(p)][::-1] - yj = np.roots(P) - else: # try different polynomial --- needs work - P = [comb(p - 1 + k, k, exact=1) / 4.0**k - for k in range(p)][::-1] - yj = np.roots(P) / 4 - # for each root, compute two z roots, select the one with |z|>1 - # Build up final polynomial - c = np.poly1d([1, 1])**p - q = np.poly1d([1]) - for k in range(p - 1): - yval = yj[k] - part = 2 * sqrt(yval * (yval - 1)) - const = 1 - 2 * yval - z1 = const + part - if (abs(z1)) < 1: - z1 = const - part - q = q * [1, -z1] - - q = c * np.real(q) - # Normalize result - q = q / np.sum(q) * sqrt(2) - return q.c[::-1] - else: - raise ValueError("Polynomial factorization does not work " - "well for p too large.") - - -def qmf(hk): - """ - Return high-pass qmf filter from low-pass - - .. deprecated:: 1.12.0 - - scipy.signal.qmf is deprecated in SciPy 1.12 and will be removed - in SciPy 1.15. We recommend using PyWavelets instead. - - Parameters - ---------- - hk : array_like - Coefficients of high-pass filter. - - Returns - ------- - array_like - High-pass filter coefficients. - - """ - warnings.warn(_msg % 'qmf', DeprecationWarning, stacklevel=2) - - N = len(hk) - 1 - asgn = [{0: 1, 1: -1}[k % 2] for k in range(N + 1)] - return hk[::-1] * np.array(asgn) - - -def cascade(hk, J=7): - """ - Return (x, phi, psi) at dyadic points ``K/2**J`` from filter coefficients. - - .. deprecated:: 1.12.0 - - scipy.signal.cascade is deprecated in SciPy 1.12 and will be removed - in SciPy 1.15. We recommend using PyWavelets instead. - - Parameters - ---------- - hk : array_like - Coefficients of low-pass filter. - J : int, optional - Values will be computed at grid points ``K/2**J``. Default is 7. - - Returns - ------- - x : ndarray - The dyadic points ``K/2**J`` for ``K=0...N * (2**J)-1`` where - ``len(hk) = len(gk) = N+1``. - phi : ndarray - The scaling function ``phi(x)`` at `x`: - ``phi(x) = sum(hk * phi(2x-k))``, where k is from 0 to N. - psi : ndarray, optional - The wavelet function ``psi(x)`` at `x`: - ``phi(x) = sum(gk * phi(2x-k))``, where k is from 0 to N. - `psi` is only returned if `gk` is not None. - - Notes - ----- - The algorithm uses the vector cascade algorithm described by Strang and - Nguyen in "Wavelets and Filter Banks". It builds a dictionary of values - and slices for quick reuse. Then inserts vectors into final vector at the - end. - - """ - warnings.warn(_msg % 'cascade', DeprecationWarning, stacklevel=2) - - N = len(hk) - 1 - - if (J > 30 - np.log2(N + 1)): - raise ValueError("Too many levels.") - if (J < 1): - raise ValueError("Too few levels.") - - # construct matrices needed - nn, kk = np.ogrid[:N, :N] - s2 = np.sqrt(2) - # append a zero so that take works - thk = np.r_[hk, 0] - gk = qmf(hk) - tgk = np.r_[gk, 0] - - indx1 = np.clip(2 * nn - kk, -1, N + 1) - indx2 = np.clip(2 * nn - kk + 1, -1, N + 1) - m = np.empty((2, 2, N, N), 'd') - m[0, 0] = np.take(thk, indx1, 0) - m[0, 1] = np.take(thk, indx2, 0) - m[1, 0] = np.take(tgk, indx1, 0) - m[1, 1] = np.take(tgk, indx2, 0) - m *= s2 - - # construct the grid of points - x = np.arange(0, N * (1 << J), dtype=float) / (1 << J) - phi = 0 * x - - psi = 0 * x - - # find phi0, and phi1 - lam, v = eig(m[0, 0]) - ind = np.argmin(np.absolute(lam - 1)) - # a dictionary with a binary representation of the - # evaluation points x < 1 -- i.e. position is 0.xxxx - v = np.real(v[:, ind]) - # need scaling function to integrate to 1 so find - # eigenvector normalized to sum(v,axis=0)=1 - sm = np.sum(v) - if sm < 0: # need scaling function to integrate to 1 - v = -v - sm = -sm - bitdic = {'0': v / sm} - bitdic['1'] = np.dot(m[0, 1], bitdic['0']) - step = 1 << J - phi[::step] = bitdic['0'] - phi[(1 << (J - 1))::step] = bitdic['1'] - psi[::step] = np.dot(m[1, 0], bitdic['0']) - psi[(1 << (J - 1))::step] = np.dot(m[1, 1], bitdic['0']) - # descend down the levels inserting more and more values - # into bitdic -- store the values in the correct location once we - # have computed them -- stored in the dictionary - # for quicker use later. - prevkeys = ['1'] - for level in range(2, J + 1): - newkeys = ['%d%s' % (xx, yy) for xx in [0, 1] for yy in prevkeys] - fac = 1 << (J - level) - for key in newkeys: - # convert key to number - num = 0 - for pos in range(level): - if key[pos] == '1': - num += (1 << (level - 1 - pos)) - pastphi = bitdic[key[1:]] - ii = int(key[0]) - temp = np.dot(m[0, ii], pastphi) - bitdic[key] = temp - phi[num * fac::step] = temp - psi[num * fac::step] = np.dot(m[1, ii], pastphi) - prevkeys = newkeys - - return x, phi, psi - - -def morlet(M, w=5.0, s=1.0, complete=True): - """ - Complex Morlet wavelet. - - .. deprecated:: 1.12.0 - - scipy.signal.morlet is deprecated in SciPy 1.12 and will be removed - in SciPy 1.15. We recommend using PyWavelets instead. - - Parameters - ---------- - M : int - Length of the wavelet. - w : float, optional - Omega0. Default is 5 - s : float, optional - Scaling factor, windowed from ``-s*2*pi`` to ``+s*2*pi``. Default is 1. - complete : bool, optional - Whether to use the complete or the standard version. - - Returns - ------- - morlet : (M,) ndarray - - See Also - -------- - morlet2 : Implementation of Morlet wavelet, compatible with `cwt`. - scipy.signal.gausspulse - - Notes - ----- - The standard version:: - - pi**-0.25 * exp(1j*w*x) * exp(-0.5*(x**2)) - - This commonly used wavelet is often referred to simply as the - Morlet wavelet. Note that this simplified version can cause - admissibility problems at low values of `w`. - - The complete version:: - - pi**-0.25 * (exp(1j*w*x) - exp(-0.5*(w**2))) * exp(-0.5*(x**2)) - - This version has a correction - term to improve admissibility. For `w` greater than 5, the - correction term is negligible. - - Note that the energy of the return wavelet is not normalised - according to `s`. - - The fundamental frequency of this wavelet in Hz is given - by ``f = 2*s*w*r / M`` where `r` is the sampling rate. - - Note: This function was created before `cwt` and is not compatible - with it. - - Examples - -------- - >>> from scipy import signal - >>> import matplotlib.pyplot as plt - - >>> M = 100 - >>> s = 4.0 - >>> w = 2.0 - >>> wavelet = signal.morlet(M, s, w) - >>> plt.plot(wavelet.real, label="real") - >>> plt.plot(wavelet.imag, label="imag") - >>> plt.legend() - >>> plt.show() - - """ - warnings.warn(_msg % 'morlet', DeprecationWarning, stacklevel=2) - - x = np.linspace(-s * 2 * np.pi, s * 2 * np.pi, M) - output = np.exp(1j * w * x) - - if complete: - output -= np.exp(-0.5 * (w**2)) - - output *= np.exp(-0.5 * (x**2)) * np.pi**(-0.25) - - return output - - -def ricker(points, a): - """ - Return a Ricker wavelet, also known as the "Mexican hat wavelet". - - .. deprecated:: 1.12.0 - - scipy.signal.ricker is deprecated in SciPy 1.12 and will be removed - in SciPy 1.15. We recommend using PyWavelets instead. - - It models the function: - - ``A * (1 - (x/a)**2) * exp(-0.5*(x/a)**2)``, - - where ``A = 2/(sqrt(3*a)*(pi**0.25))``. - - Parameters - ---------- - points : int - Number of points in `vector`. - Will be centered around 0. - a : scalar - Width parameter of the wavelet. - - Returns - ------- - vector : (N,) ndarray - Array of length `points` in shape of ricker curve. - - Examples - -------- - >>> from scipy import signal - >>> import matplotlib.pyplot as plt - - >>> points = 100 - >>> a = 4.0 - >>> vec2 = signal.ricker(points, a) - >>> print(len(vec2)) - 100 - >>> plt.plot(vec2) - >>> plt.show() - - """ - warnings.warn(_msg % 'ricker', DeprecationWarning, stacklevel=2) - return _ricker(points, a) - def _ricker(points, a): A = 2 / (np.sqrt(3 * a) * (np.pi**0.25)) @@ -370,176 +13,6 @@ def _ricker(points, a): return total -def morlet2(M, s, w=5): - """ - Complex Morlet wavelet, designed to work with `cwt`. - - .. deprecated:: 1.12.0 - - scipy.signal.morlet2 is deprecated in SciPy 1.12 and will be removed - in SciPy 1.15. We recommend using PyWavelets instead. - - Returns the complete version of morlet wavelet, normalised - according to `s`:: - - exp(1j*w*x/s) * exp(-0.5*(x/s)**2) * pi**(-0.25) * sqrt(1/s) - - Parameters - ---------- - M : int - Length of the wavelet. - s : float - Width parameter of the wavelet. - w : float, optional - Omega0. Default is 5 - - Returns - ------- - morlet : (M,) ndarray - - See Also - -------- - morlet : Implementation of Morlet wavelet, incompatible with `cwt` - - Notes - ----- - - .. versionadded:: 1.4.0 - - This function was designed to work with `cwt`. Because `morlet2` - returns an array of complex numbers, the `dtype` argument of `cwt` - should be set to `complex128` for best results. - - Note the difference in implementation with `morlet`. - The fundamental frequency of this wavelet in Hz is given by:: - - f = w*fs / (2*s*np.pi) - - where ``fs`` is the sampling rate and `s` is the wavelet width parameter. - Similarly we can get the wavelet width parameter at ``f``:: - - s = w*fs / (2*f*np.pi) - - Examples - -------- - >>> import numpy as np - >>> from scipy import signal - >>> import matplotlib.pyplot as plt - - >>> M = 100 - >>> s = 4.0 - >>> w = 2.0 - >>> wavelet = signal.morlet2(M, s, w) - >>> plt.plot(abs(wavelet)) - >>> plt.show() - - This example shows basic use of `morlet2` with `cwt` in time-frequency - analysis: - - >>> t, dt = np.linspace(0, 1, 200, retstep=True) - >>> fs = 1/dt - >>> w = 6. - >>> sig = np.cos(2*np.pi*(50 + 10*t)*t) + np.sin(40*np.pi*t) - >>> freq = np.linspace(1, fs/2, 100) - >>> widths = w*fs / (2*freq*np.pi) - >>> cwtm = signal.cwt(sig, signal.morlet2, widths, w=w) - >>> plt.pcolormesh(t, freq, np.abs(cwtm), cmap='viridis', shading='gouraud') - >>> plt.show() - - """ - warnings.warn(_msg % 'morlet2', DeprecationWarning, stacklevel=2) - - x = np.arange(0, M) - (M - 1.0) / 2 - x = x / s - wavelet = np.exp(1j * w * x) * np.exp(-0.5 * x**2) * np.pi**(-0.25) - output = np.sqrt(1/s) * wavelet - return output - - -def cwt(data, wavelet, widths, dtype=None, **kwargs): - """ - Continuous wavelet transform. - - .. deprecated:: 1.12.0 - - scipy.signal.cwt is deprecated in SciPy 1.12 and will be removed - in SciPy 1.15. We recommend using PyWavelets instead. - - Performs a continuous wavelet transform on `data`, - using the `wavelet` function. A CWT performs a convolution - with `data` using the `wavelet` function, which is characterized - by a width parameter and length parameter. The `wavelet` function - is allowed to be complex. - - Parameters - ---------- - data : (N,) ndarray - data on which to perform the transform. - wavelet : function - Wavelet function, which should take 2 arguments. - The first argument is the number of points that the returned vector - will have (len(wavelet(length,width)) == length). - The second is a width parameter, defining the size of the wavelet - (e.g. standard deviation of a gaussian). See `ricker`, which - satisfies these requirements. - widths : (M,) sequence - Widths to use for transform. - dtype : data-type, optional - The desired data type of output. Defaults to ``float64`` if the - output of `wavelet` is real and ``complex128`` if it is complex. - - .. versionadded:: 1.4.0 - - kwargs - Keyword arguments passed to wavelet function. - - .. versionadded:: 1.4.0 - - Returns - ------- - cwt: (M, N) ndarray - Will have shape of (len(widths), len(data)). - - Notes - ----- - - .. versionadded:: 1.4.0 - - For non-symmetric, complex-valued wavelets, the input signal is convolved - with the time-reversed complex-conjugate of the wavelet data [1]. - - :: - - length = min(10 * width[ii], len(data)) - cwt[ii,:] = signal.convolve(data, np.conj(wavelet(length, width[ii], - **kwargs))[::-1], mode='same') - - References - ---------- - .. [1] S. Mallat, "A Wavelet Tour of Signal Processing (3rd Edition)", - Academic Press, 2009. - - Examples - -------- - >>> import numpy as np - >>> from scipy import signal - >>> import matplotlib.pyplot as plt - >>> t = np.linspace(-1, 1, 200, endpoint=False) - >>> sig = np.cos(2 * np.pi * 7 * t) + signal.gausspulse(t - 0.4, fc=2) - >>> widths = np.arange(1, 31) - >>> cwtmatr = signal.cwt(sig, signal.ricker, widths) - - .. note:: For cwt matrix plotting it is advisable to flip the y-axis - - >>> cwtmatr_yflip = np.flipud(cwtmatr) - >>> plt.imshow(cwtmatr_yflip, extent=[-1, 1, 1, 31], cmap='PRGn', aspect='auto', - ... vmax=abs(cwtmatr).max(), vmin=-abs(cwtmatr).max()) - >>> plt.show() - """ - warnings.warn(_msg % 'cwt', DeprecationWarning, stacklevel=2) - return _cwt(data, wavelet, widths, dtype, **kwargs) - - def _cwt(data, wavelet, widths, dtype=None, **kwargs): # Determine output type if dtype is None: diff --git a/scipy/signal/signaltools.py b/scipy/signal/signaltools.py index 310c81c93cd7..85d426f5fb26 100644 --- a/scipy/signal/signaltools.py +++ b/scipy/signal/signaltools.py @@ -9,7 +9,7 @@ 'convolve', 'convolve2d', 'fftconvolve', 'oaconvolve', 'order_filter', 'medfilt', 'medfilt2d', 'wiener', 'lfilter', 'lfiltic', 'sosfilt', 'deconvolve', 'hilbert', 'hilbert2', - 'cmplx_sort', 'unique_roots', 'invres', 'invresz', 'residue', + 'unique_roots', 'invres', 'invresz', 'residue', 'residuez', 'resample', 'resample_poly', 'detrend', 'lfilter_zi', 'sosfilt_zi', 'sosfiltfilt', 'choose_conv_method', 'filtfilt', 'decimate', 'vectorstrength', diff --git a/scipy/signal/tests/test_filter_design.py b/scipy/signal/tests/test_filter_design.py index 47d24323edbf..59b07f8e8789 100644 --- a/scipy/signal/tests/test_filter_design.py +++ b/scipy/signal/tests/test_filter_design.py @@ -2493,7 +2493,7 @@ def test_invalid(self): assert_raises(ValueError, _bessel_poly, -3) assert_raises(ValueError, _bessel_poly, 3.3) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_fs_param(self): for norm in ('phase', 'mag', 'delay'): for fs in (900, 900.1, 1234.567): diff --git a/scipy/signal/tests/test_signaltools.py b/scipy/signal/tests/test_signaltools.py index 5e7ebc1de9ef..ea7634736bd5 100644 --- a/scipy/signal/tests/test_signaltools.py +++ b/scipy/signal/tests/test_signaltools.py @@ -2441,7 +2441,7 @@ def check_filtfilt_gust(b, a, shape, axis, irlen=None): assert_allclose(zg2, zo2, rtol=1e-8, atol=1e-9) -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) def test_choose_conv_method(): for mode in ['valid', 'same', 'full']: for ndim in [1, 2]: @@ -2473,7 +2473,7 @@ def test_choose_conv_method(): assert_equal(choose_conv_method(x, h, mode=mode), 'direct') -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) def test_filtfilt_gust(): # Design a filter. z, p, k = signal.ellip(3, 0.01, 120, 0.0875, output='zpk') diff --git a/scipy/signal/tests/test_wavelets.py b/scipy/signal/tests/test_wavelets.py index e83e6918429b..7a357d2eaf4a 100644 --- a/scipy/signal/tests/test_wavelets.py +++ b/scipy/signal/tests/test_wavelets.py @@ -1,161 +1,59 @@ import numpy as np -from numpy.testing import (assert_equal, - assert_array_equal, assert_array_almost_equal, assert_array_less, assert_,) -import pytest +from numpy.testing import assert_array_equal, assert_array_almost_equal import scipy.signal._wavelets as wavelets class TestWavelets: - def test_qmf(self): - with pytest.deprecated_call(): - assert_array_equal(wavelets.qmf([1, 1]), [1, -1]) - - def test_daub(self): - with pytest.deprecated_call(): - for i in range(1, 15): - assert_equal(len(wavelets.daub(i)), i * 2) - - def test_cascade(self): - with pytest.deprecated_call(): - for J in range(1, 7): - for i in range(1, 5): - lpcoef = wavelets.daub(i) - k = len(lpcoef) - x, phi, psi = wavelets.cascade(lpcoef, J) - assert_(len(x) == len(phi) == len(psi)) - assert_equal(len(x), (k - 1) * 2 ** J) - - def test_morlet(self): - with pytest.deprecated_call(): - x = wavelets.morlet(50, 4.1, complete=True) - y = wavelets.morlet(50, 4.1, complete=False) - # Test if complete and incomplete wavelet have same lengths: - assert_equal(len(x), len(y)) - # Test if complete wavelet is less than incomplete wavelet: - assert_array_less(x, y) - - x = wavelets.morlet(10, 50, complete=False) - y = wavelets.morlet(10, 50, complete=True) - # For large widths complete and incomplete wavelets should be - # identical within numerical precision: - assert_equal(x, y) - - # miscellaneous tests: - x = np.array([1.73752399e-09 + 9.84327394e-25j, - 6.49471756e-01 + 0.00000000e+00j, - 1.73752399e-09 - 9.84327394e-25j]) - y = wavelets.morlet(3, w=2, complete=True) - assert_array_almost_equal(x, y) - - x = np.array([2.00947715e-09 + 9.84327394e-25j, - 7.51125544e-01 + 0.00000000e+00j, - 2.00947715e-09 - 9.84327394e-25j]) - y = wavelets.morlet(3, w=2, complete=False) - assert_array_almost_equal(x, y, decimal=2) - - x = wavelets.morlet(10000, s=4, complete=True) - y = wavelets.morlet(20000, s=8, complete=True)[5000:15000] - assert_array_almost_equal(x, y, decimal=2) - - x = wavelets.morlet(10000, s=4, complete=False) - assert_array_almost_equal(y, x, decimal=2) - y = wavelets.morlet(20000, s=8, complete=False)[5000:15000] - assert_array_almost_equal(x, y, decimal=2) - - x = wavelets.morlet(10000, w=3, s=5, complete=True) - y = wavelets.morlet(20000, w=3, s=10, complete=True)[5000:15000] - assert_array_almost_equal(x, y, decimal=2) - - x = wavelets.morlet(10000, w=3, s=5, complete=False) - assert_array_almost_equal(y, x, decimal=2) - y = wavelets.morlet(20000, w=3, s=10, complete=False)[5000:15000] - assert_array_almost_equal(x, y, decimal=2) - - x = wavelets.morlet(10000, w=7, s=10, complete=True) - y = wavelets.morlet(20000, w=7, s=20, complete=True)[5000:15000] - assert_array_almost_equal(x, y, decimal=2) - - x = wavelets.morlet(10000, w=7, s=10, complete=False) - assert_array_almost_equal(x, y, decimal=2) - y = wavelets.morlet(20000, w=7, s=20, complete=False)[5000:15000] - assert_array_almost_equal(x, y, decimal=2) - - def test_morlet2(self): - with pytest.deprecated_call(): - w = wavelets.morlet2(1.0, 0.5) - expected = (np.pi**(-0.25) * np.sqrt(1/0.5)).astype(complex) - assert_array_equal(w, expected) - - lengths = [5, 11, 15, 51, 101] - for length in lengths: - w = wavelets.morlet2(length, 1.0) - assert_(len(w) == length) - max_loc = np.argmax(w) - assert_(max_loc == (length // 2)) - - points = 100 - w = abs(wavelets.morlet2(points, 2.0)) - half_vec = np.arange(0, points // 2) - assert_array_almost_equal(w[half_vec], w[-(half_vec + 1)]) - - x = np.array([5.03701224e-09 + 2.46742437e-24j, - 1.88279253e+00 + 0.00000000e+00j, - 5.03701224e-09 - 2.46742437e-24j]) - y = wavelets.morlet2(3, s=1/(2*np.pi), w=2) - assert_array_almost_equal(x, y) - def test_ricker(self): - with pytest.deprecated_call(): - w = wavelets.ricker(1.0, 1) - expected = 2 / (np.sqrt(3 * 1.0) * (np.pi ** 0.25)) - assert_array_equal(w, expected) - - lengths = [5, 11, 15, 51, 101] - for length in lengths: - w = wavelets.ricker(length, 1.0) - assert_(len(w) == length) - max_loc = np.argmax(w) - assert_(max_loc == (length // 2)) - - points = 100 - w = wavelets.ricker(points, 2.0) - half_vec = np.arange(0, points // 2) - #Wavelet should be symmetric - assert_array_almost_equal(w[half_vec], w[-(half_vec + 1)]) - - #Check zeros - aas = [5, 10, 15, 20, 30] - points = 99 - for a in aas: - w = wavelets.ricker(points, a) - vec = np.arange(0, points) - (points - 1.0) / 2 - exp_zero1 = np.argmin(np.abs(vec - a)) - exp_zero2 = np.argmin(np.abs(vec + a)) - assert_array_almost_equal(w[exp_zero1], 0) - assert_array_almost_equal(w[exp_zero2], 0) + w = wavelets._ricker(1.0, 1) + expected = 2 / (np.sqrt(3 * 1.0) * (np.pi ** 0.25)) + assert_array_equal(w, expected) + + lengths = [5, 11, 15, 51, 101] + for length in lengths: + w = wavelets._ricker(length, 1.0) + assert len(w) == length + max_loc = np.argmax(w) + assert max_loc == (length // 2) + + points = 100 + w = wavelets._ricker(points, 2.0) + half_vec = np.arange(0, points // 2) + # Wavelet should be symmetric + assert_array_almost_equal(w[half_vec], w[-(half_vec + 1)]) + + # Check zeros + aas = [5, 10, 15, 20, 30] + points = 99 + for a in aas: + w = wavelets._ricker(points, a) + vec = np.arange(0, points) - (points - 1.0) / 2 + exp_zero1 = np.argmin(np.abs(vec - a)) + exp_zero2 = np.argmin(np.abs(vec + a)) + assert_array_almost_equal(w[exp_zero1], 0) + assert_array_almost_equal(w[exp_zero2], 0) def test_cwt(self): - with pytest.deprecated_call(): - widths = [1.0] - def delta_wavelet(s, t): - return np.array([1]) - len_data = 100 - test_data = np.sin(np.pi * np.arange(0, len_data) / 10.0) - - #Test delta function input gives same data as output - cwt_dat = wavelets.cwt(test_data, delta_wavelet, widths) - assert_(cwt_dat.shape == (len(widths), len_data)) - assert_array_almost_equal(test_data, cwt_dat.flatten()) - - #Check proper shape on output - widths = [1, 3, 4, 5, 10] - cwt_dat = wavelets.cwt(test_data, wavelets.ricker, widths) - assert_(cwt_dat.shape == (len(widths), len_data)) - - widths = [len_data * 10] - #Note: this wavelet isn't defined quite right, but is fine for this test - def flat_wavelet(l, w): - return np.full(w, 1 / w) - cwt_dat = wavelets.cwt(test_data, flat_wavelet, widths) - assert_array_almost_equal(cwt_dat, np.mean(test_data)) + widths = [1.0] + def delta_wavelet(s, t): + return np.array([1]) + len_data = 100 + test_data = np.sin(np.pi * np.arange(0, len_data) / 10.0) + + # Test delta function input gives same data as output + cwt_dat = wavelets._cwt(test_data, delta_wavelet, widths) + assert cwt_dat.shape == (len(widths), len_data) + assert_array_almost_equal(test_data, cwt_dat.flatten()) + + # Check proper shape on output + widths = [1, 3, 4, 5, 10] + cwt_dat = wavelets._cwt(test_data, wavelets._ricker, widths) + assert cwt_dat.shape == (len(widths), len_data) + + widths = [len_data * 10] + # Note: this wavelet isn't defined quite right, but is fine for this test + def flat_wavelet(l, w): + return np.full(w, 1 / w) + cwt_dat = wavelets._cwt(test_data, flat_wavelet, widths) + assert_array_almost_equal(cwt_dat, np.mean(test_data)) diff --git a/scipy/signal/wavelets.py b/scipy/signal/wavelets.py index d29deac58eb9..fc897a248353 100644 --- a/scipy/signal/wavelets.py +++ b/scipy/signal/wavelets.py @@ -4,10 +4,7 @@ from scipy._lib.deprecation import _sub_module_deprecation -__all__ = [ # noqa: F822 - 'daub', 'qmf', 'cascade', 'morlet', 'ricker', 'morlet2', 'cwt', - 'convolve' -] +__all__: list[str] = [] def __dir__(): diff --git a/scipy/sparse/_base.py b/scipy/sparse/_base.py index 510f7565065d..3f6531e6f4b1 100644 --- a/scipy/sparse/_base.py +++ b/scipy/sparse/_base.py @@ -4,7 +4,7 @@ from ._sputils import (asmatrix, check_reshape_kwargs, check_shape, get_sum_dtype, isdense, isscalarlike, - matrix, validateaxis,) + matrix, validateaxis, getdtype) from ._matrix import spmatrix @@ -110,7 +110,7 @@ def _lil_container(self): from ._lil import lil_array return lil_array - def __init__(self, arg1, maxprint=MAXPRINT): + def __init__(self, arg1, *, maxprint=None): self._shape = None if self.__class__.__name__ == '_spbase': raise ValueError("This class is not intended" @@ -119,7 +119,7 @@ def __init__(self, arg1, maxprint=MAXPRINT): raise ValueError( "scipy sparse array classes do not support instantiation from a scalar" ) - self.maxprint = maxprint + self.maxprint = MAXPRINT if maxprint is None else maxprint @property def shape(self): @@ -217,7 +217,7 @@ def astype(self, dtype, casting='unsafe', copy=True): this array/matrix do not share any memory. """ - dtype = np.dtype(dtype) + dtype = getdtype(dtype) if self.dtype != dtype: return self.tocsr().astype( dtype, casting=casting, copy=copy).asformat(self.format) @@ -263,26 +263,71 @@ def _getmaxprint(self): """Maximum number of elements to display when printed.""" return self.maxprint - def count_nonzero(self): + def count_nonzero(self, axis=None): """Number of non-zero entries, equivalent to - np.count_nonzero(a.toarray()) + np.count_nonzero(a.toarray(), axis=axis) Unlike the nnz property, which return the number of stored entries (the length of the data attribute), this method counts the actual number of non-zero entries in data. + + Duplicate entries are summed before counting. + + Parameters + ---------- + axis : {-2, -1, 0, 1, None} optional + Count nonzeros for the whole array, or along a specified axis. + + .. versionadded:: 1.15.0 + + Returns + ------- + numpy array + A reduced array (no axis `axis`) holding the number of nonzero values + for each of the indices of the nonaxis dimensions. + + Notes + ----- + If you want to count nonzero and explicit zero stored values (e.g. nnz) + along an axis, two fast idioms are provided by `numpy` functions for the + common CSR, CSC, COO formats. + + For the major axis in CSR (rows) and CSC (cols) use `np.diff`: + + >>> import numpy as np + >>> import scipy as sp + >>> A = sp.sparse.csr_array([[4, 5, 0], [7, 0, 0]]) + >>> major_axis_stored_values = np.diff(A.indptr) # -> np.array([2, 1]) + + For the minor axis in CSR (cols) and CSC (rows) use `numpy.bincount` with + minlength ``A.shape[1]`` for CSR and ``A.shape[0]`` for CSC: + + >>> csr_minor_stored_values = np.bincount(A.indices, minlength=A.shape[1]) + + For COO, use the minor axis approach for either `axis`: + + >>> A = A.tocoo() + >>> coo_axis0_stored_values = np.bincount(A.coords[0], minlength=A.shape[1]) + >>> coo_axis1_stored_values = np.bincount(A.coords[1], minlength=A.shape[0]) + + Examples + -------- + + >>> A = sp.sparse.csr_array([[4, 5, 0], [7, 0, 0]]) + >>> A.count_nonzero(axis=0) + array([2, 1, 0]) """ - raise NotImplementedError("count_nonzero not implemented for %s." % - self.__class__.__name__) + clsname = self.__class__.__name__ + raise NotImplementedError(f"count_nonzero not implemented for {clsname}.") def _getnnz(self, axis=None): """Number of stored values, including explicit zeros. Parameters ---------- - axis : None, 0, or 1 - Select between the number of values across the whole array, in - each column, or in each row. + axis : {-2, -1, 0, 1, None} optional + Report stored values for the whole array, or along a specified axis. See also -------- @@ -902,7 +947,7 @@ def _getrow(self, i): def todense(self, order=None, out=None): """ - Return a dense representation of this sparse array/matrix. + Return a dense representation of this sparse array. Parameters ---------- @@ -914,21 +959,19 @@ def todense(self, order=None, out=None): argument. out : ndarray, 2-D, optional - If specified, uses this array (or `numpy.matrix`) as the - output buffer instead of allocating a new array to - return. The provided array must have the same shape and - dtype as the sparse array/matrix on which you are calling the - method. + If specified, uses this array as the output buffer + instead of allocating a new array to return. The + provided array must have the same shape and dtype as + the sparse array on which you are calling the method. Returns ------- - arr : numpy.matrix, 2-D - A NumPy matrix object with the same shape and containing - the same data represented by the sparse array/matrix, with the - requested memory order. If `out` was passed and was an - array (rather than a `numpy.matrix`), it will be filled - with the appropriate values and returned wrapped in a - `numpy.matrix` object that shares the same memory. + arr : ndarray, 2-D + An array with the same shape and containing the same + data represented by the sparse array, with the requested + memory order. If `out` was passed, the same object is + returned after being modified in-place to contain the + appropriate values. """ return self._ascontainer(self.toarray(order=order, out=out)) diff --git a/scipy/sparse/_bsr.py b/scipy/sparse/_bsr.py index 6a8a1be7dabe..40bc949d4a93 100644 --- a/scipy/sparse/_bsr.py +++ b/scipy/sparse/_bsr.py @@ -24,8 +24,9 @@ class _bsr_base(_cs_matrix, _minmax_mixin): _format = 'bsr' - def __init__(self, arg1, shape=None, dtype=None, copy=False, blocksize=None): - _data_matrix.__init__(self, arg1) + def __init__(self, arg1, shape=None, dtype=None, copy=False, + blocksize=None, *, maxprint=None): + _data_matrix.__init__(self, arg1, maxprint=maxprint) if issparse(arg1): if arg1.format == self.format and copy: @@ -219,6 +220,15 @@ def _getnnz(self, axis=None): _getnnz.__doc__ = _spbase._getnnz.__doc__ + def count_nonzero(self, axis=None): + if axis is not None: + raise NotImplementedError( + "count_nonzero over axis is not implemented for BSR format." + ) + return np.count_nonzero(self._deduped_data()) + + count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ + def __repr__(self): _, fmt = _formats[self.format] sparse_cls = 'array' if isinstance(self, sparray) else 'matrix' diff --git a/scipy/sparse/_compressed.py b/scipy/sparse/_compressed.py index a86c09fc5edf..c471250f81bf 100644 --- a/scipy/sparse/_compressed.py +++ b/scipy/sparse/_compressed.py @@ -2,6 +2,7 @@ __all__ = [] from warnings import warn +import itertools import operator import numpy as np @@ -24,8 +25,8 @@ class _cs_matrix(_data_matrix, _minmax_mixin, IndexMixin): base array/matrix class for compressed row- and column-oriented arrays/matrices """ - def __init__(self, arg1, shape=None, dtype=None, copy=False): - _data_matrix.__init__(self, arg1) + def __init__(self, arg1, shape=None, dtype=None, copy=False, *, maxprint=None): + _data_matrix.__init__(self, arg1, maxprint=maxprint) is_array = isinstance(self, sparray) if issparse(arg1): @@ -107,7 +108,8 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False): self._shape = check_shape(self._swap((major_d, minor_d)), allow_1d=is_array) if dtype is not None: - self.data = self.data.astype(dtype, copy=False) + newdtype = getdtype(dtype) + self.data = self.data.astype(newdtype, copy=False) self.check_format(full_check=False) @@ -124,14 +126,41 @@ def _getnnz(self, axis=None): axis, _ = self._swap((axis, 1 - axis)) _, N = self._swap(self.shape) if axis == 0: - return np.bincount(downcast_intp_index(self.indices), - minlength=N) + return np.bincount(downcast_intp_index(self.indices), minlength=N) elif axis == 1: return np.diff(self.indptr) raise ValueError('axis out of bounds') _getnnz.__doc__ = _spbase._getnnz.__doc__ + def count_nonzero(self, axis=None): + self.sum_duplicates() + if axis is None: + return np.count_nonzero(self.data) + + if self.ndim == 1: + if axis not in (0, -1): + raise ValueError('axis out of bounds') + return np.count_nonzero(self.data) + + if axis < 0: + axis += 2 + axis, _ = self._swap((axis, 1 - axis)) + if axis == 0: + _, N = self._swap(self.shape) + mask = self.data != 0 + idx = self.indices if mask.all() else self.indices[mask] + return np.bincount(downcast_intp_index(idx), minlength=N) + elif axis == 1: + if self.data.all(): + return np.diff(self.indptr) + pairs = itertools.pairwise(self.indptr) + return np.array([np.count_nonzero(self.data[i:j]) for i, j in pairs]) + else: + raise ValueError('axis out of bounds') + + count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ + def check_format(self, full_check=True): """Check whether the array/matrix respects the CSR or CSC format. diff --git a/scipy/sparse/_coo.py b/scipy/sparse/_coo.py index 2f4580b3f004..39cd87fe37aa 100644 --- a/scipy/sparse/_coo.py +++ b/scipy/sparse/_coo.py @@ -24,8 +24,8 @@ class _coo_base(_data_matrix, _minmax_mixin): _format = 'coo' - def __init__(self, arg1, shape=None, dtype=None, copy=False): - _data_matrix.__init__(self, arg1) + def __init__(self, arg1, shape=None, dtype=None, copy=False, *, maxprint=None): + _data_matrix.__init__(self, arg1, maxprint=maxprint) is_array = isinstance(self, sparray) if not copy: copy = copy_if_needed @@ -94,7 +94,8 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False): self.has_canonical_format = True if dtype is not None: - self.data = self.data.astype(dtype, copy=False) + newdtype = getdtype(dtype) + self.data = self.data.astype(newdtype, copy=False) self._check() @@ -182,6 +183,21 @@ def _getnnz(self, axis=None): _getnnz.__doc__ = _spbase._getnnz.__doc__ + def count_nonzero(self, axis=None): + self.sum_duplicates() + if axis is None: + return np.count_nonzero(self.data) + + if axis < 0: + axis += self.ndim + if axis < 0 or axis >= self.ndim: + raise ValueError('axis out of bounds') + mask = self.data != 0 + coord = self.coords[1 - axis][mask] + return np.bincount(downcast_intp_index(coord), minlength=self.shape[1 - axis]) + + count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ + def _check(self): """ Checks data structure for consistency """ if self.ndim != len(self.coords): diff --git a/scipy/sparse/_data.py b/scipy/sparse/_data.py index b619eeb90bbb..9c4a2957e237 100644 --- a/scipy/sparse/_data.py +++ b/scipy/sparse/_data.py @@ -9,7 +9,7 @@ import math import numpy as np -from ._base import _spbase, _ufuncs_with_fixed_point_at_zero +from ._base import _spbase, sparray, _ufuncs_with_fixed_point_at_zero from ._sputils import isscalarlike, validateaxis __all__ = [] @@ -18,8 +18,8 @@ # TODO implement all relevant operations # use .data.__methods__() instead of /=, *=, etc. class _data_matrix(_spbase): - def __init__(self, arg1): - _spbase.__init__(self, arg1) + def __init__(self, arg1, *, maxprint=None): + _spbase.__init__(self, arg1, maxprint=maxprint) @property def dtype(self): @@ -56,8 +56,7 @@ def __imul__(self, other): # self *= other if isscalarlike(other): self.data *= other return self - else: - return NotImplemented + return NotImplemented def __itruediv__(self, other): # self /= other if isscalarlike(other): @@ -97,11 +96,6 @@ def copy(self): copy.__doc__ = _spbase.copy.__doc__ - def count_nonzero(self): - return np.count_nonzero(self._deduped_data()) - - count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ - def power(self, n, dtype=None): """ This function performs element-wise power. @@ -195,6 +189,11 @@ def _min_or_max_axis(self, axis, min_or_max): major_index = np.compress(mask, major_index) value = np.compress(mask, value) + if isinstance(self, sparray): + coords = (major_index,) + shape = (M,) + return self._coo_container((value, coords), shape=shape, dtype=self.dtype) + if axis == 0: return self._coo_container( (value, (np.zeros(len(value), dtype=idx_dtype), major_index)), @@ -267,6 +266,9 @@ def _arg_min_or_max_axis(self, axis, argmin_or_argmax, compare): else: ret[i] = zero_ind + if isinstance(self, sparray): + return ret + if axis == 1: ret = ret.reshape(-1, 1) diff --git a/scipy/sparse/_dia.py b/scipy/sparse/_dia.py index ab6d5dcdccad..c45e90cd4aeb 100644 --- a/scipy/sparse/_dia.py +++ b/scipy/sparse/_dia.py @@ -19,8 +19,8 @@ class _dia_base(_data_matrix): _format = 'dia' - def __init__(self, arg1, shape=None, dtype=None, copy=False): - _data_matrix.__init__(self, arg1) + def __init__(self, arg1, shape=None, dtype=None, copy=False, *, maxprint=None): + _data_matrix.__init__(self, arg1, maxprint=maxprint) if issparse(arg1): if arg1.format == "dia": @@ -78,7 +78,8 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False): self._shape = check_shape(A.shape) if dtype is not None: - self.data = self.data.astype(dtype) + newdtype = getdtype(dtype) + self.data = self.data.astype(newdtype) # check format if self.offsets.ndim != 1: @@ -115,10 +116,16 @@ def _data_mask(self): mask &= (offset_inds < num_cols) return mask - def count_nonzero(self): + def count_nonzero(self, axis=None): + if axis is not None: + raise NotImplementedError( + "count_nonzero over an axis is not implemented for DIA format" + ) mask = self._data_mask() return np.count_nonzero(self.data[mask]) + count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ + def _getnnz(self, axis=None): if axis is not None: raise NotImplementedError("_getnnz over an axis is not implemented " @@ -133,7 +140,6 @@ def _getnnz(self, axis=None): return int(nnz) _getnnz.__doc__ = _spbase._getnnz.__doc__ - count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ def sum(self, axis=None, dtype=None, out=None): validateaxis(axis) diff --git a/scipy/sparse/_dok.py b/scipy/sparse/_dok.py index 08a039136ff3..0d0778a5cde6 100644 --- a/scipy/sparse/_dok.py +++ b/scipy/sparse/_dok.py @@ -18,8 +18,8 @@ class _dok_base(_spbase, IndexMixin, dict): _format = 'dok' - def __init__(self, arg1, shape=None, dtype=None, copy=False): - _spbase.__init__(self, arg1) + def __init__(self, arg1, shape=None, dtype=None, copy=False, *, maxprint=None): + _spbase.__init__(self, arg1, maxprint=maxprint) is_array = isinstance(self, sparray) if isinstance(arg1, tuple) and isshape(arg1, allow_1d=is_array): @@ -37,7 +37,7 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False): self._dict = arg1._dict self._shape = check_shape(arg1.shape, allow_1d=is_array) - self.dtype = arg1.dtype + self.dtype = getdtype(arg1.dtype) else: # Dense ctor try: arg1 = np.asarray(arg1) @@ -51,11 +51,11 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False): if dtype is not None: arg1 = arg1.astype(dtype) self._dict = {i: v for i, v in enumerate(arg1) if v != 0} - self.dtype = arg1.dtype + self.dtype = getdtype(arg1.dtype) else: d = self._coo_container(arg1, dtype=dtype).todok() self._dict = d._dict - self.dtype = d.dtype + self.dtype = getdtype(d.dtype) self._shape = check_shape(arg1.shape, allow_1d=is_array) def update(self, val): @@ -69,7 +69,11 @@ def _getnnz(self, axis=None): ) return len(self._dict) - def count_nonzero(self): + def count_nonzero(self, axis=None): + if axis is not None: + raise NotImplementedError( + "count_nonzero over an axis is not implemented for DOK format." + ) return sum(x != 0 for x in self.values()) _getnnz.__doc__ = _spbase._getnnz.__doc__ diff --git a/scipy/sparse/_lil.py b/scipy/sparse/_lil.py index 2503aa628b58..4029ac0c7848 100644 --- a/scipy/sparse/_lil.py +++ b/scipy/sparse/_lil.py @@ -20,8 +20,8 @@ class _lil_base(_spbase, IndexMixin): _format = 'lil' - def __init__(self, arg1, shape=None, dtype=None, copy=False): - _spbase.__init__(self, arg1) + def __init__(self, arg1, shape=None, dtype=None, copy=False, *, maxprint=None): + _spbase.__init__(self, arg1, maxprint=maxprint) self.dtype = getdtype(dtype, arg1, default=float) # First get the shape @@ -32,7 +32,8 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False): A = arg1.tolil() if dtype is not None: - A = A.astype(dtype, copy=False) + newdtype = getdtype(dtype) + A = A.astype(newdtype, copy=False) self._shape = check_shape(A.shape) self.dtype = A.dtype @@ -62,7 +63,7 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False): A = self._csr_container(A, dtype=dtype).tolil() self._shape = check_shape(A.shape) - self.dtype = A.dtype + self.dtype = getdtype(A.dtype) self.rows = A.rows self.data = A.data @@ -106,10 +107,27 @@ def _getnnz(self, axis=None): else: raise ValueError('axis out of bounds') - def count_nonzero(self): - return sum(np.count_nonzero(rowvals) for rowvals in self.data) - _getnnz.__doc__ = _spbase._getnnz.__doc__ + + def count_nonzero(self, axis=None): + if axis is None: + return sum(np.count_nonzero(rowvals) for rowvals in self.data) + + if axis < 0: + axis += 2 + if axis == 0: + out = np.zeros(self.shape[1], dtype=np.intp) + for row, data in zip(self.rows, self.data): + mask = [c for c, d in zip(row, data) if d != 0] + out[mask] += 1 + return out + elif axis == 1: + return np.array( + [np.count_nonzero(rowvals) for rowvals in self.data], dtype=np.intp, + ) + else: + raise ValueError('axis out of bounds') + count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ def getrowview(self, i): diff --git a/scipy/sparse/_matrix.py b/scipy/sparse/_matrix.py index 1ab874942383..5350b2a767c8 100644 --- a/scipy/sparse/_matrix.py +++ b/scipy/sparse/_matrix.py @@ -111,3 +111,35 @@ def getrow(self, i): matrix (row vector). """ return self._getrow(i) + + def todense(self, order=None, out=None): + """ + Return a dense representation of this sparse matrix. + + Parameters + ---------- + order : {'C', 'F'}, optional + Whether to store multi-dimensional data in C (row-major) + or Fortran (column-major) order in memory. The default + is 'None', which provides no ordering guarantees. + Cannot be specified in conjunction with the `out` + argument. + + out : ndarray, 2-D, optional + If specified, uses this array (or `numpy.matrix`) as the + output buffer instead of allocating a new array to + return. The provided array must have the same shape and + dtype as the sparse matrix on which you are calling the + method. + + Returns + ------- + arr : numpy.matrix, 2-D + A NumPy matrix object with the same shape and containing + the same data represented by the sparse matrix, with the + requested memory order. If `out` was passed and was an + array (rather than a `numpy.matrix`), it will be filled + with the appropriate values and returned wrapped in a + `numpy.matrix` object that shares the same memory. + """ + return super().todense(order, out) diff --git a/scipy/sparse/_sputils.py b/scipy/sparse/_sputils.py index fa515606006d..79fd38260189 100644 --- a/scipy/sparse/_sputils.py +++ b/scipy/sparse/_sputils.py @@ -126,11 +126,12 @@ def getdtype(dtype, a=None, default=None): raise TypeError("could not interpret data type") from e else: newdtype = np.dtype(dtype) - if newdtype == np.object_: - raise ValueError( - "object dtype is not supported by sparse matrices" - ) + if newdtype not in supported_dtypes: + supported_dtypes_fmt = ", ".join(t.__name__ for t in supported_dtypes) + raise ValueError(f"scipy.sparse does not support dtype {newdtype.name}. " + f"The only supported types are: {supported_dtypes_fmt}.") + return newdtype @@ -390,14 +391,21 @@ def is_pydata_spmatrix(m) -> bool: def convert_pydata_sparse_to_scipy( - arg: Any, target_format: Optional[Literal["csc", "csr"]] = None + arg: Any, + target_format: Optional[Literal["csc", "csr"]] = None, + accept_fv: Any = None, ) -> Union[Any, "sp.spmatrix"]: """ Convert a pydata/sparse array to scipy sparse matrix, pass through anything else. """ if is_pydata_spmatrix(arg): - arg = arg.to_scipy_sparse() + # The `accept_fv` keyword is new in PyData Sparse 0.15.4 (May 2024), + # remove the `except` once the minimum supported version is >=0.15.4 + try: + arg = arg.to_scipy_sparse(accept_fv=accept_fv) + except TypeError: + arg = arg.to_scipy_sparse() if target_format is not None: arg = arg.asformat(target_format) elif arg.format not in ("csc", "csr"): diff --git a/scipy/sparse/csgraph/_flow.pyx b/scipy/sparse/csgraph/_flow.pyx index 45656eaed3a8..c2ca11719e55 100644 --- a/scipy/sparse/csgraph/_flow.pyx +++ b/scipy/sparse/csgraph/_flow.pyx @@ -377,13 +377,13 @@ def _make_tails(a): cdef ITYPE_t[:] _edmonds_karp( - ITYPE_t[:] edge_ptr, - ITYPE_t[:] tails, - ITYPE_t[:] heads, - ITYPE_t[:] capacities, - ITYPE_t[:] rev_edge_ptr, - ITYPE_t source, - ITYPE_t sink) noexcept: + const ITYPE_t[:] edge_ptr, + const ITYPE_t[:] tails, + const ITYPE_t[:] heads, + const ITYPE_t[:] capacities, + const ITYPE_t[:] rev_edge_ptr, + const ITYPE_t source, + const ITYPE_t sink) noexcept: """Solves the maximum flow problem using the Edmonds--Karp algorithm. This assumes that for every edge in the graph, the edge in the opposite @@ -619,12 +619,12 @@ cdef bint _augment_paths( progress[current] += 1 cdef ITYPE_t[:] _dinic( - ITYPE_t[:] edge_ptr, - ITYPE_t[:] heads, + const ITYPE_t[:] edge_ptr, + const ITYPE_t[:] heads, ITYPE_t[:] capacities, - ITYPE_t[:] rev_edge_ptr, - ITYPE_t source, - ITYPE_t sink) noexcept: + const ITYPE_t[:] rev_edge_ptr, + const ITYPE_t source, + const ITYPE_t sink) noexcept: """Solves the maximum flow problem using the Dinic's algorithm. This assumes that for every edge in the graph, the edge in the opposite diff --git a/scipy/sparse/csgraph/_matching.pyx b/scipy/sparse/csgraph/_matching.pyx index e1145fde1115..758d00dbfa74 100644 --- a/scipy/sparse/csgraph/_matching.pyx +++ b/scipy/sparse/csgraph/_matching.pyx @@ -147,8 +147,8 @@ def maximum_bipartite_matching(graph, perm_type='row'): @cython.boundscheck(False) @cython.wraparound(False) -cdef tuple _hopcroft_karp(ITYPE_t[:] indices, ITYPE_t[:] indptr, - ITYPE_t i, ITYPE_t j): +cdef tuple _hopcroft_karp(const ITYPE_t[:] indices, const ITYPE_t[:] indptr, + const ITYPE_t i, const ITYPE_t j): cdef ITYPE_t INF = np.iinfo(ITYPE).max # x will end up containing the matchings of rows to columns, while # y will contain the matchings of columns to rows. @@ -528,8 +528,8 @@ ctypedef np.uint8_t BTYPE_t cdef ITYPE_t[:] _lapjvsp(ITYPE_t[:] first, ITYPE_t[:] kk, DTYPE_t[:] cc, - ITYPE_t nr, - ITYPE_t nc) noexcept: + const ITYPE_t nr, + const ITYPE_t nc) noexcept: """Solves the minimum weight bipartite matching problem using LAPJVsp. The implementation at hand is a straightforward port of the original Pascal diff --git a/scipy/sparse/csgraph/_min_spanning_tree.pyx b/scipy/sparse/csgraph/_min_spanning_tree.pyx index a1eaa56ddeaa..41b536b19ecc 100644 --- a/scipy/sparse/csgraph/_min_spanning_tree.pyx +++ b/scipy/sparse/csgraph/_min_spanning_tree.pyx @@ -93,10 +93,11 @@ def minimum_spanning_tree(csgraph, overwrite=False): [0, 0, 0, 0]]) """ global NULL_IDX - + is_pydata_sparse = is_pydata_spmatrix(csgraph) if is_pydata_sparse: pydata_sparse_cls = csgraph.__class__ + pydata_sparse_fill_value = csgraph.fill_value csgraph = validate_graph(csgraph, True, DTYPE, dense_output=False, copy_if_sparse=not overwrite) cdef int N = csgraph.shape[0] @@ -120,16 +121,23 @@ def minimum_spanning_tree(csgraph, overwrite=False): sp_tree.eliminate_zeros() if is_pydata_sparse: - sp_tree = pydata_sparse_cls.from_scipy_sparse(sp_tree) + # The `fill_value` keyword is new in PyData Sparse 0.15.4 (May 2024), + # remove the `except` once the minimum supported version is >=0.15.4 + try: + sp_tree = pydata_sparse_cls.from_scipy_sparse( + sp_tree, fill_value=pydata_sparse_fill_value + ) + except TypeError: + sp_tree = pydata_sparse_cls.from_scipy_sparse(sp_tree) return sp_tree @cython.boundscheck(False) @cython.wraparound(False) cdef void _min_spanning_tree(DTYPE_t[::1] data, - ITYPE_t[::1] col_indices, - ITYPE_t[::1] indptr, - ITYPE_t[::1] i_sort, + const ITYPE_t[::1] col_indices, + const ITYPE_t[::1] indptr, + const ITYPE_t[::1] i_sort, ITYPE_t[::1] row_indices, ITYPE_t[::1] predecessors, ITYPE_t[::1] rank) noexcept nogil: @@ -139,13 +147,13 @@ cdef void _min_spanning_tree(DTYPE_t[::1] data, cdef unsigned int i, j, V1, V2, R1, R2, n_edges_in_mst, n_verts, n_data n_verts = predecessors.shape[0] n_data = i_sort.shape[0] - + # Arrange `row_indices` to contain the row index of each value in `data`. # Note that the array `col_indices` already contains the column index. for i in range(n_verts): for j in range(indptr[i], indptr[i + 1]): row_indices[j] = i - + # step through the edges from smallest to largest. # V1 and V2 are connected vertices. n_edges_in_mst = 0 @@ -168,13 +176,13 @@ cdef void _min_spanning_tree(DTYPE_t[::1] data, predecessors[V1] = R1 while predecessors[V2] != R2: predecessors[V2] = R2 - + # if the subtrees are different, then we connect them and keep the # edge. Otherwise, we remove the edge: it duplicates one already # in the spanning tree. if R1 != R2: n_edges_in_mst += 1 - + # Use approximate (because of path-compression) rank to try # to keep balanced trees. if rank[R1] > rank[R2]: @@ -186,12 +194,11 @@ cdef void _min_spanning_tree(DTYPE_t[::1] data, rank[R1] += 1 else: data[j] = 0 - + i += 1 - + # We may have stopped early if we found a full-sized MST so zero out the rest while i < n_data: j = i_sort[i] data[j] = 0 i += 1 - diff --git a/scipy/sparse/csgraph/_shortest_path.pyx b/scipy/sparse/csgraph/_shortest_path.pyx index 4712f9ff3844..4054ca2e7f89 100644 --- a/scipy/sparse/csgraph/_shortest_path.pyx +++ b/scipy/sparse/csgraph/_shortest_path.pyx @@ -76,14 +76,14 @@ def shortest_path(csgraph, method='auto', 'BF' -- Bellman-Ford algorithm. This algorithm can be used when weights are negative. If a negative cycle is encountered, an error will be raised. - Computational cost is approximately ``O[N(N^2 k)]``, where - ``k`` is the average number of connected edges per node. + Computational cost is approximately ``O[N(N^2 k)]``, where + ``k`` is the average number of connected edges per node. The input csgraph will be converted to a csr representation. 'J' -- Johnson's algorithm. - Like the Bellman-Ford algorithm, Johnson's algorithm is - designed for use when the weights are negative. It combines - the Bellman-Ford algorithm with Dijkstra's algorithm for + Like the Bellman-Ford algorithm, Johnson's algorithm is + designed for use when the weights are negative. It combines + the Bellman-Ford algorithm with Dijkstra's algorithm for faster computation. directed : bool, optional @@ -161,6 +161,8 @@ def shortest_path(csgraph, method='auto', array([-9999, 0, 0, 1], dtype=int32) """ + csgraph = convert_pydata_sparse_to_scipy(csgraph, accept_fv=[0, np.inf, np.nan]) + # validate here to catch errors early but don't store the result; # we'll validate again later validate_graph(csgraph, directed, DTYPE, @@ -548,7 +550,10 @@ def dijkstra(csgraph, directed=True, indices=None, # initialize/validate indices if indices is None: indices = np.arange(N, dtype=ITYPE) - return_shape = indices.shape + (N,) + if min_only: + return_shape = (N,) + else: + return_shape = indices.shape + (N,) else: indices = np.array(indices, order='C', dtype=ITYPE, copy=True) if min_only: @@ -596,16 +601,22 @@ def dijkstra(csgraph, directed=True, indices=None, else: csr_data = csgraph.data + csgraph_indices = csgraph.indices + csgraph_indptr = csgraph.indptr + if csgraph_indices.dtype != ITYPE: + csgraph_indices = csgraph_indices.astype(ITYPE) + if csgraph_indptr.dtype != ITYPE: + csgraph_indptr = csgraph_indptr.astype(ITYPE) if directed: if min_only: _dijkstra_directed_multi(indices, - csr_data, csgraph.indices, - csgraph.indptr, + csr_data, csgraph_indices, + csgraph_indptr, dist_matrix, predecessor_matrix, source_matrix, limitf) else: _dijkstra_directed(indices, - csr_data, csgraph.indices, csgraph.indptr, + csr_data, csgraph_indices, csgraph_indptr, dist_matrix, predecessor_matrix, limitf) else: csgraphT = csgraph.T.tocsr() @@ -615,15 +626,15 @@ def dijkstra(csgraph, directed=True, indices=None, csrT_data = csgraphT.data if min_only: _dijkstra_undirected_multi(indices, - csr_data, csgraph.indices, - csgraph.indptr, + csr_data, csgraph_indices, + csgraph_indptr, csrT_data, csgraphT.indices, csgraphT.indptr, dist_matrix, predecessor_matrix, source_matrix, limitf) else: _dijkstra_undirected(indices, - csr_data, csgraph.indices, csgraph.indptr, + csr_data, csgraph_indices, csgraph_indptr, csrT_data, csgraphT.indices, csgraphT.indptr, dist_matrix, predecessor_matrix, limitf) @@ -884,13 +895,13 @@ cdef int _dijkstra_undirected( @cython.boundscheck(False) cdef int _dijkstra_undirected_multi( - int[:] source_indices, - double[:] csr_weights, - int[:] csr_indices, - int[:] csr_indptr, - double[:] csrT_weights, - int[:] csrT_indices, - int[:] csrT_indptr, + const int[:] source_indices, + const double[:] csr_weights, + const int[:] csr_indices, + const int[:] csr_indptr, + const double[:] csrT_weights, + const int[:] csrT_indices, + const int[:] csrT_indptr, double[:] dist_matrix, int[:] pred, int[:] sources, @@ -1346,9 +1357,9 @@ def johnson(csgraph, directed=True, indices=None, cdef void _johnson_add_weights( double[:] csr_weights, - int[:] csr_indices, - int[:] csr_indptr, - double[:] dist_array) noexcept: + const int[:] csr_indices, + const int[:] csr_indptr, + const double[:] dist_array) noexcept: # let w(u, v) = w(u, v) + h(u) - h(v) cdef unsigned int j, k, N = dist_array.shape[0] @@ -1495,7 +1506,7 @@ cdef void add_sibling(FibonacciNode* node, FibonacciNode* new_sibling) noexcept: # Assumptions: - node is a valid pointer # - new_sibling is a valid pointer # - new_sibling is not the child or sibling of another node - + # Insert new_sibling between node and node.right_sibling if node.right_sibling: node.right_sibling.left_sibling = new_sibling @@ -1547,7 +1558,7 @@ cdef void insert_node(FibonacciHeap* heap, # - node is not the child or sibling of another node if heap.min_node: if node.val < heap.min_node.val: - # Replace heap.min_node with node, which is always + # Replace heap.min_node with node, which is always # at the leftmost end of the roots' linked-list. node.left_sibling = NULL node.right_sibling = heap.min_node @@ -1572,7 +1583,7 @@ cdef void decrease_val(FibonacciHeap* heap, remove(node) insert_node(heap, node) elif heap.min_node.val > node.val: - # Replace heap.min_node with node, which is always + # Replace heap.min_node with node, which is always # at the leftmost end of the roots' linked-list. remove(node) node.right_sibling = heap.min_node @@ -1626,7 +1637,7 @@ cdef FibonacciNode* remove_min(FibonacciHeap* heap) noexcept: temp = heap.min_node.right_sibling remove(heap.min_node) heap.min_node = temp - + if temp == NULL: # There is a unique root in the tree, hence a unique node # which is the minimum that we return here. @@ -1642,7 +1653,7 @@ cdef FibonacciNode* remove_min(FibonacciHeap* heap) noexcept: temp_right = temp.right_sibling link(heap, temp) temp = temp_right - + # move heap.min_node to the leftmost end of the linked-list of roots temp = leftmost_sibling(heap.min_node) if heap.min_node != temp: @@ -2022,7 +2033,7 @@ cdef void _yen( if ( total_distance != INFINITY and _yen_is_path_in_candidates(candidate_predecessors, - shortest_paths_predecessors[k-1], + shortest_paths_predecessors[k-1], predecessor_matrix[0], spur_node, sink) == 0 ): diff --git a/scipy/sparse/csgraph/_tools.pyx b/scipy/sparse/csgraph/_tools.pyx index 50dc61356fe3..6740865813b6 100644 --- a/scipy/sparse/csgraph/_tools.pyx +++ b/scipy/sparse/csgraph/_tools.pyx @@ -472,6 +472,7 @@ def reconstruct_path(csgraph, predecessors, directed=True): is_pydata_sparse = is_pydata_spmatrix(csgraph) if is_pydata_sparse: pydata_sparse_cls = csgraph.__class__ + pydata_sparse_fill_value = csgraph.fill_value csgraph = validate_graph(csgraph, directed, dense_output=False) N = csgraph.shape[0] @@ -502,7 +503,14 @@ def reconstruct_path(csgraph, predecessors, directed=True): sctree = csr_matrix((data, indices, indptr), shape=(N, N)) if is_pydata_sparse: - sctree = pydata_sparse_cls.from_scipy_sparse(sctree) + # The `fill_value` keyword is new in PyData Sparse 0.15.4 (May 2024), + # remove the `except` once the minimum supported version is >=0.15.4 + try: + sctree = pydata_sparse_cls.from_scipy_sparse( + sctree, fill_value=pydata_sparse_fill_value + ) + except TypeError: + sctree = pydata_sparse_cls.from_scipy_sparse(sctree) return sctree diff --git a/scipy/sparse/csgraph/_validation.py b/scipy/sparse/csgraph/_validation.py index e160cf5e7b0e..27c30f5347a0 100644 --- a/scipy/sparse/csgraph/_validation.py +++ b/scipy/sparse/csgraph/_validation.py @@ -18,7 +18,12 @@ def validate_graph(csgraph, directed, dtype=DTYPE, if not (csr_output or dense_output): raise ValueError("Internal: dense or csr output must be true") - csgraph = convert_pydata_sparse_to_scipy(csgraph) + accept_fv = [null_value_in] + if infinity_null: + accept_fv.append(np.inf) + if nan_null: + accept_fv.append(np.nan) + csgraph = convert_pydata_sparse_to_scipy(csgraph, accept_fv=accept_fv) # if undirected and csc storage, then transposing in-place # is quicker than later converting to csr. diff --git a/scipy/sparse/csgraph/tests/test_pydata_sparse.py b/scipy/sparse/csgraph/tests/test_pydata_sparse.py index 63ed5f61a430..025aeb67c1f7 100644 --- a/scipy/sparse/csgraph/tests/test_pydata_sparse.py +++ b/scipy/sparse/csgraph/tests/test_pydata_sparse.py @@ -3,6 +3,7 @@ import numpy as np import scipy.sparse as sp import scipy.sparse.csgraph as spgraph +from scipy._lib import _pep440 from numpy.testing import assert_equal @@ -22,6 +23,15 @@ pytest.param("DOK", marks=[pytest.mark.xfail(reason=msg)])) +def check_sparse_version(min_ver): + if sparse is None: + return pytest.mark.skip(reason="sparse is not installed") + return pytest.mark.skipif( + _pep440.parse(sparse.__version__) < _pep440.Version(min_ver), + reason=f"sparse version >= {min_ver} required" + ) + + @pytest.fixture(params=sparse_params) def sparse_cls(request): return getattr(sparse, request.param) @@ -147,3 +157,38 @@ def test_min_weight_full_bipartite_matching(graphs): desired = func(sp.csc_matrix(A_dense)[0:2, 1:3]) assert_equal(actual, desired) + + +@check_sparse_version("0.15.4") +@pytest.mark.parametrize( + "func", + [ + spgraph.shortest_path, + spgraph.dijkstra, + spgraph.floyd_warshall, + spgraph.bellman_ford, + spgraph.johnson, + spgraph.minimum_spanning_tree, + ] +) +@pytest.mark.parametrize( + "fill_value, comp_func", + [(np.inf, np.isposinf), (np.nan, np.isnan)], +) +def test_nonzero_fill_value(graphs, func, fill_value, comp_func): + A_dense, A_sparse = graphs + A_sparse = A_sparse.astype(float) + A_sparse.fill_value = fill_value + sparse_cls = type(A_sparse) + + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + + if func == spgraph.minimum_spanning_tree: + assert isinstance(actual, sparse_cls) + assert comp_func(actual.fill_value) + actual = actual.todense() + actual[comp_func(actual)] = 0.0 + assert_equal(actual, desired.todense()) + else: + assert_equal(actual, desired) diff --git a/scipy/sparse/csgraph/tests/test_shortest_path.py b/scipy/sparse/csgraph/tests/test_shortest_path.py index 046df0186948..45600352e8a7 100644 --- a/scipy/sparse/csgraph/tests/test_shortest_path.py +++ b/scipy/sparse/csgraph/tests/test_shortest_path.py @@ -33,10 +33,13 @@ directed_2SP_0_to_3 = [[-9999, 0, -9999, 1, -9999], [-9999, 0, -9999, 4, 1]] -directed_sparse_zero_G = scipy.sparse.csr_matrix(([0, 1, 2, 3, 1], - ([0, 1, 2, 3, 4], - [1, 2, 0, 4, 3])), - shape = (5, 5)) +directed_sparse_zero_G = scipy.sparse.csr_matrix( + ( + [0, 1, 2, 3, 1], + ([0, 1, 2, 3, 4], [1, 2, 0, 4, 3]), + ), + shape=(5, 5), +) directed_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf], [3, 0, 1, np.inf, np.inf], @@ -44,10 +47,13 @@ [np.inf, np.inf, np.inf, 0, 3], [np.inf, np.inf, np.inf, 1, 0]] -undirected_sparse_zero_G = scipy.sparse.csr_matrix(([0, 0, 1, 1, 2, 2, 1, 1], - ([0, 1, 1, 2, 2, 0, 3, 4], - [1, 0, 2, 1, 0, 2, 4, 3])), - shape = (5, 5)) +undirected_sparse_zero_G = scipy.sparse.csr_matrix( + ( + [0, 0, 1, 1, 2, 2, 1, 1], + ([0, 1, 1, 2, 2, 0, 3, 4], [1, 0, 2, 1, 0, 2, 4, 3]) + ), + shape=(5, 5), +) undirected_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf], [0, 0, 1, np.inf, np.inf], @@ -452,3 +458,27 @@ def test_yen_negative_weights(): K=1, ) assert_allclose(distances, [-2.]) + + +@pytest.mark.parametrize("min_only", (True, False)) +@pytest.mark.parametrize("directed", (True, False)) +@pytest.mark.parametrize("return_predecessors", (True, False)) +@pytest.mark.parametrize("index_dtype", (np.int32, np.int64)) +@pytest.mark.parametrize("indices", (None, [1])) +def test_20904(min_only, directed, return_predecessors, index_dtype, indices): + """Test two failures from gh-20904: int32 and indices-as-None.""" + adj_mat = scipy.sparse.eye(4, format="csr") + adj_mat = scipy.sparse.csr_array( + ( + adj_mat.data, + adj_mat.indices.astype(index_dtype), + adj_mat.indptr.astype(index_dtype), + ), + ) + dijkstra( + adj_mat, + directed, + indices=indices, + min_only=min_only, + return_predecessors=return_predecessors, + ) diff --git a/scipy/sparse/linalg/_dsolve/_superlumodule.c b/scipy/sparse/linalg/_dsolve/_superlumodule.c index 1bcbd7544b39..65359a93f32c 100644 --- a/scipy/sparse/linalg/_dsolve/_superlumodule.c +++ b/scipy/sparse/linalg/_dsolve/_superlumodule.c @@ -17,6 +17,7 @@ #include #include "_superluobject.h" +#include "SuperLU/SRC/superlu_enum_consts.h" /* @@ -263,6 +264,179 @@ static PyObject *Py_gstrf(PyObject * self, PyObject * args, return NULL; } +static PyObject *Py_gstrs(PyObject * self, PyObject * args, + PyObject * keywds) +{ + /* compressed sparse column matrix L */ + int L_N = 0, L_nnz = 0; + PyArrayObject *L_nzvals = NULL, *L_rowind = NULL, *L_colptr = NULL; + /* compressed sparse column matrix U */ + int U_N = 0, U_nnz = 0; + PyArrayObject *U_nzvals = NULL, *U_rowind = NULL, *U_colptr = NULL; + /* right hand side / solution */ + PyObject *X_py = NULL; + /* whether the matrix is transposed ('T'), conjugate transposed ('H') or normal ('N') */ + volatile int itrans = 'N'; + volatile jmp_buf* jmpbuf_ptr; + volatile trans_t trans; + SLU_BEGIN_THREADS_DEF; + + static char* kwlist[] = { + "trans", + "L_N", "L_nnz", "L_nzvals", "L_rowind", "L_colptr", + "U_N", "U_nnz", "U_nzvals", "U_rowind", "U_colptr", + "B", NULL + }; + + /* Parse and check input arguments. */ + int res = PyArg_ParseTupleAndKeywords(args, keywds, "CiiO!O!O!iiO!O!O!O", kwlist, + &itrans, + &L_N, &L_nnz, &PyArray_Type, &L_nzvals, &PyArray_Type, &L_rowind, &PyArray_Type, &L_colptr, + &U_N, &U_nnz, &PyArray_Type, &U_nzvals, &PyArray_Type, &U_rowind, &PyArray_Type, &U_colptr, + &X_py ); + if (!res) + return NULL; + + if (itrans == 'n' || itrans == 'N') { + trans = NOTRANS; + } else if (itrans == 't' || itrans == 'T') { + trans = TRANS; + } else if (itrans == 'h' || itrans == 'H') { + trans = CONJ; + } else { + PyErr_SetString(PyExc_ValueError, "trans must be N, T, or H"); + return NULL; + } + + if (L_N!=U_N) { + PyErr_SetString(PyExc_ValueError, "L and U must have the same dimension"); + return NULL; + } + + if (!_CHECK_INTEGER(L_rowind) || !_CHECK_INTEGER(L_colptr) || + !_CHECK_INTEGER(U_rowind) || !_CHECK_INTEGER(U_colptr) ) { + PyErr_SetString(PyExc_TypeError, "row indices and column pointers must be of type cint"); + return NULL; + } + + int L_type = PyArray_TYPE((PyArrayObject*)L_nzvals); + int U_type = PyArray_TYPE((PyArrayObject*)U_nzvals); + if (L_type != U_type) { + PyErr_SetString(PyExc_TypeError, + "nzvals types of L and U differ"); + return NULL; + } + if (!CHECK_SLU_TYPE(L_type)) { + PyErr_SetString(PyExc_TypeError, + "nzvals is not of a type supported by SuperLU"); + return NULL; + } + + /* Create SuperLU matrices out of L and U. */ + int* L_col_to_sup = intMalloc(L_N+1); + int* L_sup_to_col = intMalloc(L_N+1); + for(int i=0; i<=L_N; i++){ + L_col_to_sup[i] = i; + L_sup_to_col[i] = i; + } + L_col_to_sup[L_N] = L_N - 1; + SuperMatrix L_super = {0}; + SuperMatrix U_super = {0}; + int L_conv_err = SparseFormat_from_spMatrix( + &L_super, L_N, L_N, L_nnz, -1, + (PyArrayObject*)L_nzvals, (PyArrayObject*)L_rowind, (PyArrayObject*)L_colptr, + L_type, SLU_SC, SLU_TRLU, L_col_to_sup, L_sup_to_col); + if (L_conv_err) { + return NULL; + } + int U_conv_err = SparseFormat_from_spMatrix( + &U_super, U_N, U_N, U_nnz, 0, + (PyArrayObject*)U_nzvals, (PyArrayObject*)U_rowind, (PyArrayObject*)U_colptr, + U_type, SLU_NC, SLU_TRU, NULL, NULL); + if (U_conv_err) { + Destroy_SuperMatrix_Store((SuperMatrix*)&L_super); + return NULL; + } + + /* Read right-hand-side (i.e., solution) vector. */ + PyArrayObject* X_arr = (PyArrayObject*)PyArray_FROMANY( + (PyObject*)X_py, L_type, 1, 2, + NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ENSURECOPY); + if (X_arr == NULL) { + SUPERLU_FREE((void*)L_col_to_sup); + SUPERLU_FREE((void*)L_sup_to_col); + Destroy_SuperMatrix_Store((SuperMatrix*)&L_super); + Destroy_SuperMatrix_Store((SuperMatrix*)&U_super); + return NULL; + } + if (PyArray_DIM((PyArrayObject*)X_arr, 0) != L_N) { + PyErr_SetString(PyExc_ValueError, + "right hand side array has invalid shape"); + SUPERLU_FREE((void*)L_col_to_sup); + SUPERLU_FREE((void*)L_sup_to_col); + Destroy_SuperMatrix_Store((SuperMatrix*)&L_super); + Destroy_SuperMatrix_Store((SuperMatrix*)&U_super); + Py_DECREF(X_arr); + return NULL; + } + + SuperMatrix X; + if (DenseSuper_from_Numeric((SuperMatrix*)&X, (PyObject*)X_arr)) { + SUPERLU_FREE((void*)L_col_to_sup); + SUPERLU_FREE((void*)L_sup_to_col); + Destroy_SuperMatrix_Store((SuperMatrix*)&L_super); + Destroy_SuperMatrix_Store((SuperMatrix*)&U_super); + Py_DECREF(X_arr); + return NULL; + } /* X and X_arr share the same data but X_arr "owns" it. */ + + /* Call SuperLU functions. */ + int info=0; + SuperLUStat_t stat = { 0 }; + StatInit((SuperLUStat_t *)&stat); + int* perm_c = intMalloc(L_N); + for (int i=0; i>> import numpy as np - >>> from scipy.sparse import csr_matrix + >>> from scipy.sparse import csc_array >>> from scipy.sparse.linalg import spsolve_triangular - >>> A = csr_matrix([[3, 0, 0], [1, -1, 0], [2, 0, 1]], dtype=float) + >>> A = csc_array([[3, 0, 0], [1, -1, 0], [2, 0, 1]], dtype=float) >>> B = np.array([[2, 0], [-1, 0], [2, 0]], dtype=float) >>> x = spsolve_triangular(A, B) >>> np.allclose(A.dot(x), B) @@ -662,20 +660,42 @@ def spsolve_triangular(A, b, lower=True, overwrite_A=False, overwrite_b=False, """ if is_pydata_spmatrix(A): - A = A.to_scipy_sparse().tocsr() - - # Check the input for correct type and format. - if not (issparse(A) and A.format == "csr"): - warn('CSR matrix format is required. Converting to CSR matrix.', + A = A.to_scipy_sparse().tocsc() + + trans = "N" + if issparse(A) and A.format == "csr": + A = A.T + trans = "T" + lower = not lower + + if not (issparse(A) and A.format == "csc"): + warn('CSC or CSR matrix format is required. Converting to CSC matrix.', SparseEfficiencyWarning, stacklevel=2) - A = csr_matrix(A) + A = csc_matrix(A) elif not overwrite_A: A = A.copy() - if A.shape[0] != A.shape[1]: + + M, N = A.shape + if M != N: raise ValueError( f'A must be a square matrix but its shape is {A.shape}.') + if unit_diagonal: + with catch_warnings(): + simplefilter('ignore', SparseEfficiencyWarning) + A.setdiag(1) + else: + diag = A.diagonal() + if np.any(diag == 0): + raise LinAlgError( + 'A is singular: zero entry on diagonal.') + invdiag = 1/diag + if trans == "N": + A = A @ diags(invdiag) + else: + A = (A.T @ diags(invdiag)).T + # sum duplicates for non-canonical format A.sum_duplicates() @@ -684,63 +704,39 @@ def spsolve_triangular(A, b, lower=True, overwrite_A=False, overwrite_b=False, if b.ndim not in [1, 2]: raise ValueError( f'b must have 1 or 2 dims but its shape is {b.shape}.') - if A.shape[0] != b.shape[0]: + if M != b.shape[0]: raise ValueError( 'The size of the dimensions of A must be equal to ' 'the size of the first dimension of b but the shape of A is ' f'{A.shape} and the shape of b is {b.shape}.' ) - # Init x as (a copy of) b. - x_dtype = np.result_type(A.data, b, np.float64) - if overwrite_b: - if np.can_cast(b.dtype, x_dtype, casting='same_kind'): - x = b - else: - raise ValueError( - f'Cannot overwrite b (dtype {b.dtype}) with result ' - f'of type {x_dtype}.' - ) - else: - x = b.astype(x_dtype, copy=True) + result_dtype = np.promote_types(np.promote_types(A.dtype, np.float32), b.dtype) + if A.dtype != result_dtype: + A = A.astype(result_dtype) + if b.dtype != result_dtype: + b = b.astype(result_dtype) + elif not overwrite_b: + b = b.copy() - # Choose forward or backward order. if lower: - row_indices = range(len(b)) + L = A + U = csc_matrix((N, N), dtype=result_dtype) else: - row_indices = range(len(b) - 1, -1, -1) - - # Fill x iteratively. - for i in row_indices: - - # Get indices for i-th row. - indptr_start = A.indptr[i] - indptr_stop = A.indptr[i + 1] + L = eye(N, dtype=result_dtype, format='csc') + U = A + U.setdiag(0) - if lower: - A_diagonal_index_row_i = indptr_stop - 1 - A_off_diagonal_indices_row_i = slice(indptr_start, indptr_stop - 1) - else: - A_diagonal_index_row_i = indptr_start - A_off_diagonal_indices_row_i = slice(indptr_start + 1, indptr_stop) - - # Check regularity and triangularity of A. - if not unit_diagonal and (indptr_stop <= indptr_start - or A.indices[A_diagonal_index_row_i] < i): - raise LinAlgError( - f'A is singular: diagonal {i} is zero.') - if not unit_diagonal and A.indices[A_diagonal_index_row_i] > i: - raise LinAlgError( - 'A is not triangular: A[{}, {}] is nonzero.' - ''.format(i, A.indices[A_diagonal_index_row_i])) - - # Incorporate off-diagonal entries. - A_column_indices_in_row_i = A.indices[A_off_diagonal_indices_row_i] - A_values_in_row_i = A.data[A_off_diagonal_indices_row_i] - x[i] -= np.dot(x[A_column_indices_in_row_i].T, A_values_in_row_i) + x, info = _superlu.gstrs(trans, + N, L.nnz, L.data, L.indices, L.indptr, + N, U.nnz, U.data, U.indices, U.indptr, + b) + if info: + raise LinAlgError('A is singular.') - # Compute i-th entry of x. - if not unit_diagonal: - x[i] /= A.data[A_diagonal_index_row_i] + if not unit_diagonal: + invdiag = invdiag.reshape(-1, *([1] * (len(x.shape) - 1))) + x = x * invdiag return x + diff --git a/scipy/sparse/linalg/_dsolve/tests/test_linsolve.py b/scipy/sparse/linalg/_dsolve/tests/test_linsolve.py index fe900d8bf9a0..836b0127ff66 100644 --- a/scipy/sparse/linalg/_dsolve/tests/test_linsolve.py +++ b/scipy/sparse/linalg/_dsolve/tests/test_linsolve.py @@ -722,17 +722,59 @@ def worker(): assert_equal(len(oks), 20) +class TestGstrsErrors: + def setup_method(self): + self.A = array([[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0,9.0]], dtype=np.float64) + self.b = np.array([[1.0],[2.0],[3.0]], dtype=np.float64) + + def test_trans(self): + L = scipy.sparse.tril(self.A, format='csc') + U = scipy.sparse.triu(self.A, k=1, format='csc') + with assert_raises(ValueError, match="trans must be N, T, or H"): + _superlu.gstrs('X', L.shape[0], L.nnz, L.data, L.indices, L.indptr, + U.shape[0], U.nnz, U.data, U.indices, U.indptr, self.b) + + def test_shape_LU(self): + L = scipy.sparse.tril(self.A[0:2,0:2], format='csc') + U = scipy.sparse.triu(self.A, k=1, format='csc') + with assert_raises(ValueError, match="L and U must have the same dimension"): + _superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr, + U.shape[0], U.nnz, U.data, U.indices, U.indptr, self.b) + + def test_shape_b(self): + L = scipy.sparse.tril(self.A, format='csc') + U = scipy.sparse.triu(self.A, k=1, format='csc') + with assert_raises(ValueError, match="right hand side array has invalid shape"): + _superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr, + U.shape[0], U.nnz, U.data, U.indices, U.indptr, + self.b[0:2]) + + def test_types_differ(self): + L = scipy.sparse.tril(self.A.astype(np.float32), format='csc') + U = scipy.sparse.triu(self.A, k=1, format='csc') + with assert_raises(TypeError, match="nzvals types of L and U differ"): + _superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr, + U.shape[0], U.nnz, U.data, U.indices, U.indptr, self.b) + + def test_types_unsupported(self): + L = scipy.sparse.tril(self.A.astype(np.uint8), format='csc') + U = scipy.sparse.triu(self.A.astype(np.uint8), k=1, format='csc') + with assert_raises(TypeError, match="nzvals is not of a type supported"): + _superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr, + U.shape[0], U.nnz, U.data, U.indices, U.indptr, + self.b.astype(np.uint8)) class TestSpsolveTriangular: def setup_method(self): use_solver(useUmfpack=False) - def test_zero_diagonal(self): + @pytest.mark.parametrize("fmt",["csr","csc"]) + def test_zero_diagonal(self,fmt): n = 5 rng = np.random.default_rng(43876432987) A = rng.standard_normal((n, n)) b = np.arange(n) - A = scipy.sparse.tril(A, k=0, format='csr') + A = scipy.sparse.tril(A, k=0, format=fmt) x = spsolve_triangular(A, b, unit_diagonal=True, lower=True) @@ -743,12 +785,16 @@ def test_zero_diagonal(self): A = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0]], dtype=np.float64) b = np.array([1., 2., 3.]) with suppress_warnings() as sup: - sup.filter(SparseEfficiencyWarning, "CSR matrix format is") + sup.filter(SparseEfficiencyWarning, "CSC or CSR matrix format is") spsolve_triangular(A, b, unit_diagonal=True) - def test_singular(self): + @pytest.mark.parametrize("fmt",["csr","csc"]) + def test_singular(self,fmt): n = 5 - A = csr_matrix((n, n)) + if fmt == "csr": + A = csr_matrix((n, n)) + else: + A = csc_matrix((n, n)) b = np.arange(n) for lower in (True, False): assert_raises(scipy.linalg.LinAlgError, @@ -774,32 +820,53 @@ def test_input_types(self): assert_array_almost_equal(A.dot(x), b) @pytest.mark.slow - @pytest.mark.timeout(120) # prerelease_deps_coverage_64bit_blas job @sup_sparse_efficiency - def test_random(self): - def random_triangle_matrix(n, lower=True): - A = scipy.sparse.random(n, n, density=0.1, format='coo') + @pytest.mark.parametrize("n", [10, 10**2, 10**3]) + @pytest.mark.parametrize("m", [1, 10]) + @pytest.mark.parametrize("lower", [True, False]) + @pytest.mark.parametrize("format", ["csr", "csc"]) + @pytest.mark.parametrize("unit_diagonal", [False, True]) + @pytest.mark.parametrize("choice_of_A", ["real", "complex"]) + @pytest.mark.parametrize("choice_of_b", ["floats", "ints", "complexints"]) + def test_random(self, n, m, lower, format, unit_diagonal, choice_of_A, choice_of_b): + def random_triangle_matrix(n, lower=True, format="csr", choice_of_A="real"): + if choice_of_A == "real": + dtype = np.float64 + elif choice_of_A == "complex": + dtype = np.complex128 + else: + raise ValueError("choice_of_A must be 'real' or 'complex'.") + rng = np.random.default_rng(789002319) + rvs = rng.random + A = scipy.sparse.random(n, n, density=0.1, format='lil', dtype=dtype, + random_state=rng, data_rvs=rvs) if lower: - A = scipy.sparse.tril(A) + A = scipy.sparse.tril(A, format="lil") else: - A = scipy.sparse.triu(A) - A = A.tocsr(copy=False) + A = scipy.sparse.triu(A, format="lil") for i in range(n): A[i, i] = np.random.rand() + 1 + if format == "csc": + A = A.tocsc(copy=False) + else: + A = A.tocsr(copy=False) return A np.random.seed(1234) - for lower in (True, False): - for n in (10, 10**2, 10**3): - A = random_triangle_matrix(n, lower=lower) - for m in (1, 10): - for b in (np.random.rand(n, m), - np.random.randint(-9, 9, (n, m)), - np.random.randint(-9, 9, (n, m)) + - np.random.randint(-9, 9, (n, m)) * 1j): - x = spsolve_triangular(A, b, lower=lower) - assert_array_almost_equal(A.dot(x), b) - x = spsolve_triangular(A, b, lower=lower, - unit_diagonal=True) - A.setdiag(1) - assert_array_almost_equal(A.dot(x), b) + A = random_triangle_matrix(n, lower=lower) + if choice_of_b == "floats": + b = np.random.rand(n, m) + elif choice_of_b == "ints": + b = np.random.randint(-9, 9, (n, m)) + elif choice_of_b == "complexints": + b = np.random.randint(-9, 9, (n, m)) + np.random.randint(-9, 9, (n, m)) * 1j + else: + raise ValueError( + "choice_of_b must be 'floats', 'ints', or 'complexints'.") + x = spsolve_triangular(A, b, lower=lower, unit_diagonal=unit_diagonal) + if unit_diagonal: + A.setdiag(1) + assert_allclose(A.dot(x), b, atol=1.5e-6) + + + diff --git a/scipy/sparse/linalg/_eigen/arpack/arpack.py b/scipy/sparse/linalg/_eigen/arpack/arpack.py index f7a6fa218ca4..4fb6f3e4eb09 100644 --- a/scipy/sparse/linalg/_eigen/arpack/arpack.py +++ b/scipy/sparse/linalg/_eigen/arpack/arpack.py @@ -40,7 +40,9 @@ from scipy.sparse.linalg._interface import aslinearoperator, LinearOperator from scipy.sparse import eye, issparse from scipy.linalg import eig, eigh, lu_factor, lu_solve -from scipy.sparse._sputils import isdense, is_pydata_spmatrix +from scipy.sparse._sputils import ( + convert_pydata_sparse_to_scipy, isdense, is_pydata_spmatrix, +) from scipy.sparse.linalg import gmres, splu from scipy._lib._util import _aligned_zeros from scipy._lib._threadsafety import ReentrancyLock @@ -1256,6 +1258,8 @@ def eigs(A, k=6, M=None, sigma=None, which='LM', v0=None, (13, 6) """ + A = convert_pydata_sparse_to_scipy(A) + M = convert_pydata_sparse_to_scipy(M) if A.shape[0] != A.shape[1]: raise ValueError(f'expected square matrix (shape={A.shape})') if M is not None: diff --git a/scipy/sparse/linalg/_isolve/lsqr.py b/scipy/sparse/linalg/_isolve/lsqr.py index 010f61bc5412..ba684d147e42 100644 --- a/scipy/sparse/linalg/_isolve/lsqr.py +++ b/scipy/sparse/linalg/_isolve/lsqr.py @@ -54,6 +54,7 @@ import numpy as np from math import sqrt from scipy.sparse.linalg._interface import aslinearoperator +from scipy.sparse._sputils import convert_pydata_sparse_to_scipy eps = np.finfo(np.float64).eps @@ -319,6 +320,7 @@ def lsqr(A, b, damp=0.0, atol=1e-6, btol=1e-6, conlim=1e8, approximate solution to the corresponding least-squares problem. `r1norm` contains the norm of the minimal residual that was found. """ + A = convert_pydata_sparse_to_scipy(A) A = aslinearoperator(A) b = np.atleast_1d(b) if b.ndim > 1: diff --git a/scipy/sparse/linalg/_isolve/tests/test_iterative.py b/scipy/sparse/linalg/_isolve/tests/test_iterative.py index 2fda70fcb319..4ebfa62c4e74 100644 --- a/scipy/sparse/linalg/_isolve/tests/test_iterative.py +++ b/scipy/sparse/linalg/_isolve/tests/test_iterative.py @@ -308,7 +308,7 @@ def identity(b, which=None): # Specific test for poisson1d and poisson2d cases -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) @pytest.mark.parametrize('case', [x for x in IterativeParams().cases if x.name in ('poisson1d', 'poisson2d')], ids=['poisson1d', 'poisson2d']) @@ -685,7 +685,7 @@ def test_abi(self): assert_allclose(r_x, x) assert r_info == info - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_atol_legacy(self): A = eye(2) diff --git a/scipy/sparse/linalg/_norm.py b/scipy/sparse/linalg/_norm.py index 38f3a6d7a6f8..4cd7fd6f50dc 100644 --- a/scipy/sparse/linalg/_norm.py +++ b/scipy/sparse/linalg/_norm.py @@ -4,6 +4,7 @@ import numpy as np from scipy.sparse import issparse from scipy.sparse.linalg import svds +from scipy.sparse._sputils import convert_pydata_sparse_to_scipy import scipy.sparse as sp from numpy import sqrt, abs @@ -110,6 +111,7 @@ def norm(x, ord=None, axis=None): >>> norm(b, 2) 1.9753... """ + x = convert_pydata_sparse_to_scipy(x, target_format="csr") if not issparse(x): raise TypeError("input is not sparse. use numpy.linalg.norm") diff --git a/scipy/sparse/linalg/tests/test_expm_multiply.py b/scipy/sparse/linalg/tests/test_expm_multiply.py index 858ce11b9d4b..0afd22c94661 100644 --- a/scipy/sparse/linalg/tests/test_expm_multiply.py +++ b/scipy/sparse/linalg/tests/test_expm_multiply.py @@ -179,7 +179,7 @@ def test_complex(self): class TestExpmActionInterval: - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(20) def test_sparse_expm_multiply_interval(self): np.random.seed(1234) start = 0.1 @@ -205,7 +205,7 @@ def test_sparse_expm_multiply_interval(self): for solution, t in zip(X, samples): assert_allclose(solution, sp_expm(t*A).dot(target)) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(20) def test_expm_multiply_interval_vector(self): np.random.seed(1234) interval = {'start': 0.1, 'stop': 3.2, 'endpoint': True} @@ -232,7 +232,7 @@ def test_expm_multiply_interval_vector(self): assert_allclose(sol_given, correct) assert_allclose(sol_wrong, correct) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(20) def test_expm_multiply_interval_matrix(self): np.random.seed(1234) interval = {'start': 0.1, 'stop': 3.2, 'endpoint': True} diff --git a/scipy/sparse/linalg/tests/test_pydata_sparse.py b/scipy/sparse/linalg/tests/test_pydata_sparse.py index b42448d0ef1a..62a7adc49e98 100644 --- a/scipy/sparse/linalg/tests/test_pydata_sparse.py +++ b/scipy/sparse/linalg/tests/test_pydata_sparse.py @@ -212,6 +212,13 @@ def test_onenormest(matrices): assert_allclose(est, est0) +def test_norm(matrices): + A_dense, A_sparse, b = matrices + norm0 = splin.norm(sp.csr_matrix(A_dense)) + norm = splin.norm(A_sparse) + assert_allclose(norm, norm0) + + def test_inv(matrices): A_dense, A_sparse, b = matrices x0 = splin.inv(sp.csc_matrix(A_dense)) diff --git a/scipy/sparse/tests/test_array_api.py b/scipy/sparse/tests/test_array_api.py index 73fb46da5913..a3be7b868b1a 100644 --- a/scipy/sparse/tests/test_array_api.py +++ b/scipy/sparse/tests/test_array_api.py @@ -256,13 +256,18 @@ def test_spsolve(B): ) -def test_spsolve_triangular(): - X = scipy.sparse.csr_array([ +@pytest.mark.parametrize("fmt",["csr","csc"]) +def test_spsolve_triangular(fmt): + arr = [ [1, 0, 0, 0], [2, 1, 0, 0], [3, 2, 1, 0], [4, 3, 2, 1], - ]) + ] + if fmt == "csr": + X = scipy.sparse.csr_array(arr) + else: + X = scipy.sparse.csc_array(arr) spla.spsolve_triangular(X, [1, 2, 3, 4]) diff --git a/scipy/sparse/tests/test_base.py b/scipy/sparse/tests/test_base.py index 1aa76b8ff26e..4cffa3feab35 100644 --- a/scipy/sparse/tests/test_base.py +++ b/scipy/sparse/tests/test_base.py @@ -633,11 +633,21 @@ def test_empty(self): assert_equal(self.spcreator((3, 3)).toarray(), zeros((3, 3))) assert_equal(self.spcreator((3, 3)).nnz, 0) assert_equal(self.spcreator((3, 3)).count_nonzero(), 0) + if self.datsp.format in ["coo", "csr", "csc", "lil"]: + assert_equal(self.spcreator((3, 3)).count_nonzero(axis=0), array([0, 0, 0])) def test_count_nonzero(self): - expected = np.count_nonzero(self.datsp.toarray()) - assert_equal(self.datsp.count_nonzero(), expected) - assert_equal(self.datsp.T.count_nonzero(), expected) + axis_support = self.datsp.format in ["coo", "csr", "csc", "lil"] + axes = [None, 0, 1, -1, -2] if axis_support else [None] + + for A in (self.datsp, self.datsp.T): + for ax in axes: + expected = np.count_nonzero(A.toarray(), axis=ax) + assert_equal(A.count_nonzero(axis=ax), expected) + + if not axis_support: + with assert_raises(NotImplementedError, match="not implemented .* format"): + self.datsp.count_nonzero(axis=0) def test_invalid_shapes(self): assert_raises(ValueError, self.spcreator, (-1,3)) @@ -658,6 +668,26 @@ def test_repr(self): ) assert repr(datsp) == expected + def test_str_maxprint(self): + datsp = self.spcreator(np.arange(75).reshape(5, 15)) + assert datsp.maxprint == 50 + assert len(str(datsp).split('\n')) == 51 + 3 + + dat = np.arange(15).reshape(5,3) + datsp = self.spcreator(dat) + # format dia reports nnz=15, but we want 14 + nnz_small = 14 if datsp.format == 'dia' else datsp.nnz + datsp_mp6 = self.spcreator(dat, maxprint=6) + + assert len(str(datsp).split('\n')) == nnz_small + 3 + assert len(str(datsp_mp6).split('\n')) == 6 + 4 + + # Check parameter `maxprint` is keyword only + datsp = self.spcreator(dat, shape=(5, 3), dtype='i', copy=False, maxprint=4) + datsp = self.spcreator(dat, (5, 3), 'i', False, maxprint=4) + with pytest.raises(TypeError, match="positional argument|unpack non-iterable"): + self.spcreator(dat, (5, 3), 'i', False, 4) + def test_str(self): datsp = self.spcreator([[1, 0, 0], [0, 0, 0], [0, 0, -2]]) if datsp.nnz != 2: @@ -1655,7 +1685,7 @@ def test_small_multiplication(self): assert_equal(A @ np.ones((1, 1)), array([[1], [2], [3]])) assert_equal(A @ np.ones((1, 0)), np.ones((3, 0))) - def test_start_vs_at_sign_for_sparray_and_spmatrix(self): + def test_star_vs_at_sign_for_sparray_and_spmatrix(self): # test that * is matmul for spmatrix and mul for sparray A = self.spcreator([[1],[2],[3]]) @@ -1745,10 +1775,6 @@ def test_matmul(self): assert_array_almost_equal(matmul(M, B).toarray(), (M @ B).toarray()) assert_array_almost_equal(matmul(M.toarray(), B), (M @ B).toarray()) assert_array_almost_equal(matmul(M, B.toarray()), (M @ B).toarray()) - if not isinstance(M, sparray): - assert_array_almost_equal(matmul(M, B).toarray(), (M * B).toarray()) - assert_array_almost_equal(matmul(M.toarray(), B), (M * B).toarray()) - assert_array_almost_equal(matmul(M, B.toarray()), (M * B).toarray()) # check error on matrix-scalar assert_raises(ValueError, matmul, M, 1) @@ -1773,7 +1799,7 @@ def test_matvec(self): bad_vecs = [array([1,2]), array([1,2,3,4]), array([[1],[2]]), matrix([1,2,3]), matrix([[1],[2]])] for x in bad_vecs: - assert_raises(ValueError, M.__mul__, x) + assert_raises(ValueError, M.__matmul__, x) # The current relationship between sparse matrix products and array # products is as follows: @@ -2242,21 +2268,45 @@ def test_inplace_dense(self): y -= b assert_array_equal(x, y) - x = a.copy() - y = a.copy() if isinstance(b, sparray): - assert_raises(ValueError, operator.imul, x, b.T) + # Elementwise multiply from __rmul__ + x = a.copy() + y = a.copy() + with assert_raises(ValueError, match="dimension mismatch"): + x *= b.T x = x * a y *= b + assert_array_equal(x, y) else: - # This is matrix product, from __rmul__ - assert_raises(ValueError, operator.imul, x, b) + # Matrix Product from __rmul__ + x = a.copy() + y = a.copy() + with assert_raises(ValueError, match="dimension mismatch"): + x *= b x = x.dot(a.T) y *= b.T - assert_array_equal(x, y) + assert_array_equal(x, y) - # Matrix (non-elementwise) floor division is not defined - assert_raises(TypeError, operator.ifloordiv, x, b) + # Now matrix product, from __rmatmul__ + y = a.copy() + # skip this test if numpy doesn't support __imatmul__ yet. + # move out of the try/except once numpy 1.24 is no longer supported. + try: + y @= b.T + except TypeError: + pass + else: + x = a.copy() + y = a.copy() + with assert_raises(ValueError, match="dimension mismatch"): + x @= b + x = x.dot(a.T) + y @= b.T + assert_array_equal(x, y) + + # Floor division is not supported + with assert_raises(TypeError, match="unsupported operand"): + x //= b def test_imul_scalar(self): def check(dtype): @@ -2317,15 +2367,21 @@ def test_inplace_success(self): bp = bp + a assert_allclose(b.toarray(), bp.toarray()) - b *= a - bp = bp * a + if isinstance(b, sparray): + b *= a + bp = bp * a + assert_allclose(b.toarray(), bp.toarray()) + + b @= a + bp = bp @ a assert_allclose(b.toarray(), bp.toarray()) b -= a bp = bp - a assert_allclose(b.toarray(), bp.toarray()) - assert_raises(TypeError, operator.ifloordiv, a, b) + with assert_raises(TypeError, match="unsupported operand"): + a //= b class _TestGetSet: @@ -2580,6 +2636,7 @@ def test_slicing_2(self): assert_equal(A[s, :].toarray(), B[2:4, :]) assert_equal(A[:, s].toarray(), B[:, 2:4]) + @pytest.mark.fail_slow(2) def test_slicing_3(self): B = asmatrix(arange(50).reshape(5,10)) A = self.spcreator(B) @@ -3383,7 +3440,7 @@ def __arith_init(self): self.__Asp = self.spcreator(self.__A) self.__Bsp = self.spcreator(self.__B) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(20) def test_add_sub(self): self.__arith_init() @@ -4186,11 +4243,11 @@ class TestDOK(sparse_test_class(minmax=False, nnz_axis=False)): math_dtypes = [np.int_, np.float64, np.complex128] def test_mult(self): - A = dok_matrix((10,10)) - A[0,3] = 10 - A[5,6] = 20 - D = A*A.T - E = A*A.T.conjugate() + A = dok_matrix((10, 12)) + A[0, 3] = 10 + A[5, 6] = 20 + D = A @ A.T + E = A @ A.T.conjugate() assert_array_equal(D.toarray(), E.toarray()) def test_add_nonzero(self): @@ -4297,9 +4354,9 @@ def test_dot(self): # TODO: properly handle this assertion on ppc64le if platform.machine() != 'ppc64le': - assert_array_equal(A @ A.T, (B * B.T).toarray()) + assert_array_equal(A @ A.T, (B @ B.T).toarray()) - assert_array_equal(A @ A.conjugate().T, (B * B.conjugate().T).toarray()) + assert_array_equal(A @ A.conjugate().T, (B @ B.conjugate().T).toarray()) def test_scalar_mul(self): x = lil_matrix((3, 3)) @@ -4773,12 +4830,12 @@ def test_eliminate_zeros_all_zero(self): def test_bsr_matvec(self): A = bsr_matrix(arange(2*3*4*5).reshape(2*4,3*5), blocksize=(4,5)) x = arange(A.shape[1]).reshape(-1,1) - assert_equal(A*x, A.toarray() @ x) + assert_equal(A @ x, A.toarray() @ x) def test_bsr_matvecs(self): A = bsr_matrix(arange(2*3*4*5).reshape(2*4,3*5), blocksize=(4,5)) x = arange(A.shape[1]*6).reshape(-1,6) - assert_equal(A*x, A.toarray() @ x) + assert_equal(A @ x, A.toarray() @ x) @pytest.mark.xfail(run=False, reason='BSR does not have a __getitem__') def test_iterator(self): @@ -4889,11 +4946,11 @@ def _same_sum_duplicate(data, *inds, **kwargs): class _NonCanonicalMixin: - def spcreator(self, D, sorted_indices=False, **kwargs): + def spcreator(self, D, *args, sorted_indices=False, **kwargs): """Replace D with a non-canonical equivalent: containing duplicate elements and explicit zeros""" construct = super().spcreator - M = construct(D, **kwargs) + M = construct(D, *args, **kwargs) zero_pos = (M.toarray() == 0).nonzero() has_zeros = (zero_pos[0].size > 0) @@ -5153,6 +5210,7 @@ def check(cls, method_name): def test_resiliency_limit_10(self, cls, method_name): self._check_resiliency(cls, method_name, maxval_limit=10) + @pytest.mark.fail_slow(2) @pytest.mark.parametrize('cls,method_name', cases_64bit()) def test_resiliency_random(self, cls, method_name): # bsr_matrix.eliminate_zeros relies on csr_matrix constructor @@ -5168,6 +5226,7 @@ def test_resiliency_all_32(self, cls, method_name): def test_resiliency_all_64(self, cls, method_name): self._check_resiliency(cls, method_name, fixed_dtype=np.int64) + @pytest.mark.fail_slow(5) @pytest.mark.parametrize('cls,method_name', cases_64bit()) def test_no_64(self, cls, method_name): self._check_resiliency(cls, method_name, assert_32bit=True) diff --git a/scipy/sparse/tests/test_minmax1d.py b/scipy/sparse/tests/test_minmax1d.py index 53e961931492..dca3f44fa485 100644 --- a/scipy/sparse/tests/test_minmax1d.py +++ b/scipy/sparse/tests/test_minmax1d.py @@ -6,7 +6,8 @@ from numpy.testing import assert_equal, assert_array_equal -from scipy.sparse import coo_array +from scipy.sparse import coo_array, csr_array, csc_array, bsr_array +from scipy.sparse import coo_matrix, csr_matrix, csc_matrix, bsr_matrix from scipy.sparse._sputils import isscalarlike @@ -16,10 +17,11 @@ def toarray(a): return a.toarray() -formats_for_minmax = [coo_array] +formats_for_minmax = [bsr_array, coo_array, csc_array, csr_array] +formats_for_minmax_supporting_1d = [coo_array, csr_array] -@pytest.mark.parametrize("spcreator", formats_for_minmax) +@pytest.mark.parametrize("spcreator", formats_for_minmax_supporting_1d) class Test_MinMaxMixin1D: def test_minmax(self, spcreator): D = np.arange(5) @@ -30,7 +32,6 @@ def test_minmax(self, spcreator): assert_equal((-X).min(), -4) assert_equal((-X).max(), 0) - def test_minmax_axis(self, spcreator): D = np.arange(50) X = spcreator(D) @@ -48,7 +49,6 @@ def test_minmax_axis(self, spcreator): with pytest.raises(ValueError, match="axis out of range"): X.max(axis=axis) - def test_numpy_minmax(self, spcreator): dat = np.array([0, 1, 2]) datsp = spcreator(dat) @@ -80,3 +80,49 @@ def test_argmax(self, spcreator): mat.argmin(axis=axis) with pytest.raises(ValueError, match="to an empty matrix"): mat.argmax(axis=axis) + + +@pytest.mark.parametrize("spcreator", formats_for_minmax) +class Test_ShapeMinMax2DWithAxis: + def test_minmax(self, spcreator): + dat = np.array([[-1, 5, 0, 3], [0, 0, -1, -2], [0, 0, 1, 2]]) + datsp = spcreator(dat) + + for (spminmax, npminmax) in [ + (datsp.min, np.min), + (datsp.max, np.max), + (datsp.nanmin, np.nanmin), + (datsp.nanmax, np.nanmax), + ]: + for ax, result_shape in [(0, (4,)), (1, (3,))]: + assert_equal(toarray(spminmax(axis=ax)), npminmax(dat, axis=ax)) + assert_equal(spminmax(axis=ax).shape, result_shape) + assert spminmax(axis=ax).format == "coo" + + for spminmax in [datsp.argmin, datsp.argmax]: + for ax in [0, 1]: + assert isinstance(spminmax(axis=ax), np.ndarray) + + # verify spmatrix behavior + spmat_form = { + 'coo': coo_matrix, + 'csr': csr_matrix, + 'csc': csc_matrix, + 'bsr': bsr_matrix, + } + datspm = spmat_form[datsp.format](dat) + + for spm, npm in [ + (datspm.min, np.min), + (datspm.max, np.max), + (datspm.nanmin, np.nanmin), + (datspm.nanmax, np.nanmax), + ]: + for ax, result_shape in [(0, (1, 4)), (1, (3, 1))]: + assert_equal(toarray(spm(axis=ax)), npm(dat, axis=ax, keepdims=True)) + assert_equal(spm(axis=ax).shape, result_shape) + assert spm(axis=ax).format == "coo" + + for spminmax in [datspm.argmin, datspm.argmax]: + for ax in [0, 1]: + assert isinstance(spminmax(axis=ax), np.ndarray) diff --git a/scipy/sparse/tests/test_sputils.py b/scipy/sparse/tests/test_sputils.py index 4545b49bea2c..e0e21b348a81 100644 --- a/scipy/sparse/tests/test_sputils.py +++ b/scipy/sparse/tests/test_sputils.py @@ -23,10 +23,16 @@ def test_getdtype(self): with assert_raises( ValueError, - match="object dtype is not supported by sparse matrices", + match="scipy.sparse does not support dtype object. .*", ): sputils.getdtype("O") + with assert_raises( + ValueError, + match="scipy.sparse does not support dtype float16. .*", + ): + sputils.getdtype(None, default=np.float16) + def test_isscalarlike(self): assert_equal(sputils.isscalarlike(3.0), True) assert_equal(sputils.isscalarlike(-4), True) diff --git a/scipy/spatial/_hausdorff.pyx b/scipy/spatial/_hausdorff.pyx index 01cfce9df3f3..315fec38380b 100644 --- a/scipy/spatial/_hausdorff.pyx +++ b/scipy/spatial/_hausdorff.pyx @@ -22,7 +22,7 @@ np.import_array() __all__ = ['directed_hausdorff'] @cython.boundscheck(False) -def directed_hausdorff(double[:,::1] ar1, double[:,::1] ar2, seed=0): +def directed_hausdorff(const double[:,::1] ar1, const double[:,::1] ar2, seed=0): cdef double cmax, cmin, d = 0 cdef Py_ssize_t N1 = ar1.shape[0] diff --git a/scipy/spatial/_qhull.pyx b/scipy/spatial/_qhull.pyx index 143315aba74e..c51bc95ebef5 100644 --- a/scipy/spatial/_qhull.pyx +++ b/scipy/spatial/_qhull.pyx @@ -16,6 +16,10 @@ Wrappers for Qhull triangulation, plus some additional N-D geometry utilities import numpy as np cimport numpy as np cimport cython +from cpython.pythread cimport ( + PyThread_type_lock, PyThread_allocate_lock, PyThread_free_lock, + PyThread_acquire_lock, PyThread_release_lock) + from . cimport _qhull from . cimport setlist from libc cimport stdlib @@ -242,6 +246,7 @@ cdef class _Qhull: cdef int _nridges cdef np.ndarray _ridge_equations + cdef PyThread_type_lock _lock @cython.final def __init__(self, @@ -254,6 +259,10 @@ cdef class _Qhull: np.ndarray[np.double_t, ndim=1] interior_point=None): cdef int exitcode + self._lock = PyThread_allocate_lock() + if self._lock == NULL: + raise MemoryError("thread lock allocation failed") + self._qh = NULL self._messages = MessageStream() @@ -342,6 +351,13 @@ cdef class _Qhull: self.close() raise QhullError(msg) + cdef void acquire_lock(self): + if not PyThread_acquire_lock(self._lock, 0): + PyThread_acquire_lock(self._lock, 1) + + cdef void release_lock(self): + PyThread_release_lock(self._lock) + def check_active(self): if self._qh == NULL: raise RuntimeError("Qhull instance is closed") @@ -350,19 +366,25 @@ cdef class _Qhull: def __dealloc__(self): cdef int curlong, totlong - if self._qh != NULL: - qh_freeqhull(self._qh, qh_ALL) - qh_memfreeshort(self._qh, &curlong, &totlong) - stdlib.free(self._qh) - self._qh = NULL - - if curlong != 0 or totlong != 0: - raise QhullError( - "qhull: did not free %d bytes (%d pieces)" % - (totlong, curlong)) + self.acquire_lock() + try: + if self._qh != NULL: + qh_freeqhull(self._qh, qh_ALL) + qh_memfreeshort(self._qh, &curlong, &totlong) + stdlib.free(self._qh) + self._qh = NULL + + if curlong != 0 or totlong != 0: + raise QhullError( + "qhull: did not free %d bytes (%d pieces)" % + (totlong, curlong)) + + if self._messages is not None: + self._messages.close() + finally: + self.release_lock() - if self._messages is not None: - self._messages.close() + PyThread_free_lock(self._lock) @cython.final def close(self): @@ -377,19 +399,23 @@ cdef class _Qhull: cdef int curlong, totlong - if self._qh != NULL: - qh_freeqhull(self._qh, qh_ALL) - qh_memfreeshort(self._qh, &curlong, &totlong) - stdlib.free(self._qh) - self._qh = NULL - - if curlong != 0 or totlong != 0: - raise QhullError( - "qhull: did not free %d bytes (%d pieces)" % - (totlong, curlong)) - - if self._messages is not None: - self._messages.close() + self.acquire_lock() + try: + if self._qh != NULL: + qh_freeqhull(self._qh, qh_ALL) + qh_memfreeshort(self._qh, &curlong, &totlong) + stdlib.free(self._qh) + self._qh = NULL + + if curlong != 0 or totlong != 0: + raise QhullError( + "qhull: did not free %d bytes (%d pieces)" % + (totlong, curlong)) + + if self._messages is not None: + self._messages.close() + finally: + self.release_lock() @cython.final def get_points(self): @@ -409,107 +435,124 @@ cdef class _Qhull: cdef boolT isoutside cdef np.ndarray arr - self.check_active() - - points = np.asarray(points) - if points.ndim!=2 or points.shape[1] != self._point_arrays[0].shape[1]: - raise ValueError("invalid size for new points array") - if points.size == 0: - return - - if self._is_delaunay: - arr = np.empty((points.shape[0], self.ndim+1), dtype=np.double) - arr[:,:-1] = points - elif self._is_halfspaces: - #Store the halfspaces in _points and the dual points in _dual_points later - self._point_arrays.append(np.array(points, copy=True)) - dists = points[:, :-1].dot(interior_point)+points[:, -1] - arr = np.array(-points[:, :-1]/dists, dtype=np.double, order="C", copy=True) - else: - arr = np.array(points, dtype=np.double, order="C", copy=True) - - self._messages.clear() + self.acquire_lock() try: - # nonlocal error handling - exitcode = setjmp(self._qh[0].errexit) - if exitcode != 0: - raise QhullError(self._messages.get()) - self._qh[0].NOerrexit = 0 + self.check_active() + + points = np.asarray(points) + if points.ndim!=2 or points.shape[1] != self._point_arrays[0].shape[1]: + raise ValueError("invalid size for new points array") + if points.size == 0: + return - # add points to triangulation if self._is_delaunay: - # lift to paraboloid - qh_setdelaunay(self._qh, arr.shape[1], arr.shape[0], arr.data) + arr = np.empty((points.shape[0], self.ndim+1), dtype=np.double) + arr[:,:-1] = points + elif self._is_halfspaces: + #Store the halfspaces in _points and the dual points in _dual_points later + self._point_arrays.append(np.array(points, copy=True)) + dists = points[:, :-1].dot(interior_point)+points[:, -1] + arr = np.array(-points[:, :-1]/dists, dtype=np.double, order="C", copy=True) + else: + arr = np.array(points, dtype=np.double, order="C", copy=True) - p = arr.data + self._messages.clear() - for j in range(arr.shape[0]): - facet = qh_findbestfacet(self._qh, p, 0, &bestdist, &isoutside) - if isoutside: - if not qh_addpoint(self._qh, p, facet, 0): - break - else: - # append the point to the "other points" list, to - # maintain the point IDs - qh_setappend(self._qh, &self._qh[0].other_points, p) + try: + # nonlocal error handling + exitcode = setjmp(self._qh[0].errexit) + if exitcode != 0: + raise QhullError(self._messages.get()) + self._qh[0].NOerrexit = 0 + + # add points to triangulation + if self._is_delaunay: + # lift to paraboloid + qh_setdelaunay(self._qh, arr.shape[1], arr.shape[0], arr.data) + + p = arr.data + + for j in range(arr.shape[0]): + facet = qh_findbestfacet(self._qh, p, 0, &bestdist, &isoutside) + if isoutside: + if not qh_addpoint(self._qh, p, facet, 0): + break + else: + # append the point to the "other points" list, to + # maintain the point IDs + qh_setappend(self._qh, &self._qh[0].other_points, p) - p += arr.shape[1] + p += arr.shape[1] - qh_check_maxout(self._qh) - self._qh[0].hasTriangulation = 0 + qh_check_maxout(self._qh) + self._qh[0].hasTriangulation = 0 - if self._is_halfspaces: - self._dual_point_arrays.append(arr) - else: - self._point_arrays.append(arr) - self.numpoints += arr.shape[0] + if self._is_halfspaces: + self._dual_point_arrays.append(arr) + else: + self._point_arrays.append(arr) + self.numpoints += arr.shape[0] - # update facet visibility - with nogil: - qh_findgood_all(self._qh, self._qh[0].facet_list) + # update facet visibility + with nogil: + qh_findgood_all(self._qh, self._qh[0].facet_list) + finally: + self._qh[0].NOerrexit = 1 finally: - self._qh[0].NOerrexit = 1 + self.release_lock() @cython.final def get_paraboloid_shift_scale(self): cdef double paraboloid_scale cdef double paraboloid_shift - self.check_active() + self.acquire_lock() + try: + self.check_active() - if self._qh[0].SCALElast: - paraboloid_scale = self._qh[0].last_newhigh / ( - self._qh[0].last_high - self._qh[0].last_low) - paraboloid_shift = - self._qh[0].last_low * paraboloid_scale - else: - paraboloid_scale = 1.0 - paraboloid_shift = 0.0 + if self._qh[0].SCALElast: + paraboloid_scale = self._qh[0].last_newhigh / ( + self._qh[0].last_high - self._qh[0].last_low) + paraboloid_shift = - self._qh[0].last_low * paraboloid_scale + else: + paraboloid_scale = 1.0 + paraboloid_shift = 0.0 - return paraboloid_scale, paraboloid_shift + return paraboloid_scale, paraboloid_shift + finally: + self.release_lock() @cython.final def volume_area(self): cdef double volume cdef double area - self.check_active() + self.acquire_lock() + try: + self.check_active() - self._qh.hasAreaVolume = 0 - with nogil: - qh_getarea(self._qh, self._qh[0].facet_list) + self._qh.hasAreaVolume = 0 + with nogil: + qh_getarea(self._qh, self._qh[0].facet_list) - volume = self._qh[0].totvol - area = self._qh[0].totarea + volume = self._qh[0].totvol + area = self._qh[0].totarea - return volume, area + return volume, area + finally: + self.release_lock() @cython.final def triangulate(self): - self.check_active() + self.acquire_lock() + try: + self.check_active() - with nogil: - qh_triangulate(self._qh) # get rid of non-simplical facets + with nogil: + qh_triangulate(self._qh) # get rid of non-simplical facets + finally: + self.release_lock() @cython.final @cython.boundscheck(False) @@ -548,120 +591,124 @@ cdef class _Qhull: cdef unsigned int lower_bound cdef unsigned int swapped_index - self.check_active() + self.acquire_lock() + try: + self.check_active() - facet_ndim = self.ndim + facet_ndim = self.ndim - if self._is_halfspaces: - facet_ndim = self.ndim - 1 + if self._is_halfspaces: + facet_ndim = self.ndim - 1 - if self._is_delaunay: - facet_ndim += 1 + if self._is_delaunay: + facet_ndim += 1 - id_map = np.empty(self._qh[0].facet_id, dtype=np.intc) + id_map = np.empty(self._qh[0].facet_id, dtype=np.intc) - # Compute facet indices - with nogil: - for i in range(self._qh[0].facet_id): - id_map[i] = -1 + # Compute facet indices + with nogil: + for i in range(self._qh[0].facet_id): + id_map[i] = -1 + + facet = self._qh[0].facet_list + j = 0 + while facet and facet.next: + if not self._is_delaunay or facet.upperdelaunay == self._qh[0].UPPERdelaunay: + if not facet.simplicial and ( \ + qh_setsize(self._qh, facet.vertices) != facet_ndim or \ + qh_setsize(self._qh, facet.neighbors) != facet_ndim): + with gil: + raise QhullError( + "non-simplical facet encountered: %r vertices" + % (qh_setsize(self._qh, facet.vertices),)) - facet = self._qh[0].facet_list - j = 0 - while facet and facet.next: - if not self._is_delaunay or facet.upperdelaunay == self._qh[0].UPPERdelaunay: - if not facet.simplicial and ( \ - qh_setsize(self._qh, facet.vertices) != facet_ndim or \ - qh_setsize(self._qh, facet.neighbors) != facet_ndim): - with gil: - raise QhullError( - "non-simplical facet encountered: %r vertices" - % (qh_setsize(self._qh, facet.vertices),)) - - id_map[facet.id] = j - j += 1 + id_map[facet.id] = j + j += 1 - facet = facet.next + facet = facet.next - # Allocate output - facets = np.zeros((j, facet_ndim), dtype=np.intc) - good = np.zeros(j, dtype=np.intc) - neighbors = np.zeros((j, facet_ndim), dtype=np.intc) - equations = np.zeros((j, facet_ndim+1), dtype=np.double) + # Allocate output + facets = np.zeros((j, facet_ndim), dtype=np.intc) + good = np.zeros(j, dtype=np.intc) + neighbors = np.zeros((j, facet_ndim), dtype=np.intc) + equations = np.zeros((j, facet_ndim+1), dtype=np.double) - ncoplanar = 0 - coplanar = np.zeros((10, 3), dtype=np.intc) - coplanar_shape = coplanar.shape + ncoplanar = 0 + coplanar = np.zeros((10, 3), dtype=np.intc) + coplanar_shape = coplanar.shape - # Retrieve facet information - with nogil: - facet = self._qh[0].facet_list - j = 0 - while facet and facet.next: - if self._is_delaunay and facet.upperdelaunay != self._qh[0].UPPERdelaunay: - facet = facet.next - continue + # Retrieve facet information + with nogil: + facet = self._qh[0].facet_list + j = 0 + while facet and facet.next: + if self._is_delaunay and facet.upperdelaunay != self._qh[0].UPPERdelaunay: + facet = facet.next + continue - # Use a lower bound so that the tight loop in high dimensions - # is not affected by the conditional below - lower_bound = 0 - if (self._is_delaunay and - facet.toporient == qh_ORIENTclock and facet_ndim == 3): - # Swap the first and second indices to maintain a - # counter-clockwise orientation. - for i in range(2): + # Use a lower bound so that the tight loop in high dimensions + # is not affected by the conditional below + lower_bound = 0 + if (self._is_delaunay and + facet.toporient == qh_ORIENTclock and facet_ndim == 3): + # Swap the first and second indices to maintain a + # counter-clockwise orientation. + for i in range(2): + # Save the vertex info + swapped_index = 1 ^ i + vertex = facet.vertices.e[i].p + ipoint = qh_pointid(self._qh, vertex.point) + facets[j, swapped_index] = ipoint + + # Save the neighbor info + neighbor = facet.neighbors.e[i].p + neighbors[j, swapped_index] = id_map[neighbor.id] + + lower_bound = 2 + + for i in range(lower_bound, facet_ndim): # Save the vertex info - swapped_index = 1 ^ i vertex = facet.vertices.e[i].p ipoint = qh_pointid(self._qh, vertex.point) - facets[j, swapped_index] = ipoint + facets[j, i] = ipoint # Save the neighbor info neighbor = facet.neighbors.e[i].p - neighbors[j, swapped_index] = id_map[neighbor.id] - - lower_bound = 2 - - for i in range(lower_bound, facet_ndim): - # Save the vertex info - vertex = facet.vertices.e[i].p - ipoint = qh_pointid(self._qh, vertex.point) - facets[j, i] = ipoint - - # Save the neighbor info - neighbor = facet.neighbors.e[i].p - neighbors[j, i] = id_map[neighbor.id] - - # Save simplex equation info - for i in range(facet_ndim): - equations[j, i] = facet.normal[i] - equations[j, facet_ndim] = facet.offset + neighbors[j, i] = id_map[neighbor.id] + + # Save simplex equation info + for i in range(facet_ndim): + equations[j, i] = facet.normal[i] + equations[j, facet_ndim] = facet.offset + + # Save coplanar info + if facet.coplanarset: + for i in range(qh_setsize(self._qh, facet.coplanarset)): + point = facet.coplanarset.e[i].p + vertex = qh_nearvertex(self._qh, facet, point, &dist) + + if ncoplanar >= coplanar_shape[0]: + with gil: + tmp = coplanar + coplanar = None + # The array is always safe to resize + tmp.resize(2 * ncoplanar + 1, 3, refcheck=False) + coplanar = tmp + + coplanar[ncoplanar, 0] = qh_pointid(self._qh, point) + coplanar[ncoplanar, 1] = id_map[facet.id] + coplanar[ncoplanar, 2] = qh_pointid(self._qh, vertex.point) + ncoplanar += 1 + + # Save good info + good[j] = facet.good - # Save coplanar info - if facet.coplanarset: - for i in range(qh_setsize(self._qh, facet.coplanarset)): - point = facet.coplanarset.e[i].p - vertex = qh_nearvertex(self._qh, facet, point, &dist) - - if ncoplanar >= coplanar_shape[0]: - with gil: - tmp = coplanar - coplanar = None - # The array is always safe to resize - tmp.resize(2 * ncoplanar + 1, 3, refcheck=False) - coplanar = tmp - - coplanar[ncoplanar, 0] = qh_pointid(self._qh, point) - coplanar[ncoplanar, 1] = id_map[facet.id] - coplanar[ncoplanar, 2] = qh_pointid(self._qh, vertex.point) - ncoplanar += 1 - - # Save good info - good[j] = facet.good - - j += 1 - facet = facet.next + j += 1 + facet = facet.next - return facets, neighbors, equations, coplanar[:ncoplanar], good + return facets, neighbors, equations, coplanar[:ncoplanar], good + finally: + self.release_lock() @cython.final @cython.boundscheck(False) @@ -682,27 +729,32 @@ cdef class _Qhull: cdef int i, j, numpoints, point_ndim cdef np.ndarray[np.npy_double, ndim=2] points - self.check_active() + self.acquire_lock() - point_ndim = self.ndim + try: + self.check_active() - if self._is_halfspaces: - point_ndim -= 1 + point_ndim = self.ndim - if self._is_delaunay: - point_ndim += 1 + if self._is_halfspaces: + point_ndim -= 1 - numpoints = self._qh.num_points - points = np.empty((numpoints, point_ndim)) + if self._is_delaunay: + point_ndim += 1 - with nogil: - point = self._qh.first_point - for i in range(numpoints): - for j in range(point_ndim): - points[i,j] = point[j] - point += self._qh.hull_dim + numpoints = self._qh.num_points + points = np.empty((numpoints, point_ndim)) + + with nogil: + point = self._qh.first_point + for i in range(numpoints): + for j in range(point_ndim): + points[i,j] = point[j] + point += self._qh.hull_dim - return points + return points + finally: + self.release_lock() @cython.final @cython.boundscheck(False) @@ -724,45 +776,49 @@ cdef class _Qhull: cdef np.ndarray[np.double_t, ndim=2] equations cdef list facets, facetsi - self.check_active() - - facet_ndim = self.ndim + self.acquire_lock() + try: + self.check_active() - if self._is_halfspaces: - facet_ndim -= 1 + facet_ndim = self.ndim - if self._is_delaunay: - facet_ndim += 1 + if self._is_halfspaces: + facet_ndim -= 1 - numfacets = self._qh.num_facets - self._qh.num_visible + if self._is_delaunay: + facet_ndim += 1 - facet = self._qh.facet_list - equations = np.empty((numfacets, facet_ndim+1)) + numfacets = self._qh.num_facets - self._qh.num_visible - facets = [] + facet = self._qh.facet_list + equations = np.empty((numfacets, facet_ndim+1)) - i = 0 - while facet and facet.next: - facetsi = [] - j = 0 - for j in range(facet_ndim): - equations[i, j] = facet.normal[j] - equations[i, facet_ndim] = facet.offset + facets = [] - j = 0 - vertex = facet.vertices.e[0].p - while vertex: - # Save the vertex info - ipoint = qh_pointid(self._qh, vertex.point) - facetsi.append(ipoint) - j += 1 - vertex = facet.vertices.e[j].p + i = 0 + while facet and facet.next: + facetsi = [] + j = 0 + for j in range(facet_ndim): + equations[i, j] = facet.normal[j] + equations[i, facet_ndim] = facet.offset + + j = 0 + vertex = facet.vertices.e[0].p + while vertex: + # Save the vertex info + ipoint = qh_pointid(self._qh, vertex.point) + facetsi.append(ipoint) + j += 1 + vertex = facet.vertices.e[j].p - i += 1 - facets.append(facetsi) - facet = facet.next + i += 1 + facets.append(facetsi) + facet = facet.next - return facets, equations + return facets, equations + finally: + self.release_lock() @cython.final @cython.boundscheck(False) @@ -808,101 +864,105 @@ cdef class _Qhull: cdef list regions cdef list cur_region - self.check_active() + self.acquire_lock() + try: + self.check_active() - # -- Grab Voronoi ridges - self._nridges = 0 - self._ridge_error = None - self._ridge_points = np.empty((10, 2), np.intc) - self._ridge_vertices = [] + # -- Grab Voronoi ridges + self._nridges = 0 + self._ridge_error = None + self._ridge_points = np.empty((10, 2), np.intc) + self._ridge_vertices = [] - qh_eachvoronoi_all(self._qh, self, &_visit_voronoi, self._qh[0].UPPERdelaunay, - qh_RIDGEall, 1) + qh_eachvoronoi_all(self._qh, self, &_visit_voronoi, self._qh[0].UPPERdelaunay, + qh_RIDGEall, 1) - self._ridge_points = self._ridge_points[:self._nridges] + self._ridge_points = self._ridge_points[:self._nridges] - if self._ridge_error is not None: - raise self._ridge_error + if self._ridge_error is not None: + raise self._ridge_error - # Now, qh_eachvoronoi_all has initialized the visitids of facets - # to correspond do the Voronoi vertex indices. + # Now, qh_eachvoronoi_all has initialized the visitids of facets + # to correspond do the Voronoi vertex indices. - # -- Grab Voronoi regions - regions = [] + # -- Grab Voronoi regions + regions = [] - point_region = np.empty(self.numpoints, np.intp) - for i in range(self.numpoints): - point_region[i] = -1 + point_region = np.empty(self.numpoints, np.intp) + for i in range(self.numpoints): + point_region[i] = -1 - vertex = self._qh[0].vertex_list - while vertex and vertex.next: - qh_order_vertexneighbors_nd(self._qh, self.ndim+1, vertex) + vertex = self._qh[0].vertex_list + while vertex and vertex.next: + qh_order_vertexneighbors_nd(self._qh, self.ndim+1, vertex) - i = qh_pointid(self._qh, vertex.point) - if i < self.numpoints: - # Qz results to one extra point - point_region[i] = len(regions) + i = qh_pointid(self._qh, vertex.point) + if i < self.numpoints: + # Qz results to one extra point + point_region[i] = len(regions) - inf_seen = 0 - cur_region = [] - for k in range(qh_setsize(self._qh, vertex.neighbors)): - neighbor = vertex.neighbors.e[k].p - i = neighbor.visitid - 1 - if i == -1: - if not inf_seen: - inf_seen = 1 - else: - continue - cur_region.append(int(i)) - if len(cur_region) == 1 and cur_region[0] == -1: - # report similarly as qvoronoi o + inf_seen = 0 cur_region = [] - regions.append(cur_region) - - vertex = vertex.next + for k in range(qh_setsize(self._qh, vertex.neighbors)): + neighbor = vertex.neighbors.e[k].p + i = neighbor.visitid - 1 + if i == -1: + if not inf_seen: + inf_seen = 1 + else: + continue + cur_region.append(int(i)) + if len(cur_region) == 1 and cur_region[0] == -1: + # report similarly as qvoronoi o + cur_region = [] + regions.append(cur_region) + + vertex = vertex.next + + # -- Grab Voronoi vertices and point-to-region map + nvoronoi_vertices = 0 + voronoi_vertices = np.empty((10, self.ndim), np.double) - # -- Grab Voronoi vertices and point-to-region map - nvoronoi_vertices = 0 - voronoi_vertices = np.empty((10, self.ndim), np.double) - - facet = self._qh[0].facet_list - while facet and facet.next: - if facet.visitid > 0: - # finite Voronoi vertex + facet = self._qh[0].facet_list + while facet and facet.next: + if facet.visitid > 0: + # finite Voronoi vertex - center = qh_facetcenter(self._qh, facet.vertices) + center = qh_facetcenter(self._qh, facet.vertices) - nvoronoi_vertices = max(facet.visitid, nvoronoi_vertices) - if nvoronoi_vertices >= voronoi_vertices.shape[0]: - tmp = voronoi_vertices - voronoi_vertices = None - # Array is safe to resize - tmp.resize(2*nvoronoi_vertices + 1, self.ndim, refcheck=False) - voronoi_vertices = tmp + nvoronoi_vertices = max(facet.visitid, nvoronoi_vertices) + if nvoronoi_vertices >= voronoi_vertices.shape[0]: + tmp = voronoi_vertices + voronoi_vertices = None + # Array is safe to resize + tmp.resize(2*nvoronoi_vertices + 1, self.ndim, refcheck=False) + voronoi_vertices = tmp - for k in range(self.ndim): - voronoi_vertices[facet.visitid-1, k] = center[k] + for k in range(self.ndim): + voronoi_vertices[facet.visitid-1, k] = center[k] - qh_memfree(self._qh, center, self._qh[0].center_size) + qh_memfree(self._qh, center, self._qh[0].center_size) - if facet.coplanarset: - for k in range(qh_setsize(self._qh, facet.coplanarset)): - point = facet.coplanarset.e[k].p - vertex = qh_nearvertex(self._qh, facet, point, &dist) + if facet.coplanarset: + for k in range(qh_setsize(self._qh, facet.coplanarset)): + point = facet.coplanarset.e[k].p + vertex = qh_nearvertex(self._qh, facet, point, &dist) - i = qh_pointid(self._qh, point) - j = qh_pointid(self._qh, vertex.point) + i = qh_pointid(self._qh, point) + j = qh_pointid(self._qh, vertex.point) - if i < self.numpoints: - # Qz can result to one extra point - point_region[i] = point_region[j] + if i < self.numpoints: + # Qz can result to one extra point + point_region[i] = point_region[j] - facet = facet.next + facet = facet.next - voronoi_vertices = voronoi_vertices[:nvoronoi_vertices] + voronoi_vertices = voronoi_vertices[:nvoronoi_vertices] - return voronoi_vertices, self._ridge_points, self._ridge_vertices, \ - regions, point_region + return voronoi_vertices, self._ridge_points, self._ridge_vertices, \ + regions, point_region + finally: + self.release_lock() @cython.final @cython.boundscheck(False) @@ -923,59 +983,63 @@ cdef class _Qhull: cdef int[:] extremes cdef int nextremes - self.check_active() + self.acquire_lock() + try: + self.check_active() - if self._is_delaunay: - raise ValueError("Cannot compute for Delaunay") + if self._is_delaunay: + raise ValueError("Cannot compute for Delaunay") - nextremes = 0 - extremes_arr = np.zeros(100, dtype=np.intc) - extremes = extremes_arr + nextremes = 0 + extremes_arr = np.zeros(100, dtype=np.intc) + extremes = extremes_arr - self._qh[0].visit_id += 1 - self._qh[0].vertex_visit += 1 + self._qh[0].visit_id += 1 + self._qh[0].vertex_visit += 1 - facet = self._qh[0].facet_list - startfacet = facet - while facet: - if facet.visitid == self._qh[0].visit_id: - raise QhullError("Qhull internal error: loop in facet list") + facet = self._qh[0].facet_list + startfacet = facet + while facet: + if facet.visitid == self._qh[0].visit_id: + raise QhullError("Qhull internal error: loop in facet list") + + if facet.toporient: + vertexA = facet.vertices.e[0].p + vertexB = facet.vertices.e[1].p + nextfacet = facet.neighbors.e[0].p + else: + vertexB = facet.vertices.e[0].p + vertexA = facet.vertices.e[1].p + nextfacet = facet.neighbors.e[1].p - if facet.toporient: - vertexA = facet.vertices.e[0].p - vertexB = facet.vertices.e[1].p - nextfacet = facet.neighbors.e[0].p - else: - vertexB = facet.vertices.e[0].p - vertexA = facet.vertices.e[1].p - nextfacet = facet.neighbors.e[1].p - - if nextremes + 2 >= extremes.shape[0]: - extremes = None - # Array is safe to resize - extremes_arr.resize(2*extremes_arr.shape[0]+1, refcheck=False) - extremes = extremes_arr - - if vertexA.visitid != self._qh[0].vertex_visit: - vertexA.visitid = self._qh[0].vertex_visit - extremes[nextremes] = qh_pointid(self._qh, vertexA.point) - nextremes += 1 - - if vertexB.visitid != self._qh[0].vertex_visit: - vertexB.visitid = self._qh[0].vertex_visit - extremes[nextremes] = qh_pointid(self._qh, vertexB.point) - nextremes += 1 - - facet.visitid = self._qh[0].visit_id - facet = nextfacet - - if facet == startfacet: - break + if nextremes + 2 >= extremes.shape[0]: + extremes = None + # Array is safe to resize + extremes_arr.resize(2*extremes_arr.shape[0]+1, refcheck=False) + extremes = extremes_arr + + if vertexA.visitid != self._qh[0].vertex_visit: + vertexA.visitid = self._qh[0].vertex_visit + extremes[nextremes] = qh_pointid(self._qh, vertexA.point) + nextremes += 1 + + if vertexB.visitid != self._qh[0].vertex_visit: + vertexB.visitid = self._qh[0].vertex_visit + extremes[nextremes] = qh_pointid(self._qh, vertexB.point) + nextremes += 1 - extremes = None - # This array is always safe to resize - extremes_arr.resize(nextremes, refcheck=False) - return extremes_arr + facet.visitid = self._qh[0].visit_id + facet = nextfacet + + if facet == startfacet: + break + + extremes = None + # This array is always safe to resize + extremes_arr.resize(nextremes, refcheck=False) + return extremes_arr + finally: + self.release_lock() cdef void _visit_voronoi(qhT *_qh, FILE *ptr, vertexT *vertex, vertexT *vertexA, diff --git a/scipy/spatial/_voronoi.pyx b/scipy/spatial/_voronoi.pyx index 662068d138f0..70ce781106fa 100644 --- a/scipy/spatial/_voronoi.pyx +++ b/scipy/spatial/_voronoi.pyx @@ -20,7 +20,7 @@ __all__ = ['sort_vertices_of_regions'] @cython.boundscheck(False) -def sort_vertices_of_regions(int[:,::1] simplices, list regions): +def sort_vertices_of_regions(const int[:,::1] simplices, list regions): cdef np.npy_intp n, k, s, i, max_len cdef np.npy_intp num_regions = len(regions) cdef np.npy_intp current_simplex, current_vertex diff --git a/scipy/spatial/tests/test_kdtree.py b/scipy/spatial/tests/test_kdtree.py index b710c9df6a8f..462907ef9a58 100644 --- a/scipy/spatial/tests/test_kdtree.py +++ b/scipy/spatial/tests/test_kdtree.py @@ -971,7 +971,7 @@ def test_kdtree_list_k(kdtree_type): assert_equal(dd, np.ravel(dd1)) assert_equal(ii, np.ravel(ii1)) -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) def test_kdtree_box(kdtree_type): # check ckdtree periodic boundary n = 2000 @@ -1146,7 +1146,7 @@ def test_kdtree_weights(kdtree_type): assert_raises(ValueError, tree1.count_neighbors, tree2, np.linspace(0, 10, 100), weights=w1) -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(10) def test_kdtree_count_neighbous_multiple_r(kdtree_type): n = 2000 m = 2 diff --git a/scipy/spatial/tests/test_qhull.py b/scipy/spatial/tests/test_qhull.py index 94ed6c40a8dd..66b3c4f96193 100644 --- a/scipy/spatial/tests/test_qhull.py +++ b/scipy/spatial/tests/test_qhull.py @@ -327,7 +327,7 @@ def barycentric_transform(tr, x): ok = (j != -1) | at_boundary assert_(ok.all(), f"{err_msg} {np.nonzero(~ok)}") - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) def test_degenerate_barycentric_transforms(self): # The triangulation should not produce invalid barycentric # transforms that stump the simplex finding @@ -346,7 +346,7 @@ def test_degenerate_barycentric_transforms(self): self._check_barycentric_transforms(tri) @pytest.mark.slow - @pytest.mark.fail_slow(10) + @pytest.mark.fail_slow(20) # OK per https://github.com/scipy/scipy/pull/20487#discussion_r1572684869 def test_more_barycentric_transforms(self): # Triangulate some "nasty" grids @@ -962,7 +962,7 @@ def test_furthest_site_flag(self): vor = Voronoi(points,furthest_site=True) assert_equal(vor.furthest_site,True) - @pytest.mark.fail_slow(5) + @pytest.mark.fail_slow(10) @pytest.mark.parametrize("name", sorted(INCREMENTAL_DATASETS)) def test_incremental(self, name): # Test incremental construction of the triangulation diff --git a/scipy/spatial/transform/_rotation.pyx b/scipy/spatial/transform/_rotation.pyx index 2a9bfc89fd8e..52db31c24da2 100644 --- a/scipy/spatial/transform/_rotation.pyx +++ b/scipy/spatial/transform/_rotation.pyx @@ -63,7 +63,7 @@ cdef inline double _normalize4(double[:] elems) noexcept nogil: @cython.boundscheck(False) @cython.wraparound(False) -cdef inline int _argmax4(double[:] a) noexcept nogil: +cdef inline int _argmax4(const double[:] a) noexcept nogil: cdef int imax = 0 cdef double vmax = a[0] @@ -86,7 +86,7 @@ cdef inline const double[:] _elementary_basis_vector(uchar axis) noexcept: if axis == b'x': return _ex elif axis == b'y': return _ey elif axis == b'z': return _ez - + @cython.boundscheck(False) @cython.wraparound(False) cdef inline int _elementary_basis_index(uchar axis) noexcept: @@ -118,9 +118,9 @@ cdef inline void _quat_canonical(double[:, :] q) noexcept: @cython.boundscheck(False) @cython.wraparound(False) cdef inline void _get_angles( - double[:] angles, bint extrinsic, bint symmetric, bint sign, + double[:] angles, bint extrinsic, bint symmetric, bint sign, double lamb, double a, double b, double c, double d): - + # intrinsic/extrinsic conversion helpers cdef int angle_first, angle_third if extrinsic: @@ -149,18 +149,18 @@ cdef inline void _get_angles( # compute first and third angles, according to case half_sum = atan2(b, a) half_diff = atan2(d, c) - + if case == 0: # no singularities angles[angle_first] = half_sum - half_diff angles[angle_third] = half_sum + half_diff - + else: # any degenerate case angles[2] = 0 if case == 1: angles[0] = 2 * half_sum else: angles[0] = 2 * half_diff * (-1 if extrinsic else 1) - + # for Tait-Bryan/asymmetric sequences if not symmetric: angles[angle_third] *= sign @@ -328,12 +328,12 @@ cdef double[:, :] _compute_euler_from_quat( # The algorithm assumes extrinsic frame transformations. The algorithm # in the paper is formulated for rotation quaternions, which are stored # directly by Rotation. - # Adapt the algorithm for our case by reversing both axis sequence and + # Adapt the algorithm for our case by reversing both axis sequence and # angles for intrinsic rotations when needed - + if not extrinsic: seq = seq[::-1] - + cdef int i = _elementary_basis_index(seq[0]) cdef int j = _elementary_basis_index(seq[1]) cdef int k = _elementary_basis_index(seq[2]) @@ -341,9 +341,9 @@ cdef double[:, :] _compute_euler_from_quat( cdef bint symmetric = i == k if symmetric: k = 3 - i - j # get third axis - + # Step 0 - # Check if permutation is even (+1) or odd (-1) + # Check if permutation is even (+1) or odd (-1) cdef int sign = (i - j) * (j - k) * (k - i) // 2 cdef Py_ssize_t num_rotations = quat.shape[0] @@ -355,7 +355,7 @@ cdef double[:, :] _compute_euler_from_quat( for ind in range(num_rotations): # Step 1 - # Permutate quaternion elements + # Permutate quaternion elements if symmetric: a = quat[ind, 3] b = quat[ind, i] @@ -1392,9 +1392,9 @@ cdef class Rotation: (extrinsic) or in a body centred frame of reference (intrinsic), which is attached to, and moves with, the object under rotation [1]_. - For both Euler angles and Davenport angles, consecutive axes must - be are orthogonal (``axis2`` is orthogonal to both ``axis1`` and - ``axis3``). For Euler angles, there is an additional relationship + For both Euler angles and Davenport angles, consecutive axes must + be are orthogonal (``axis2`` is orthogonal to both ``axis1`` and + ``axis3``). For Euler angles, there is an additional relationship between ``axis1`` or ``axis3``, with two possibilities: - ``axis1`` and ``axis3`` are also orthogonal (asymmetric sequence) @@ -1406,13 +1406,13 @@ cdef class Rotation: Parameters ---------- axes : array_like, shape (3,) or ([1 or 2 or 3], 3) - Axis of rotation, if one dimensional. If two dimensional, describes the + Axis of rotation, if one dimensional. If two dimensional, describes the sequence of axes for rotations, where each axes[i, :] is the ith axis. If more than one axis is given, then the second axis must be orthogonal to both the first and third axes. order : string - If it is equal to 'e' or 'extrinsic', the sequence will be - extrinsic. If it is equal to 'i' or 'intrinsic', sequence + If it is equal to 'e' or 'extrinsic', the sequence will be + extrinsic. If it is equal to 'i' or 'intrinsic', sequence will be treated as intrinsic. angles : float or array_like, shape (N,) or (N, [1 or 2 or 3]) Euler angles specified in radians (`degrees` is False) or degrees @@ -1430,11 +1430,11 @@ cdef class Rotation: - array_like with shape (W,) where `W` is the number of rows of `axes`, which corresponds to a single rotation with `W` axes - array_like with shape (N, W) where each `angle[i]` - corresponds to a sequence of Davenport angles describing a + corresponds to a sequence of Davenport angles describing a single rotation degrees : bool, optional - If True, then the given angles are assumed to be in degrees. + If True, then the given angles are assumed to be in degrees. Default is False. Returns @@ -1520,10 +1520,10 @@ cdef class Rotation: norm = np.repeat(np.linalg.norm(axes, axis=1), 3) axes = axes / norm.reshape(num_axes, 3) - if (num_axes > 1 and abs(np.dot(axes[0], axes[1])) >= 1e-7 or + if (num_axes > 1 and abs(np.dot(axes[0], axes[1])) >= 1e-7 or num_axes > 2 and abs(np.dot(axes[1], axes[2])) >= 1e-7): raise ValueError("Consecutive axes must be orthogonal.") - + angles, is_single = _format_angles(angles, degrees, num_axes) q = Rotation.identity(len(angles)) @@ -1665,7 +1665,7 @@ cdef class Rotation: The mapping from quaternions to rotations is two-to-one, i.e. quaternions ``q`` and ``-q``, where ``-q`` simply reverses the sign of each component, represent the same spatial - rotation. + rotation. Parameters ---------- @@ -1937,7 +1937,7 @@ cdef class Rotation: @cython.embedsignature(True) def _compute_euler(self, seq, degrees, algorithm): # Prepare axis sequence to call Euler angles conversion algorithm. - + if len(seq) != 3: raise ValueError("Expected 3 axes, got {}.".format(seq)) @@ -1953,7 +1953,7 @@ cdef class Rotation: "got {}".format(seq)) seq = seq.lower() - + if algorithm == 'from_matrix': matrix = self.as_matrix() if matrix.ndim == 2: @@ -1969,7 +1969,7 @@ cdef class Rotation: else: # algorithm can only be 'from_quat' or 'from_matrix' assert False - + if degrees: angles = np.rad2deg(angles) @@ -2037,7 +2037,7 @@ cdef class Rotation: rotations. Once the axis sequence has been chosen, Euler angles define the angle of rotation around each respective axis [1]_. - The algorithm from [2]_ has been used to calculate Euler angles for the + The algorithm from [2]_ has been used to calculate Euler angles for the rotation about a given sequence of axes. Euler angles suffer from the problem of gimbal lock [3]_, where the @@ -2075,9 +2075,9 @@ cdef class Rotation: References ---------- .. [1] https://en.wikipedia.org/wiki/Euler_angles#Definition_by_intrinsic_rotations - .. [2] Bernardes E, Viollet S (2022) Quaternion to Euler angles - conversion: A direct, general and computationally efficient - method. PLoS ONE 17(11): e0276302. + .. [2] Bernardes E, Viollet S (2022) Quaternion to Euler angles + conversion: A direct, general and computationally efficient + method. PLoS ONE 17(11): e0276302. https://doi.org/10.1371/journal.pone.0276302 .. [3] https://en.wikipedia.org/wiki/Gimbal_lock#In_applied_mathematics @@ -2125,9 +2125,9 @@ cdef class Rotation: Any orientation can be expressed as a composition of 3 elementary rotations. - For both Euler angles and Davenport angles, consecutive axes must - be are orthogonal (``axis2`` is orthogonal to both ``axis1`` and - ``axis3``). For Euler angles, there is an additional relationship + For both Euler angles and Davenport angles, consecutive axes must + be are orthogonal (``axis2`` is orthogonal to both ``axis1`` and + ``axis3``). For Euler angles, there is an additional relationship between ``axis1`` or ``axis3``, with two possibilities: - ``axis1`` and ``axis3`` are also orthogonal (asymmetric sequence) @@ -2150,13 +2150,13 @@ cdef class Rotation: Parameters ---------- axes : array_like, shape (3,) or ([1 or 2 or 3], 3) - Axis of rotation, if one dimensional. If two dimensional, describes the + Axis of rotation, if one dimensional. If two dimensional, describes the sequence of axes for rotations, where each axes[i, :] is the ith axis. If more than one axis is given, then the second axis must be orthogonal to both the first and third axes. order : string - If it belongs to the set {'e', 'extrinsic'}, the sequence will be - extrinsic. If if belongs to the set {'i', 'intrinsic'}, sequence + If it belongs to the set {'e', 'extrinsic'}, the sequence will be + extrinsic. If if belongs to the set {'i', 'intrinsic'}, sequence will be treated as intrinsic. degrees : boolean, optional Returned angles are in degrees if this flag is True, else they are @@ -2170,7 +2170,7 @@ cdef class Rotation: - First angle belongs to [-180, 180] degrees (both inclusive) - Third angle belongs to [-180, 180] degrees (both inclusive) - - Second angle belongs to a set of size 180 degrees, + - Second angle belongs to a set of size 180 degrees, given by: ``[-abs(lambda), 180 - abs(lambda)]``, where ``lambda`` is the angle between the first and third axes. diff --git a/scipy/special/__init__.py b/scipy/special/__init__.py index a86d6f9f69f8..90c288585c8d 100644 --- a/scipy/special/__init__.py +++ b/scipy/special/__init__.py @@ -810,7 +810,8 @@ def _load_libsf_error_state(): # Replace some function definitions from _ufuncs to add Array API support from ._support_alternative_backends import ( log_ndtr, ndtr, ndtri, erf, erfc, i0, i0e, i1, i1e, gammaln, - gammainc, gammaincc, logit, expit, entr, rel_entr, xlogy, chdtrc) + gammainc, gammaincc, logit, expit, entr, rel_entr, xlogy, + chdtr, chdtrc, betainc, betaincc, stdtr) from . import _basic from ._basic import * diff --git a/scipy/special/_basic.py b/scipy/special/_basic.py index ca54215b533f..c041a1084ff0 100644 --- a/scipy/special/_basic.py +++ b/scipy/special/_basic.py @@ -3020,8 +3020,8 @@ def factorial(n, exact=False): return math.factorial(n) elif exact: msg = ("Non-integer values of `n` together with `exact=True` are " - "deprecated. Either ensure integer `n` or use `exact=False`.") - warnings.warn(msg, DeprecationWarning, stacklevel=2) + "not supported. Either ensure integer `n` or use `exact=False`.") + raise ValueError(msg) return _factorialx_approx_core(n, k=1) # arrays & array-likes diff --git a/scipy/special/_convex_analysis.pxd b/scipy/special/_convex_analysis.pxd index 2fe95b95ccaf..aff10cb0562f 100644 --- a/scipy/special/_convex_analysis.pxd +++ b/scipy/special/_convex_analysis.pxd @@ -1,4 +1,6 @@ from libc.math cimport log, fabs, expm1, log1p, isnan, NAN, INFINITY +from libc.float cimport DBL_MIN +import cython cdef inline double entr(double x) noexcept nogil: if isnan(x): @@ -20,15 +22,26 @@ cdef inline double kl_div(double x, double y) noexcept nogil: else: return INFINITY +@cython.cdivision(True) cdef inline double rel_entr(double x, double y) noexcept nogil: + cdef double ratio if isnan(x) or isnan(y): return NAN - elif x > 0 and y > 0: - return x * log(x / y) - elif x == 0 and y >= 0: - return 0 - else: + if x <= 0 or y <= 0: + if x == 0 and y >= 0: + return 0 return INFINITY + ratio = x / y + if 0.5 < ratio < 2: + # When x and y are close, this is more accurate + return x * log1p((x - y) / y) + if DBL_MIN < ratio < INFINITY: + # There are no underflow/overflow issues + return x * log(ratio) + # x and y are so far apart that taking x / y + # results in either an underflow, overflow, + # or subnormal number. Do the logarithm first + return x * (log(x) - log(y)) cdef inline double huber(double delta, double r) noexcept nogil: if delta < 0: diff --git a/scipy/special/_gufuncs.cpp b/scipy/special/_gufuncs.cpp index 90ee4d69baf7..d35edf3ab096 100644 --- a/scipy/special/_gufuncs.cpp +++ b/scipy/special/_gufuncs.cpp @@ -53,7 +53,7 @@ extern const char *sph_harm_all_doc; extern "C" int wrap_PyUFunc_getfperr() { return PyUFunc_getfperr(); } static PyModuleDef _gufuncs_def = { - PyModuleDef_HEAD_INIT, + .m_base = PyModuleDef_HEAD_INIT, .m_name = "_gufuncs", .m_size = -1, }; diff --git a/scipy/special/_orthogonal.py b/scipy/special/_orthogonal.py index c4b49cd8c6d4..e444195ae12b 100644 --- a/scipy/special/_orthogonal.py +++ b/scipy/special/_orthogonal.py @@ -239,7 +239,6 @@ def roots_jacobi(n, alpha, beta, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -422,7 +421,6 @@ def roots_sh_jacobi(n, p1, q1, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -534,7 +532,6 @@ def roots_genlaguerre(n, alpha, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -704,7 +701,6 @@ def roots_laguerre(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad numpy.polynomial.laguerre.laggauss @@ -843,7 +839,6 @@ def roots_hermite(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad numpy.polynomial.hermite.hermgauss roots_hermitenorm @@ -1375,7 +1370,6 @@ def roots_hermitenorm(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad numpy.polynomial.hermite_e.hermegauss @@ -1508,7 +1502,6 @@ def roots_gegenbauer(n, alpha, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -1668,7 +1661,6 @@ def roots_chebyt(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad numpy.polynomial.chebyshev.chebgauss @@ -1829,7 +1821,6 @@ def roots_chebyu(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -1979,7 +1970,6 @@ def roots_chebyc(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -2085,7 +2075,6 @@ def roots_chebys(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -2192,7 +2181,6 @@ def roots_sh_chebyt(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -2272,7 +2260,6 @@ def roots_sh_chebyu(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References @@ -2355,7 +2342,6 @@ def roots_legendre(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad numpy.polynomial.legendre.leggauss @@ -2538,7 +2524,6 @@ def roots_sh_legendre(n, mu=False): See Also -------- - scipy.integrate.quadrature scipy.integrate.fixed_quad References diff --git a/scipy/special/_special_ufuncs.cpp b/scipy/special/_special_ufuncs.cpp index b9c2dfc0672b..0ce889b9c43d 100644 --- a/scipy/special/_special_ufuncs.cpp +++ b/scipy/special/_special_ufuncs.cpp @@ -206,7 +206,7 @@ extern const char *yve_doc; extern "C" int wrap_PyUFunc_getfperr() { return PyUFunc_getfperr(); } static PyModuleDef _special_ufuncs_def = { - PyModuleDef_HEAD_INIT, + .m_base = PyModuleDef_HEAD_INIT, .m_name = "_special_ufuncs", .m_size = -1, }; diff --git a/scipy/special/_support_alternative_backends.py b/scipy/special/_support_alternative_backends.py index 1be09d29cc2f..dd5ee1fc755f 100644 --- a/scipy/special/_support_alternative_backends.py +++ b/scipy/special/_support_alternative_backends.py @@ -13,7 +13,7 @@ from ._ufuncs import ( log_ndtr, ndtr, ndtri, erf, erfc, i0, i0e, i1, i1e, gammaln, # noqa: F401 gammainc, gammaincc, logit, expit, entr, rel_entr, xlogy, # noqa: F401 - chdtrc # noqa: F401 + chdtr, chdtrc, betainc, betaincc, stdtr # noqa: F401 ) _SCIPY_ARRAY_API = os.environ.get("SCIPY_ARRAY_API", False) @@ -81,11 +81,30 @@ def __xlogy(x, y, *, xp=xp): return __xlogy +def _chdtr(xp, spx): + # The difference between this and just using `gammainc` + # defined by `get_array_special_func` is that if `gammainc` + # isn't found, we don't want to use the SciPy version; we'll + # return None here and use the SciPy version of `chdtr`. + gammainc = getattr(spx, 'gammainc', None) # noqa: F811 + if gammainc is None and hasattr(xp, 'special'): + gammainc = getattr(xp.special, 'gammainc', None) + if gammainc is None: + return None + + def __chdtr(v, x): + res = xp.where(x >= 0, gammainc(v/2, x/2), 0) + i_nan = ((x == 0) & (v == 0)) | xp.isnan(x) | xp.isnan(v) + res = xp.where(i_nan, xp.nan, res) + return res + return __chdtr + + def _chdtrc(xp, spx): # The difference between this and just using `gammaincc` # defined by `get_array_special_func` is that if `gammaincc` # isn't found, we don't want to use the SciPy version; we'll - # return None here and use the SciPy version of `chdtrc`.. + # return None here and use the SciPy version of `chdtrc`. gammaincc = getattr(spx, 'gammaincc', None) # noqa: F811 if gammaincc is None and hasattr(xp, 'special'): gammaincc = getattr(xp.special, 'gammaincc', None) @@ -100,9 +119,41 @@ def __chdtrc(v, x): return __chdtrc +def _betaincc(xp, spx): + betainc = getattr(spx, 'betainc', None) # noqa: F811 + if betainc is None and hasattr(xp, 'special'): + betainc = getattr(xp.special, 'betainc', None) + if betainc is None: + return None + + def __betaincc(a, b, x): + # not perfect; might want to just rely on SciPy + return betainc(b, a, 1-x) + return __betaincc + + +def _stdtr(xp, spx): + betainc = getattr(spx, 'betainc', None) # noqa: F811 + if betainc is None and hasattr(xp, 'special'): + betainc = getattr(xp.special, 'betainc', None) + if betainc is None: + return None + + def __stdtr(df, t): + x = df / (t ** 2 + df) + tail = betainc(df / 2, xp.asarray(0.5), x) / 2 + return xp.where(x < 0, tail, 1 - tail) + + return __stdtr + + _generic_implementations = {'rel_entr': _rel_entr, 'xlogy': _xlogy, - 'chdtrc': _chdtrc} + 'chdtr,': _chdtr, + 'chdtrc': _chdtrc, + 'betaincc': _betaincc, + 'stdtr': _stdtr, + } # functools.wraps doesn't work because: @@ -137,7 +188,11 @@ def wrapped(*args, **kwargs): 'entr': 1, 'rel_entr': 2, 'xlogy': 2, + 'chdtr': 2, 'chdtrc': 2, + 'betainc': 3, + 'betaincc': 3, + 'stdtr': 2, } for f_name, n_array_args in array_special_func_map.items(): diff --git a/scipy/special/special/gamma.h b/scipy/special/special/gamma.h index f0cb1ec41680..970706ff51f5 100644 --- a/scipy/special/special/gamma.h +++ b/scipy/special/special/gamma.h @@ -15,9 +15,18 @@ SPECFUN_HOST_DEVICE T gammaln(T x) { return cephes::lgam(x); } -template -SPECFUN_HOST_DEVICE inline std::complex gamma(std::complex z) { - return gamma(z); +SPECFUN_HOST_DEVICE inline std::complex gamma(std::complex z) { + // Compute Gamma(z) using loggamma. + if (z.real() <= 0 && z == std::floor(z.real())) { + // Poles + set_error("gamma", SF_ERROR_SINGULAR, NULL); + return {std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()}; + } + return std::exp(loggamma(z)); +} + +SPECFUN_HOST_DEVICE inline std::complex gamma(std::complex z) { + return static_cast>(gamma(static_cast>(z))); } } // namespace special diff --git a/scipy/special/special/hyp2f1.h b/scipy/special/special/hyp2f1.h index a4ea56513e53..a8f8e80e4300 100644 --- a/scipy/special/special/hyp2f1.h +++ b/scipy/special/special/hyp2f1.h @@ -196,8 +196,8 @@ namespace detail { SPECFUN_HOST_DEVICE inline double four_gammas(double u, double v, double w, double x) { double result; - // Without loss of generality, assume |u| >= |v| and |w| >= |x|. - if (std::abs(u) > std::abs(v)) { + // Without loss of generality, ensure |u| >= |v| and |w| >= |x|. + if (std::abs(v) > std::abs(u)) { std::swap(u, v); } if (std::abs(x) > std::abs(w)) { diff --git a/scipy/special/special/loggamma.h b/scipy/special/special/loggamma.h index 1002d1d83332..a74770fb8c28 100644 --- a/scipy/special/special/loggamma.h +++ b/scipy/special/special/loggamma.h @@ -143,16 +143,6 @@ SPECFUN_HOST_DEVICE inline std::complex loggamma(std::complex z) { return static_cast>(loggamma(static_cast>(z))); } -SPECFUN_HOST_DEVICE inline std::complex gamma(std::complex z) { - // Compute Gamma(z) using loggamma. - if (z.real() <= 0 && z == std::floor(z.real())) { - // Poles - set_error("gamma", SF_ERROR_SINGULAR, NULL); - return {std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()}; - } - return std::exp(loggamma(z)); -} - SPECFUN_HOST_DEVICE inline double rgamma(double z) { return cephes::rgamma(z); } SPECFUN_HOST_DEVICE inline float rgamma(float z) { return rgamma(static_cast(z)); } diff --git a/scipy/special/special/mdspan.h b/scipy/special/special/mdspan.h deleted file mode 100644 index 4dce5c3805b1..000000000000 --- a/scipy/special/special/mdspan.h +++ /dev/null @@ -1,311 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -namespace std { - -template -class extents; - -namespace detail { - - template - struct fill_extents { - using type = typename fill_extents::type; - }; - - template - struct fill_extents { - using type = extents; - }; - - template - using fill_extents_t = typename fill_extents::type; - -} // namespace detail - -inline constexpr size_t dynamic_extent = numeric_limits::max(); - -template -class extents { - static_assert(((Extents == dynamic_extent) && ... && true), "extents must all be dynamic"); - - public: - using index_type = Index; - using size_type = make_unsigned_t; - using rank_type = size_t; - - private: - array m_dexts; - - public: - constexpr extents() = default; - - template - constexpr explicit extents(OtherIndex... exts) : m_dexts{exts...} {} - - template - constexpr extents(const array &dexts) noexcept : m_dexts(dexts) {} - - constexpr index_type extent(rank_type i) const noexcept { return m_dexts[i]; } - - static constexpr rank_type rank() noexcept { return sizeof...(Extents); } -}; - -template -using dextents = detail::fill_extents_t; - -struct full_extent_t { - explicit full_extent_t() = default; -}; - -inline constexpr full_extent_t full_extent; - -template -struct strided_slice { - using offset_type = Offset; - using extent_type = Extent; - using stride_type = Stride; - - strided_slice() = default; - - strided_slice(offset_type offset, extent_type extent, stride_type stride) - : offset(offset), extent(extent), stride(stride) {} - - offset_type offset; - extent_type extent; - stride_type stride; -}; - -namespace detail { - - template - Index submdspan_extent(Index ext, strided_slice slice) { - return (slice.extent - slice.offset) / slice.stride; - } - - template - Index submdspan_extent(Index ext, std::tuple slice) { - return std::get<1>(slice) - std::get<0>(slice); - } - - template - Index submdspan_extent(Index ext, full_extent_t slice) { - return ext; - } - - template - auto submdspan_extents(std::index_sequence, const extents exts, Slices... slices) { - return extents{submdspan_extent(exts.extent(I), slices)...}; - } - - template - auto submdspan_extents(const extents exts, Slices... slices) { - return submdspan_extents(std::index_sequence_for(), exts, slices...); - } - -} // namespace detail - -template -auto submdspan_extents(const extents &exts, Slices... slices) { - return detail::submdspan_extents(exts, slices...); -} - -template -auto submdspan_mapping(const Mapping &, Slices...); - -struct layout_left; - -struct layout_right; - -struct layout_stride { - template - class mapping { - public: - using extents_type = Extents; - using index_type = typename extents_type::index_type; - using size_type = typename extents_type::size_type; - using rank_type = typename extents_type::rank_type; - using layout_type = layout_stride; - - private: - extents_type m_exts; - array m_strides; - - public: - constexpr mapping() = default; - - constexpr mapping(const Extents &exts, const array &strides) - : m_exts(exts), m_strides(strides) {} - - constexpr const extents_type &extents() const noexcept { return m_exts; } - - constexpr const array &strides() const noexcept { return m_strides; } - - constexpr index_type extent(rank_type i) const noexcept { return m_exts.extent(i); } - - constexpr index_type stride(rank_type i) const noexcept { return m_strides[i]; } - - template - constexpr index_type operator()(Args... args) const noexcept { - static_assert(sizeof...(Args) == extents_type::rank(), "index must have same rank as extents"); - - index_type indices[extents_type::rank()] = {args...}; - index_type res = 0; - for (rank_type i = 0; i < extents_type::rank(); ++i) { - res += indices[i] * m_strides[i]; - } - - return res; - } - }; -}; - -namespace detail { - - template - Index submdspan_stride(Index stride, strided_slice slice) { - return stride * slice.stride; - } - - template - Index submdspan_stride(Index stride, std::tuple slice) { - return stride; - } - - template - Index submdspan_stride(Index stride, full_extent_t slice) { - return stride; - } - - template - auto submdspan_strides(std::index_sequence, const array strides, Slices... slices) { - array res{submdspan_stride(strides[I], slices)...}; - return res; - } - - template - auto submdspan_strides(const array strides, Slices... slices) { - return submdspan_strides(std::index_sequence_for(), strides, slices...); - } - -} // namespace detail - -template -auto submdspan_strides(const array &strides, Slices... slices) { - return detail::submdspan_strides(strides, slices...); -} - -template -auto submdspan_mapping(const layout_stride::mapping &map, Slices... slices) { - return layout_stride::mapping(submdspan_extents(map.extents(), slices...), - submdspan_strides(map.strides(), slices...)); -} - -template -class default_accessor { - public: - using offset_policy = default_accessor; - using element_type = Element; - using reference = Element &; - using data_handle_type = Element *; - - constexpr reference access(data_handle_type p, size_t i) const noexcept { return p[i]; } - - constexpr data_handle_type offset(data_handle_type p, size_t i) const noexcept { return p + i; } -}; - -template > -class mdspan { - public: - using extents_type = Extents; - using layout_type = LayoutPolicy; - using accessor_type = AccessorPolicy; - using mapping_type = typename LayoutPolicy::template mapping; - using element_type = T; - using value_type = remove_cv_t; - using index_type = typename Extents::index_type; - using size_type = typename Extents::size_type; - using rank_type = typename Extents::rank_type; - using data_handle_type = typename AccessorPolicy::data_handle_type; - using reference = typename AccessorPolicy::reference; - - private: - data_handle_type m_ptr; - mapping_type m_map; - accessor_type m_acc; - - public: - constexpr mdspan() = default; - - constexpr mdspan(data_handle_type p, const mapping_type &m) : m_ptr(p), m_map(m) {} - - constexpr mdspan(data_handle_type p, const mapping_type &m, const accessor_type &a) - : m_ptr(p), m_map(m), m_acc(a) {} - - template - constexpr reference operator()(OtherIndices... indices) const { - return m_acc.access(m_ptr, m_map(static_cast(std::move(indices))...)); - } - - template - constexpr reference operator[](OtherIndex index) const { - return m_acc.access(m_ptr, m_map(static_cast(index))); - } - - constexpr const data_handle_type &data_handle() const noexcept { return m_ptr; } - - constexpr const mapping_type &mapping() const noexcept { return m_map; } - - constexpr const accessor_type &accessor() const noexcept { return m_acc; } - - constexpr index_type stride(rank_type r) const { return m_map.stride(r); } - - constexpr const extents_type &extents() const noexcept { return m_map.extents(); } - - constexpr index_type extent(rank_type r) const noexcept { return m_map.extent(r); } - - constexpr size_type size() const noexcept { - size_type res = 1; - for (rank_type i = 0; i < extents_type::rank(); ++i) { - res *= m_map.extent(i); - } - - return res; - } -}; - -namespace detail { - - template - auto submdspan_offset(strided_slice slice) { - return slice.offset; - } - - template - auto submdspan_offset(std::tuple slice) { - return std::get<0>(slice); - } - - inline auto submdspan_offset(full_extent_t slice) { return 0; } - -} // namespace detail - -template -auto submdspan(const mdspan &src, SliceArgs... args) { - static_assert(Extents::rank() == sizeof...(SliceArgs), "number of slices must equal extents rank"); - - using submdspan_type = mdspan; - - auto src_map = src.mapping(); - auto src_acc = src.accessor(); - return submdspan_type(src_acc.offset(src.data_handle(), src_map(detail::submdspan_offset(args)...)), - submdspan_mapping(src.mapping(), args...), src_acc); -} - -} // namespace std diff --git a/scipy/special/special/sph_harm.h b/scipy/special/special/sph_harm.h index 4fc4ac2f10d4..5c1bdd790850 100644 --- a/scipy/special/special/sph_harm.h +++ b/scipy/special/special/sph_harm.h @@ -2,8 +2,8 @@ #include "error.h" #include "legendre.h" -#include "mdspan.h" #include "specfun.h" +#include "third_party/kokkos/mdspan.hpp" #include "cephes/poch.h" diff --git a/scipy/special/special/third_party/kokkos/mdspan.hpp b/scipy/special/special/third_party/kokkos/mdspan.hpp new file mode 100644 index 000000000000..ecfa332cf010 --- /dev/null +++ b/scipy/special/special/third_party/kokkos/mdspan.hpp @@ -0,0 +1,5674 @@ +#ifndef _MDSPAN_SINGLE_HEADER_INCLUDE_GUARD_ +#define _MDSPAN_SINGLE_HEADER_INCLUDE_GUARD_ + +//BEGIN_FILE_INCLUDE: mdspan/include/mdspan/mdspan.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +#ifndef MDSPAN_HPP_ +#define MDSPAN_HPP_ + +#ifndef MDSPAN_IMPL_STANDARD_NAMESPACE + #define MDSPAN_IMPL_STANDARD_NAMESPACE std +#endif + +#ifndef MDSPAN_IMPL_PROPOSED_NAMESPACE + #define MDSPAN_IMPL_PROPOSED_NAMESPACE experimental +#endif + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/default_accessor.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/macros.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/config.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +#ifndef __has_include +# define __has_include(x) 0 +#endif + +#if __has_include() +# include +#else +# include +# include +#endif + +#ifdef _MSVC_LANG +#define _MDSPAN_CPLUSPLUS _MSVC_LANG +#else +#define _MDSPAN_CPLUSPLUS __cplusplus +#endif + +#define MDSPAN_CXX_STD_14 201402L +#define MDSPAN_CXX_STD_17 201703L +#define MDSPAN_CXX_STD_20 202002L +// Note GCC has not updated this in version 13 +#ifdef __clang__ +#define MDSPAN_CXX_STD_23 202302L +#else +#define MDSPAN_CXX_STD_23 202100L +#endif + +#define MDSPAN_HAS_CXX_14 (_MDSPAN_CPLUSPLUS >= MDSPAN_CXX_STD_14) +#define MDSPAN_HAS_CXX_17 (_MDSPAN_CPLUSPLUS >= MDSPAN_CXX_STD_17) +#define MDSPAN_HAS_CXX_20 (_MDSPAN_CPLUSPLUS >= MDSPAN_CXX_STD_20) +#define MDSPAN_HAS_CXX_23 (_MDSPAN_CPLUSPLUS >= MDSPAN_CXX_STD_23) + +static_assert(_MDSPAN_CPLUSPLUS >= MDSPAN_CXX_STD_14, "mdspan requires C++14 or later."); + +#ifndef _MDSPAN_COMPILER_CLANG +# if defined(__clang__) +# define _MDSPAN_COMPILER_CLANG __clang__ +# endif +#endif + +#if !defined(_MDSPAN_COMPILER_MSVC) && !defined(_MDSPAN_COMPILER_MSVC_CLANG) +# if defined(_MSC_VER) +# if !defined(_MDSPAN_COMPILER_CLANG) +# define _MDSPAN_COMPILER_MSVC _MSC_VER +# else +# define _MDSPAN_COMPILER_MSVC_CLANG _MSC_VER +# endif +# endif +#endif + +#ifndef _MDSPAN_COMPILER_INTEL +# ifdef __INTEL_COMPILER +# define _MDSPAN_COMPILER_INTEL __INTEL_COMPILER +# endif +#endif + +#ifndef _MDSPAN_COMPILER_APPLECLANG +# ifdef __apple_build_version__ +# define _MDSPAN_COMPILER_APPLECLANG __apple_build_version__ +# endif +#endif + +#ifndef _MDSPAN_HAS_CUDA +# if defined(__CUDACC__) +# define _MDSPAN_HAS_CUDA __CUDACC__ +# endif +#endif + +#ifndef _MDSPAN_HAS_HIP +# if defined(__HIPCC__) +# define _MDSPAN_HAS_HIP __HIPCC__ +# endif +#endif + +#ifndef _MDSPAN_HAS_SYCL +# if defined(SYCL_LANGUAGE_VERSION) +# define _MDSPAN_HAS_SYCL SYCL_LANGUAGE_VERSION +# endif +#endif + +#ifndef __has_cpp_attribute +# define __has_cpp_attribute(x) 0 +#endif + +#ifndef _MDSPAN_PRESERVE_STANDARD_LAYOUT +// Preserve standard layout by default, but we're not removing the old version +// that turns this off until we're sure this doesn't have an unreasonable cost +// to the compiler or optimizer. +# define _MDSPAN_PRESERVE_STANDARD_LAYOUT 1 +#endif + +#if !defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) +# if ((__has_cpp_attribute(no_unique_address) >= 201803L) && \ + (!defined(__NVCC__) || MDSPAN_HAS_CXX_20) && \ + (!defined(_MDSPAN_COMPILER_MSVC) || MDSPAN_HAS_CXX_20)) +# define _MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS 1 +# define _MDSPAN_NO_UNIQUE_ADDRESS [[no_unique_address]] +# else +# define _MDSPAN_NO_UNIQUE_ADDRESS +# endif +#endif + +// NVCC older than 11.6 chokes on the no-unique-address-emulation +// so just pretend to use it (to avoid the full blown EBO workaround +// which NVCC also doesn't like ...), and leave the macro empty +#ifndef _MDSPAN_NO_UNIQUE_ADDRESS +# if defined(__NVCC__) +# define _MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS 1 +# define _MDSPAN_USE_FAKE_ATTRIBUTE_NO_UNIQUE_ADDRESS +# endif +# define _MDSPAN_NO_UNIQUE_ADDRESS +#endif + +// AMDs HIP compiler seems to have issues with concepts +// it pretends concepts exist, but doesn't ship +#ifndef __HIPCC__ +#ifndef _MDSPAN_USE_CONCEPTS +# if defined(__cpp_concepts) && __cpp_concepts >= 201507L +# define _MDSPAN_USE_CONCEPTS 1 +# endif +#endif +#endif + +#ifndef _MDSPAN_USE_FOLD_EXPRESSIONS +# if (defined(__cpp_fold_expressions) && __cpp_fold_expressions >= 201603L) \ + || (!defined(__cpp_fold_expressions) && MDSPAN_HAS_CXX_17) +# define _MDSPAN_USE_FOLD_EXPRESSIONS 1 +# endif +#endif + +#ifndef _MDSPAN_USE_INLINE_VARIABLES +# if defined(__cpp_inline_variables) && __cpp_inline_variables >= 201606L \ + || (!defined(__cpp_inline_variables) && MDSPAN_HAS_CXX_17) +# define _MDSPAN_USE_INLINE_VARIABLES 1 +# endif +#endif + +#ifndef _MDSPAN_NEEDS_TRAIT_VARIABLE_TEMPLATE_BACKPORTS +# if (!(defined(__cpp_lib_type_trait_variable_templates) && __cpp_lib_type_trait_variable_templates >= 201510L) \ + || !MDSPAN_HAS_CXX_17) +# if !(defined(_MDSPAN_COMPILER_APPLECLANG) && MDSPAN_HAS_CXX_17) +# define _MDSPAN_NEEDS_TRAIT_VARIABLE_TEMPLATE_BACKPORTS 1 +# endif +# endif +#endif + +#ifndef _MDSPAN_USE_VARIABLE_TEMPLATES +# if (defined(__cpp_variable_templates) && __cpp_variable_templates >= 201304 && MDSPAN_HAS_CXX_17) \ + || (!defined(__cpp_variable_templates) && MDSPAN_HAS_CXX_17) +# define _MDSPAN_USE_VARIABLE_TEMPLATES 1 +# endif +#endif // _MDSPAN_USE_VARIABLE_TEMPLATES + +#ifndef _MDSPAN_USE_CONSTEXPR_14 +# if (defined(__cpp_constexpr) && __cpp_constexpr >= 201304) \ + || (!defined(__cpp_constexpr) && MDSPAN_HAS_CXX_14) \ + && (!(defined(__INTEL_COMPILER) && __INTEL_COMPILER <= 1700)) +# define _MDSPAN_USE_CONSTEXPR_14 1 +# endif +#endif + +#ifndef _MDSPAN_USE_INTEGER_SEQUENCE +# if defined(_MDSPAN_COMPILER_MSVC) +# if (defined(__cpp_lib_integer_sequence) && __cpp_lib_integer_sequence >= 201304) +# define _MDSPAN_USE_INTEGER_SEQUENCE 1 +# endif +# endif +#endif +#ifndef _MDSPAN_USE_INTEGER_SEQUENCE +# if (defined(__cpp_lib_integer_sequence) && __cpp_lib_integer_sequence >= 201304) \ + || (!defined(__cpp_lib_integer_sequence) && MDSPAN_HAS_CXX_14) \ + /* as far as I can tell, libc++ seems to think this is a C++11 feature... */ \ + || (defined(__GLIBCXX__) && __GLIBCXX__ > 20150422 && __GNUC__ < 5 && !defined(__INTEL_CXX11_MODE__)) + // several compilers lie about integer_sequence working properly unless the C++14 standard is used +# define _MDSPAN_USE_INTEGER_SEQUENCE 1 +# elif defined(_MDSPAN_COMPILER_APPLECLANG) && MDSPAN_HAS_CXX_14 + // appleclang seems to be missing the __cpp_lib_... macros, but doesn't seem to lie about C++14 making + // integer_sequence work +# define _MDSPAN_USE_INTEGER_SEQUENCE 1 +# endif +#endif + +#ifndef _MDSPAN_USE_RETURN_TYPE_DEDUCTION +# if (defined(__cpp_return_type_deduction) && __cpp_return_type_deduction >= 201304) \ + || (!defined(__cpp_return_type_deduction) && MDSPAN_HAS_CXX_14) +# define _MDSPAN_USE_RETURN_TYPE_DEDUCTION 1 +# endif +#endif + +#ifndef _MDSPAN_USE_CLASS_TEMPLATE_ARGUMENT_DEDUCTION +# if (!defined(__NVCC__) || (__CUDACC_VER_MAJOR__ * 100 + __CUDACC_VER_MINOR__ * 10 >= 1170)) && \ + ((defined(__cpp_deduction_guides) && __cpp_deduction_guides >= 201703) || \ + (!defined(__cpp_deduction_guides) && MDSPAN_HAS_CXX_17)) +# define _MDSPAN_USE_CLASS_TEMPLATE_ARGUMENT_DEDUCTION 1 +# endif +#endif + +#ifndef _MDSPAN_USE_STANDARD_TRAIT_ALIASES +# if (defined(__cpp_lib_transformation_trait_aliases) && __cpp_lib_transformation_trait_aliases >= 201304) \ + || (!defined(__cpp_lib_transformation_trait_aliases) && MDSPAN_HAS_CXX_14) +# define _MDSPAN_USE_STANDARD_TRAIT_ALIASES 1 +# elif defined(_MDSPAN_COMPILER_APPLECLANG) && MDSPAN_HAS_CXX_14 + // appleclang seems to be missing the __cpp_lib_... macros, but doesn't seem to lie about C++14 +# define _MDSPAN_USE_STANDARD_TRAIT_ALIASES 1 +# endif +#endif + +#ifndef _MDSPAN_DEFAULTED_CONSTRUCTORS_INHERITANCE_WORKAROUND +# ifdef __GNUC__ +# if __GNUC__ < 9 +# define _MDSPAN_DEFAULTED_CONSTRUCTORS_INHERITANCE_WORKAROUND 1 +# endif +# endif +#endif + +#ifndef MDSPAN_CONDITIONAL_EXPLICIT +# if MDSPAN_HAS_CXX_20 +# define MDSPAN_CONDITIONAL_EXPLICIT(COND) explicit(COND) +# else +# define MDSPAN_CONDITIONAL_EXPLICIT(COND) +# endif +#endif + +#ifndef MDSPAN_USE_BRACKET_OPERATOR +# if defined(__cpp_multidimensional_subscript) +# define MDSPAN_USE_BRACKET_OPERATOR 1 +# else +# define MDSPAN_USE_BRACKET_OPERATOR 0 +# endif +#endif + +#ifndef MDSPAN_USE_PAREN_OPERATOR +# if !MDSPAN_USE_BRACKET_OPERATOR +# define MDSPAN_USE_PAREN_OPERATOR 1 +# else +# define MDSPAN_USE_PAREN_OPERATOR 0 +# endif +#endif + +#if MDSPAN_USE_BRACKET_OPERATOR +# define __MDSPAN_OP(mds,...) mds[__VA_ARGS__] +// Corentins demo compiler for subscript chokes on empty [] call, +// though I believe the proposal supports it? +#ifdef MDSPAN_NO_EMPTY_BRACKET_OPERATOR +# define __MDSPAN_OP0(mds) mds.accessor().access(mds.data_handle(),0) +#else +# define __MDSPAN_OP0(mds) mds[] +#endif +# define __MDSPAN_OP1(mds, a) mds[a] +# define __MDSPAN_OP2(mds, a, b) mds[a,b] +# define __MDSPAN_OP3(mds, a, b, c) mds[a,b,c] +# define __MDSPAN_OP4(mds, a, b, c, d) mds[a,b,c,d] +# define __MDSPAN_OP5(mds, a, b, c, d, e) mds[a,b,c,d,e] +# define __MDSPAN_OP6(mds, a, b, c, d, e, f) mds[a,b,c,d,e,f] +#else +# define __MDSPAN_OP(mds,...) mds(__VA_ARGS__) +# define __MDSPAN_OP0(mds) mds() +# define __MDSPAN_OP1(mds, a) mds(a) +# define __MDSPAN_OP2(mds, a, b) mds(a,b) +# define __MDSPAN_OP3(mds, a, b, c) mds(a,b,c) +# define __MDSPAN_OP4(mds, a, b, c, d) mds(a,b,c,d) +# define __MDSPAN_OP5(mds, a, b, c, d, e) mds(a,b,c,d,e) +# define __MDSPAN_OP6(mds, a, b, c, d, e, f) mds(a,b,c,d,e,f) +#endif +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/config.hpp + +#include +#include +#include // std::is_void +#if defined(_MDSPAN_HAS_CUDA) || defined(_MDSPAN_HAS_HIP) || defined(_MDSPAN_HAS_SYCL) +#include "assert.h" +#endif + +#ifndef _MDSPAN_HOST_DEVICE +# if defined(_MDSPAN_HAS_CUDA) || defined(_MDSPAN_HAS_HIP) +# define _MDSPAN_HOST_DEVICE __host__ __device__ +# else +# define _MDSPAN_HOST_DEVICE +# endif +#endif + +#ifndef MDSPAN_FORCE_INLINE_FUNCTION +# ifdef _MDSPAN_COMPILER_MSVC // Microsoft compilers +# define MDSPAN_FORCE_INLINE_FUNCTION __forceinline _MDSPAN_HOST_DEVICE +# else +# define MDSPAN_FORCE_INLINE_FUNCTION __attribute__((always_inline)) _MDSPAN_HOST_DEVICE +# endif +#endif + +#ifndef MDSPAN_INLINE_FUNCTION +# define MDSPAN_INLINE_FUNCTION inline _MDSPAN_HOST_DEVICE +#endif + +#ifndef MDSPAN_FUNCTION +# define MDSPAN_FUNCTION _MDSPAN_HOST_DEVICE +#endif + +#ifdef _MDSPAN_HAS_HIP +# define MDSPAN_DEDUCTION_GUIDE _MDSPAN_HOST_DEVICE +#else +# define MDSPAN_DEDUCTION_GUIDE +#endif + +// In CUDA defaulted functions do not need host device markup +#ifndef MDSPAN_INLINE_FUNCTION_DEFAULTED +# define MDSPAN_INLINE_FUNCTION_DEFAULTED +#endif + +//============================================================================== +// {{{1 + +#define MDSPAN_PP_COUNT(...) \ + _MDSPAN_PP_INTERNAL_EXPAND_ARGS_PRIVATE( \ + _MDSPAN_PP_INTERNAL_ARGS_AUGMENTER(__VA_ARGS__) \ + ) + +#define _MDSPAN_PP_INTERNAL_ARGS_AUGMENTER(...) unused, __VA_ARGS__ +#define _MDSPAN_PP_INTERNAL_EXPAND(x) x +#define _MDSPAN_PP_INTERNAL_EXPAND_ARGS_PRIVATE(...) \ + _MDSPAN_PP_INTERNAL_EXPAND( \ + _MDSPAN_PP_INTERNAL_COUNT_PRIVATE( \ + __VA_ARGS__, 69, 68, 67, 66, 65, 64, 63, 62, 61, \ + 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, \ + 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, \ + 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, \ + 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, \ + 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 \ + ) \ + ) +# define _MDSPAN_PP_INTERNAL_COUNT_PRIVATE( \ + _1_, _2_, _3_, _4_, _5_, _6_, _7_, _8_, _9_, \ + _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, \ + _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, \ + _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, \ + _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, \ + _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, \ + _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, \ + _70, count, ...) count \ + /**/ + +#define MDSPAN_PP_STRINGIFY_IMPL(x) #x +#define MDSPAN_PP_STRINGIFY(x) MDSPAN_PP_STRINGIFY_IMPL(x) + +#define MDSPAN_PP_CAT_IMPL(x, y) x ## y +#define MDSPAN_PP_CAT(x, y) MDSPAN_PP_CAT_IMPL(x, y) + +#define MDSPAN_PP_EVAL(X, ...) X(__VA_ARGS__) + +#define MDSPAN_PP_REMOVE_PARENS_IMPL(...) __VA_ARGS__ +#define MDSPAN_PP_REMOVE_PARENS(...) MDSPAN_PP_REMOVE_PARENS_IMPL __VA_ARGS__ + +#define MDSPAN_IMPL_STANDARD_NAMESPACE_STRING MDSPAN_PP_STRINGIFY(MDSPAN_IMPL_STANDARD_NAMESPACE) +#define MDSPAN_IMPL_PROPOSED_NAMESPACE_STRING MDSPAN_PP_STRINGIFY(MDSPAN_IMPL_STANDARD_NAMESPACE) "::" MDSPAN_PP_STRINGIFY(MDSPAN_IMPL_PROPOSED_NAMESPACE) + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace detail { + +#if defined(_MDSPAN_HAS_CUDA) || defined(_MDSPAN_HAS_HIP) +MDSPAN_FUNCTION inline void default_precondition_violation_handler(const char* cond, const char* file, unsigned line) +{ + printf("%s:%u: precondition failure: `%s`\n", file, line, cond); + assert(0); +} +#elif defined(_MDSPAN_HAS_SYCL) +MDSPAN_FUNCTION inline void default_precondition_violation_handler(const char* cond, const char* file, unsigned line) +{ + sycl::ext::oneapi::experimental::printf("%s:%u: precondition failure: `%s`\n", file, line, cond); + assert(0); +} +#else +MDSPAN_FUNCTION inline void default_precondition_violation_handler(const char* cond, const char* file, unsigned line) +{ + std::fprintf(stderr, "%s:%u: precondition failure: `%s`\n", file, line, cond); + std::abort(); +} +#endif + +} // namespace detail +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +#ifndef MDSPAN_IMPL_PRECONDITION_VIOLATION_HANDLER +#define MDSPAN_IMPL_PRECONDITION_VIOLATION_HANDLER(cond, file, line) \ + MDSPAN_IMPL_STANDARD_NAMESPACE::detail::default_precondition_violation_handler(cond, file, line) +#endif + +#ifndef MDSPAN_IMPL_CHECK_PRECONDITION + #ifndef NDEBUG + #define MDSPAN_IMPL_CHECK_PRECONDITION 0 + #else + #define MDSPAN_IMPL_CHECK_PRECONDITION 1 + #endif +#endif + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace detail { + +template +MDSPAN_FUNCTION constexpr void precondition(const char* cond, const char* file, unsigned line) +{ + if (not check) { return; } + // in case the macro doesn't use the arguments for custom macros + (void) cond; + (void) file; + (void) line; + MDSPAN_IMPL_PRECONDITION_VIOLATION_HANDLER(cond, file, line); +} + +} // namespace detail +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +#define MDSPAN_IMPL_PRECONDITION(...) \ + do { \ + if (not (__VA_ARGS__)) { \ + MDSPAN_IMPL_STANDARD_NAMESPACE::detail::precondition(#__VA_ARGS__, __FILE__, __LINE__); \ + } \ + } while (0) + +// end Preprocessor helpers }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +// These compatibility macros don't help with partial ordering, but they should do the trick +// for what we need to do with concepts in mdspan +#ifdef _MDSPAN_USE_CONCEPTS +# define MDSPAN_CLOSE_ANGLE_REQUIRES(REQ) > requires REQ +# define MDSPAN_FUNCTION_REQUIRES(PAREN_PREQUALS, FNAME, PAREN_PARAMS, QUALS, REQ) \ + MDSPAN_PP_REMOVE_PARENS(PAREN_PREQUALS) FNAME PAREN_PARAMS QUALS requires REQ \ + /**/ +#else +# define MDSPAN_CLOSE_ANGLE_REQUIRES(REQ) , typename ::std::enable_if<(REQ), int>::type = 0> +# define MDSPAN_FUNCTION_REQUIRES(PAREN_PREQUALS, FNAME, PAREN_PARAMS, QUALS, REQ) \ + MDSPAN_TEMPLATE_REQUIRES( \ + class __function_requires_ignored=void, \ + (std::is_void<__function_requires_ignored>::value && REQ) \ + ) MDSPAN_PP_REMOVE_PARENS(PAREN_PREQUALS) FNAME PAREN_PARAMS QUALS \ + /**/ +#endif + +#if defined(_MDSPAN_COMPILER_MSVC) && (!defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL) +# define MDSPAN_TEMPLATE_REQUIRES(...) \ + MDSPAN_PP_CAT( \ + MDSPAN_PP_CAT(MDSPAN_TEMPLATE_REQUIRES_, MDSPAN_PP_COUNT(__VA_ARGS__))\ + (__VA_ARGS__), \ + ) \ + /**/ +#else +# define MDSPAN_TEMPLATE_REQUIRES(...) \ + MDSPAN_PP_EVAL( \ + MDSPAN_PP_CAT(MDSPAN_TEMPLATE_REQUIRES_, MDSPAN_PP_COUNT(__VA_ARGS__)), \ + __VA_ARGS__ \ + ) \ + /**/ +#endif + +#define MDSPAN_TEMPLATE_REQUIRES_2(TP1, REQ) \ + template end Concept emulation }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +#ifdef _MDSPAN_USE_INLINE_VARIABLES +# define _MDSPAN_INLINE_VARIABLE inline +#else +# define _MDSPAN_INLINE_VARIABLE +#endif + +// end inline variables }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +#if _MDSPAN_USE_RETURN_TYPE_DEDUCTION +# define _MDSPAN_DEDUCE_RETURN_TYPE_SINGLE_LINE(SIGNATURE, BODY) \ + auto MDSPAN_PP_REMOVE_PARENS(SIGNATURE) { return MDSPAN_PP_REMOVE_PARENS(BODY); } +# define _MDSPAN_DEDUCE_DECLTYPE_AUTO_RETURN_TYPE_SINGLE_LINE(SIGNATURE, BODY) \ + decltype(auto) MDSPAN_PP_REMOVE_PARENS(SIGNATURE) { return MDSPAN_PP_REMOVE_PARENS(BODY); } +#else +# define _MDSPAN_DEDUCE_RETURN_TYPE_SINGLE_LINE(SIGNATURE, BODY) \ + auto MDSPAN_PP_REMOVE_PARENS(SIGNATURE) \ + -> std::remove_cv_t> \ + { return MDSPAN_PP_REMOVE_PARENS(BODY); } +# define _MDSPAN_DEDUCE_DECLTYPE_AUTO_RETURN_TYPE_SINGLE_LINE(SIGNATURE, BODY) \ + auto MDSPAN_PP_REMOVE_PARENS(SIGNATURE) \ + -> decltype(BODY) \ + { return MDSPAN_PP_REMOVE_PARENS(BODY); } + +#endif + +// end Return type deduction }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +struct __mdspan_enable_fold_comma { }; + +#ifdef _MDSPAN_USE_FOLD_EXPRESSIONS +# define _MDSPAN_FOLD_AND(...) ((__VA_ARGS__) && ...) +# define _MDSPAN_FOLD_AND_TEMPLATE(...) ((__VA_ARGS__) && ...) +# define _MDSPAN_FOLD_OR(...) ((__VA_ARGS__) || ...) +# define _MDSPAN_FOLD_ASSIGN_LEFT(INIT, ...) (INIT = ... = (__VA_ARGS__)) +# define _MDSPAN_FOLD_ASSIGN_RIGHT(PACK, ...) (PACK = ... = (__VA_ARGS__)) +# define _MDSPAN_FOLD_TIMES_RIGHT(PACK, ...) (PACK * ... * (__VA_ARGS__)) +# define _MDSPAN_FOLD_PLUS_RIGHT(PACK, ...) (PACK + ... + (__VA_ARGS__)) +# define _MDSPAN_FOLD_COMMA(...) ((__VA_ARGS__), ...) +#else + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +namespace __fold_compatibility_impl { + +// We could probably be more clever here, but at the (small) risk of losing some compiler understanding. For the +// few operations we need, it's not worth generalizing over the operation + +#if _MDSPAN_USE_RETURN_TYPE_DEDUCTION + +MDSPAN_FORCE_INLINE_FUNCTION +constexpr decltype(auto) __fold_right_and_impl() { + return true; +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr decltype(auto) __fold_right_and_impl(Arg&& arg, Args&&... args) { + return ((Arg&&)arg) && __fold_compatibility_impl::__fold_right_and_impl((Args&&)args...); +} + +MDSPAN_FORCE_INLINE_FUNCTION +constexpr decltype(auto) __fold_right_or_impl() { + return false; +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_right_or_impl(Arg&& arg, Args&&... args) { + return ((Arg&&)arg) || __fold_compatibility_impl::__fold_right_or_impl((Args&&)args...); +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_left_assign_impl(Arg1&& arg1) { + return (Arg1&&)arg1; +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_left_assign_impl(Arg1&& arg1, Arg2&& arg2, Args&&... args) { + return __fold_compatibility_impl::__fold_left_assign_impl((((Arg1&&)arg1) = ((Arg2&&)arg2)), (Args&&)args...); +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_right_assign_impl(Arg1&& arg1) { + return (Arg1&&)arg1; +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_right_assign_impl(Arg1&& arg1, Arg2&& arg2, Args&&... args) { + return ((Arg1&&)arg1) = __fold_compatibility_impl::__fold_right_assign_impl((Arg2&&)arg2, (Args&&)args...); +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_right_plus_impl(Arg1&& arg1) { + return (Arg1&&)arg1; +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_right_plus_impl(Arg1&& arg1, Arg2&& arg2, Args&&... args) { + return ((Arg1&&)arg1) + __fold_compatibility_impl::__fold_right_plus_impl((Arg2&&)arg2, (Args&&)args...); +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_right_times_impl(Arg1&& arg1) { + return (Arg1&&)arg1; +} + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr auto __fold_right_times_impl(Arg1&& arg1, Arg2&& arg2, Args&&... args) { + return ((Arg1&&)arg1) * __fold_compatibility_impl::__fold_right_times_impl((Arg2&&)arg2, (Args&&)args...); +} + +#else + +//------------------------------------------------------------------------------ +// {{{2 + +template +struct __fold_right_and_impl_; +template <> +struct __fold_right_and_impl_<> { + using __rv = bool; + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl() noexcept { + return true; + } +}; +template +struct __fold_right_and_impl_ { + using __next_t = __fold_right_and_impl_; + using __rv = decltype(std::declval() && std::declval()); + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg&& arg, Args&&... args) noexcept { + return ((Arg&&)arg) && __next_t::__impl((Args&&)args...); + } +}; + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr typename __fold_right_and_impl_::__rv +__fold_right_and_impl(Args&&... args) { + return __fold_right_and_impl_::__impl((Args&&)args...); +} + +// end right and }}}2 +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// {{{2 + +template +struct __fold_right_or_impl_; +template <> +struct __fold_right_or_impl_<> { + using __rv = bool; + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl() noexcept { + return false; + } +}; +template +struct __fold_right_or_impl_ { + using __next_t = __fold_right_or_impl_; + using __rv = decltype(std::declval() || std::declval()); + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg&& arg, Args&&... args) noexcept { + return ((Arg&&)arg) || __next_t::__impl((Args&&)args...); + } +}; + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr typename __fold_right_or_impl_::__rv +__fold_right_or_impl(Args&&... args) { + return __fold_right_or_impl_::__impl((Args&&)args...); +} + +// end right or }}}2 +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// {{{2 + +template +struct __fold_right_plus_impl_; +template +struct __fold_right_plus_impl_ { + using __rv = Arg&&; + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg&& arg) noexcept { + return (Arg&&)arg; + } +}; +template +struct __fold_right_plus_impl_ { + using __next_t = __fold_right_plus_impl_; + using __rv = decltype(std::declval() + std::declval()); + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg1&& arg, Arg2&& arg2, Args&&... args) noexcept { + return ((Arg1&&)arg) + __next_t::__impl((Arg2&&)arg2, (Args&&)args...); + } +}; + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr typename __fold_right_plus_impl_::__rv +__fold_right_plus_impl(Args&&... args) { + return __fold_right_plus_impl_::__impl((Args&&)args...); +} + +// end right plus }}}2 +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// {{{2 + +template +struct __fold_right_times_impl_; +template +struct __fold_right_times_impl_ { + using __rv = Arg&&; + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg&& arg) noexcept { + return (Arg&&)arg; + } +}; +template +struct __fold_right_times_impl_ { + using __next_t = __fold_right_times_impl_; + using __rv = decltype(std::declval() * std::declval()); + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg1&& arg, Arg2&& arg2, Args&&... args) noexcept { + return ((Arg1&&)arg) * __next_t::__impl((Arg2&&)arg2, (Args&&)args...); + } +}; + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr typename __fold_right_times_impl_::__rv +__fold_right_times_impl(Args&&... args) { + return __fold_right_times_impl_::__impl((Args&&)args...); +} + +// end right times }}}2 +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// {{{2 + +template +struct __fold_right_assign_impl_; +template +struct __fold_right_assign_impl_ { + using __rv = Arg&&; + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg&& arg) noexcept { + return (Arg&&)arg; + } +}; +template +struct __fold_right_assign_impl_ { + using __next_t = __fold_right_assign_impl_; + using __rv = decltype(std::declval() = std::declval()); + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg1&& arg, Arg2&& arg2, Args&&... args) noexcept { + return ((Arg1&&)arg) = __next_t::__impl((Arg2&&)arg2, (Args&&)args...); + } +}; + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr typename __fold_right_assign_impl_::__rv +__fold_right_assign_impl(Args&&... args) { + return __fold_right_assign_impl_::__impl((Args&&)args...); +} + +// end right assign }}}2 +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// {{{2 + +template +struct __fold_left_assign_impl_; +template +struct __fold_left_assign_impl_ { + using __rv = Arg&&; + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg&& arg) noexcept { + return (Arg&&)arg; + } +}; +template +struct __fold_left_assign_impl_ { + using __assign_result_t = decltype(std::declval() = std::declval()); + using __next_t = __fold_left_assign_impl_<__assign_result_t, Args...>; + using __rv = typename __next_t::__rv; + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr __rv + __impl(Arg1&& arg, Arg2&& arg2, Args&&... args) noexcept { + return __next_t::__impl(((Arg1&&)arg) = (Arg2&&)arg2, (Args&&)args...); + } +}; + +template +MDSPAN_FORCE_INLINE_FUNCTION +constexpr typename __fold_left_assign_impl_::__rv +__fold_left_assign_impl(Args&&... args) { + return __fold_left_assign_impl_::__impl((Args&&)args...); +} + +// end left assign }}}2 +//------------------------------------------------------------------------------ + +#endif + + +template +constexpr __mdspan_enable_fold_comma __fold_comma_impl(Args&&... args) noexcept { return { }; } + +template +struct __bools; + +} // __fold_compatibility_impl + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +# define _MDSPAN_FOLD_AND(...) MDSPAN_IMPL_STANDARD_NAMESPACE::__fold_compatibility_impl::__fold_right_and_impl((__VA_ARGS__)...) +# define _MDSPAN_FOLD_OR(...) MDSPAN_IMPL_STANDARD_NAMESPACE::__fold_compatibility_impl::__fold_right_or_impl((__VA_ARGS__)...) +# define _MDSPAN_FOLD_ASSIGN_LEFT(INIT, ...) MDSPAN_IMPL_STANDARD_NAMESPACE::__fold_compatibility_impl::__fold_left_assign_impl(INIT, (__VA_ARGS__)...) +# define _MDSPAN_FOLD_ASSIGN_RIGHT(PACK, ...) MDSPAN_IMPL_STANDARD_NAMESPACE::__fold_compatibility_impl::__fold_right_assign_impl((PACK)..., __VA_ARGS__) +# define _MDSPAN_FOLD_TIMES_RIGHT(PACK, ...) MDSPAN_IMPL_STANDARD_NAMESPACE::__fold_compatibility_impl::__fold_right_times_impl((PACK)..., __VA_ARGS__) +# define _MDSPAN_FOLD_PLUS_RIGHT(PACK, ...) MDSPAN_IMPL_STANDARD_NAMESPACE::__fold_compatibility_impl::__fold_right_plus_impl((PACK)..., __VA_ARGS__) +# define _MDSPAN_FOLD_COMMA(...) MDSPAN_IMPL_STANDARD_NAMESPACE::__fold_compatibility_impl::__fold_comma_impl((__VA_ARGS__)...) + +# define _MDSPAN_FOLD_AND_TEMPLATE(...) \ + _MDSPAN_TRAIT(std::is_same, __fold_compatibility_impl::__bools<(__VA_ARGS__)..., true>, __fold_compatibility_impl::__bools) + +#endif + +// end fold expressions }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +#if _MDSPAN_USE_VARIABLE_TEMPLATES +# define _MDSPAN_TRAIT(TRAIT, ...) TRAIT##_v<__VA_ARGS__> +#else +# define _MDSPAN_TRAIT(TRAIT, ...) TRAIT<__VA_ARGS__>::value +#endif + +// end Variable template compatibility }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +#if _MDSPAN_USE_CONSTEXPR_14 +# define _MDSPAN_CONSTEXPR_14 constexpr +// Workaround for a bug (I think?) in EDG frontends +# ifdef __EDG__ +# define _MDSPAN_CONSTEXPR_14_DEFAULTED +# else +# define _MDSPAN_CONSTEXPR_14_DEFAULTED constexpr +# endif +#else +# define _MDSPAN_CONSTEXPR_14 +# define _MDSPAN_CONSTEXPR_14_DEFAULTED +#endif + +// end Pre-C++14 constexpr }}}1 +//============================================================================== +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/macros.hpp + +#include // size_t + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +template +struct default_accessor { + + using offset_policy = default_accessor; + using element_type = ElementType; + using reference = ElementType&; + using data_handle_type = ElementType*; + + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr default_accessor() noexcept = default; + + MDSPAN_TEMPLATE_REQUIRES( + class OtherElementType, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, OtherElementType(*)[], element_type(*)[]) + ) + ) + MDSPAN_INLINE_FUNCTION + constexpr default_accessor(default_accessor) noexcept {} + + MDSPAN_INLINE_FUNCTION + constexpr data_handle_type + offset(data_handle_type p, size_t i) const noexcept { + return p + i; + } + + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference access(data_handle_type p, size_t i) const noexcept { + return p[i]; + } + +}; + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/default_accessor.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/full_extent_t.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +struct full_extent_t { explicit full_extent_t() = default; }; + +_MDSPAN_INLINE_VARIABLE constexpr auto full_extent = full_extent_t{ }; + +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/full_extent_t.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/mdspan.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/layout_right.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/trait_backports.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER +#ifndef MDSPAN_INCLUDE_EXPERIMENTAL_BITS_TRAIT_BACKPORTS_HPP_ +#define MDSPAN_INCLUDE_EXPERIMENTAL_BITS_TRAIT_BACKPORTS_HPP_ + + +#include +#include // integer_sequence + +//============================================================================== +// {{{1 + +#ifdef _MDSPAN_NEEDS_TRAIT_VARIABLE_TEMPLATE_BACKPORTS + +#if _MDSPAN_USE_VARIABLE_TEMPLATES +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +#define _MDSPAN_BACKPORT_TRAIT(TRAIT) \ + template _MDSPAN_INLINE_VARIABLE constexpr auto TRAIT##_v = TRAIT::value; + +_MDSPAN_BACKPORT_TRAIT(is_assignable) +_MDSPAN_BACKPORT_TRAIT(is_constructible) +_MDSPAN_BACKPORT_TRAIT(is_convertible) +_MDSPAN_BACKPORT_TRAIT(is_default_constructible) +_MDSPAN_BACKPORT_TRAIT(is_trivially_destructible) +_MDSPAN_BACKPORT_TRAIT(is_same) +_MDSPAN_BACKPORT_TRAIT(is_empty) +_MDSPAN_BACKPORT_TRAIT(is_void) + +#undef _MDSPAN_BACKPORT_TRAIT + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +#endif // _MDSPAN_USE_VARIABLE_TEMPLATES + +#endif // _MDSPAN_NEEDS_TRAIT_VARIABLE_TEMPLATE_BACKPORTS + +// end Variable template trait backports (e.g., is_void_v) }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +#if !defined(_MDSPAN_USE_INTEGER_SEQUENCE) || !_MDSPAN_USE_INTEGER_SEQUENCE + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +template +struct integer_sequence { + static constexpr size_t size() noexcept { return sizeof...(Vals); } + using value_type = T; +}; + +template +using index_sequence = std::integer_sequence; + +namespace __detail { + +template +struct __make_int_seq_impl; + +template +struct __make_int_seq_impl> +{ + using type = integer_sequence; +}; + +template +struct __make_int_seq_impl< + T, N, I, integer_sequence +> : __make_int_seq_impl> +{ }; + +} // end namespace __detail + +template +using make_integer_sequence = typename __detail::__make_int_seq_impl>::type; + +template +using make_index_sequence = typename __detail::__make_int_seq_impl>::type; + +template +using index_sequence_for = make_index_sequence; + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +#endif + +// end integer sequence (ugh...) }}}1 +//============================================================================== + +//============================================================================== +// {{{1 + +#if !defined(_MDSPAN_USE_STANDARD_TRAIT_ALIASES) || !_MDSPAN_USE_STANDARD_TRAIT_ALIASES + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +#define _MDSPAN_BACKPORT_TRAIT_ALIAS(TRAIT) \ + template using TRAIT##_t = typename TRAIT::type; + +_MDSPAN_BACKPORT_TRAIT_ALIAS(remove_cv) +_MDSPAN_BACKPORT_TRAIT_ALIAS(remove_reference) + +template +using enable_if_t = typename enable_if<_B, _T>::type; + +#undef _MDSPAN_BACKPORT_TRAIT_ALIAS + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +#endif + +// end standard trait aliases }}}1 +//============================================================================== + +#endif //MDSPAN_INCLUDE_EXPERIMENTAL_BITS_TRAIT_BACKPORTS_HPP_ +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/trait_backports.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/extents.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/dynamic_extent.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +#if defined(__cpp_lib_span) +#include +#endif + +#include // size_t +#include // numeric_limits + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +#if defined(__cpp_lib_span) +using std::dynamic_extent; +#else +_MDSPAN_INLINE_VARIABLE constexpr auto dynamic_extent = std::numeric_limits::max(); +#endif +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +//============================================================================================================== +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/dynamic_extent.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/utility.hpp + +#include +#include + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace detail { + +// type alias used for rank-based tag dispatch +// +// this is used to enable alternatives to constexpr if when building for C++14 +// +template +using with_rank = std::integral_constant; + +template +constexpr bool common_integral_compare(I1 x, I2 y) +{ + static_assert(std::is_integral::value and + std::is_integral::value, ""); + + using I = std::common_type_t; + return static_cast(x) == static_cast(y); +} + +template +constexpr bool rankwise_equal(with_rank<0>, const T1&, const T2&, F) +{ + return true; +} +template +constexpr bool rankwise_equal(with_rank, const T1& x, const T2& y, F func) +{ + bool match = true; + + for (std::size_t r = 0; r < N; r++) { + match = match && common_integral_compare(func(x, r), func(y, r)); + } + + return match; +} + +constexpr struct +{ + template + constexpr auto operator()(const T& x, I i) const + { + return x.extent(i); + } +} extent; + +constexpr struct +{ + template + constexpr auto operator()(const T& x, I i) const + { + return x.stride(i); + } +} stride; + +} // namespace detail + +constexpr struct mdspan_non_standard_tag { +} mdspan_non_standard; + +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/utility.hpp + +#ifdef __cpp_lib_span +#include +#endif +#include +#include + +#include +#include + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace detail { + +// Function used to check compatibility of extents in converting constructor +// can't be a private member function for some reason. +template +static constexpr std::integral_constant __check_compatible_extents( + std::integral_constant, + std::integer_sequence, + std::integer_sequence) noexcept { + return {}; +} + +// This helper prevents ICE's on MSVC. +template +struct __compare_extent_compatible : std::integral_constant +{}; + +template +static constexpr std::integral_constant< + bool, _MDSPAN_FOLD_AND(__compare_extent_compatible::value)> +__check_compatible_extents( + std::integral_constant, + std::integer_sequence, + std::integer_sequence) noexcept { + return {}; +} + +template +MDSPAN_INLINE_FUNCTION +static constexpr bool are_valid_indices() { + return + _MDSPAN_FOLD_AND(std::is_convertible::value) && + _MDSPAN_FOLD_AND(std::is_nothrow_constructible::value); +} + +// ------------------------------------------------------------------ +// ------------ static_array ---------------------------------------- +// ------------------------------------------------------------------ + +// array like class which provides an array of static values with get +// function and operator []. + +// Implementation of Static Array with recursive implementation of get. +template struct static_array_impl; + +template +struct static_array_impl { + MDSPAN_INLINE_FUNCTION + constexpr static T get(size_t r) { + if (r == R) + return FirstExt; + else + return static_array_impl::get(r); + } + template MDSPAN_INLINE_FUNCTION constexpr static T get() { +#if MDSPAN_HAS_CXX_17 + if constexpr (r == R) + return FirstExt; + else + return static_array_impl::template get(); +#else + get(r); +#endif + } +}; + +// End the recursion +template +struct static_array_impl { + MDSPAN_INLINE_FUNCTION + constexpr static T get(size_t) { return FirstExt; } + template MDSPAN_INLINE_FUNCTION constexpr static T get() { + return FirstExt; + } +}; + +// Don't start recursion if size 0 +template struct static_array_impl<0, T> { + MDSPAN_INLINE_FUNCTION + constexpr static T get(size_t) { return T(); } + template MDSPAN_INLINE_FUNCTION constexpr static T get() { + return T(); + } +}; + +// Static array, provides get(), get(r) and operator[r] +template struct static_array: + public static_array_impl<0, T, Values...> { + +public: + using value_type = T; + + MDSPAN_INLINE_FUNCTION + constexpr static size_t size() { return sizeof...(Values); } +}; + + +// ------------------------------------------------------------------ +// ------------ index_sequence_scan --------------------------------- +// ------------------------------------------------------------------ + +// index_sequence_scan takes compile time values and provides get(r) +// and get() which return the sum of the first r-1 values. + +// Recursive implementation for get +template struct index_sequence_scan_impl; + +template +struct index_sequence_scan_impl { + MDSPAN_INLINE_FUNCTION + constexpr static size_t get(size_t r) { + if (r > R) + return FirstVal + index_sequence_scan_impl::get(r); + else + return 0; + } +}; + +template +struct index_sequence_scan_impl { +#if defined(__NVCC__) || defined(__NVCOMPILER) || \ + defined(_MDSPAN_COMPILER_INTEL) + // NVCC warns about pointless comparison with 0 for R==0 and r being const + // evaluatable and also 0. + MDSPAN_INLINE_FUNCTION + constexpr static size_t get(size_t r) { + return static_cast(R) > static_cast(r) ? FirstVal : 0; + } +#else + MDSPAN_INLINE_FUNCTION + constexpr static size_t get(size_t r) { return R > r ? FirstVal : 0; } +#endif +}; +template <> struct index_sequence_scan_impl<0> { + MDSPAN_INLINE_FUNCTION + constexpr static size_t get(size_t) { return 0; } +}; + +// ------------------------------------------------------------------ +// ------------ possibly_empty_array ------------------------------- +// ------------------------------------------------------------------ + +// array like class which provides get function and operator [], and +// has a specialization for the size 0 case. +// This is needed to make the maybe_static_array be truly empty, for +// all static values. + +template struct possibly_empty_array { + T vals[N]{}; + MDSPAN_INLINE_FUNCTION + constexpr T &operator[](size_t r) { return vals[r]; } + MDSPAN_INLINE_FUNCTION + constexpr const T &operator[](size_t r) const { return vals[r]; } +}; + +template struct possibly_empty_array { + MDSPAN_INLINE_FUNCTION + constexpr T operator[](size_t) { return T(); } + MDSPAN_INLINE_FUNCTION + constexpr const T operator[](size_t) const { return T(); } +}; + +// ------------------------------------------------------------------ +// ------------ maybe_static_array ---------------------------------- +// ------------------------------------------------------------------ + +// array like class which has a mix of static and runtime values but +// only stores the runtime values. +// The type of the static and the runtime values can be different. +// The position of a dynamic value is indicated through a tag value. +template +struct maybe_static_array { + + static_assert(std::is_convertible::value, "maybe_static_array: TStatic must be convertible to TDynamic"); + static_assert(std::is_convertible::value, "maybe_static_array: TDynamic must be convertible to TStatic"); + +private: + // Static values member + using static_vals_t = static_array; + constexpr static size_t m_size = sizeof...(Values); + constexpr static size_t m_size_dynamic = + _MDSPAN_FOLD_PLUS_RIGHT((Values == dyn_tag), 0); + + // Dynamic values member + _MDSPAN_NO_UNIQUE_ADDRESS possibly_empty_array + m_dyn_vals; + + // static mapping of indices to the position in the dynamic values array + using dyn_map_t = index_sequence_scan_impl<0, static_cast(Values == dyn_tag)...>; +public: + + // two types for static and dynamic values + using value_type = TDynamic; + using static_value_type = TStatic; + // tag value indicating dynamic value + constexpr static static_value_type tag_value = dyn_tag; + + constexpr maybe_static_array() = default; + + // constructor for all static values + // TODO: add precondition check? + MDSPAN_TEMPLATE_REQUIRES(class... Vals, + /* requires */ ((m_size_dynamic == 0) && + (sizeof...(Vals) > 0))) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(Vals...) : m_dyn_vals{} {} + + // constructors from dynamic values only + MDSPAN_TEMPLATE_REQUIRES(class... DynVals, + /* requires */ (sizeof...(DynVals) == + m_size_dynamic && + m_size_dynamic > 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(DynVals... vals) + : m_dyn_vals{static_cast(vals)...} {} + + + MDSPAN_TEMPLATE_REQUIRES(class T, size_t N, + /* requires */ (N == m_size_dynamic && N > 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(const std::array &vals) { + for (size_t r = 0; r < N; r++) + m_dyn_vals[r] = static_cast(vals[r]); + } + + MDSPAN_TEMPLATE_REQUIRES(class T, size_t N, + /* requires */ (N == m_size_dynamic && N == 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(const std::array &) : m_dyn_vals{} {} + +#ifdef __cpp_lib_span + MDSPAN_TEMPLATE_REQUIRES(class T, size_t N, + /* requires */ (N == m_size_dynamic && N > 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(const std::span &vals) { + for (size_t r = 0; r < N; r++) + m_dyn_vals[r] = static_cast(vals[r]); + } + + MDSPAN_TEMPLATE_REQUIRES(class T, size_t N, + /* requires */ (N == m_size_dynamic && N == 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(const std::span &) : m_dyn_vals{} {} +#endif + + // constructors from all values + MDSPAN_TEMPLATE_REQUIRES(class... DynVals, + /* requires */ (sizeof...(DynVals) != + m_size_dynamic && + m_size_dynamic > 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(DynVals... vals) + : m_dyn_vals{} { + static_assert((sizeof...(DynVals) == m_size), "Invalid number of values."); + TDynamic values[m_size]{static_cast(vals)...}; + for (size_t r = 0; r < m_size; r++) { + TStatic static_val = static_vals_t::get(r); + if (static_val == dyn_tag) { + m_dyn_vals[dyn_map_t::get(r)] = values[r]; + } +// Precondition check +#ifdef _MDSPAN_DEBUG + else { + assert(values[r] == static_cast(static_val)); + } +#endif + } + } + + MDSPAN_TEMPLATE_REQUIRES( + class T, size_t N, + /* requires */ (N != m_size_dynamic && m_size_dynamic > 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(const std::array &vals) { + static_assert((N == m_size), "Invalid number of values."); +// Precondition check +#ifdef _MDSPAN_DEBUG + assert(N == m_size); +#endif + for (size_t r = 0; r < m_size; r++) { + TStatic static_val = static_vals_t::get(r); + if (static_val == dyn_tag) { + m_dyn_vals[dyn_map_t::get(r)] = static_cast(vals[r]); + } +// Precondition check +#ifdef _MDSPAN_DEBUG + else { + assert(static_cast(vals[r]) == + static_cast(static_val)); + } +#endif + } + } + +#ifdef __cpp_lib_span + MDSPAN_TEMPLATE_REQUIRES( + class T, size_t N, + /* requires */ (N != m_size_dynamic && m_size_dynamic > 0)) + MDSPAN_INLINE_FUNCTION + constexpr maybe_static_array(const std::span &vals) { + static_assert((N == m_size) || (m_size == dynamic_extent)); +#ifdef _MDSPAN_DEBUG + assert(N == m_size); +#endif + for (size_t r = 0; r < m_size; r++) { + TStatic static_val = static_vals_t::get(r); + if (static_val == dyn_tag) { + m_dyn_vals[dyn_map_t::get(r)] = static_cast(vals[r]); + } +#ifdef _MDSPAN_DEBUG + else { + assert(static_cast(vals[r]) == + static_cast(static_val)); + } +#endif + } + } +#endif + + // access functions + MDSPAN_INLINE_FUNCTION + constexpr static TStatic static_value(size_t r) { return static_vals_t::get(r); } + + MDSPAN_INLINE_FUNCTION + constexpr TDynamic value(size_t r) const { + TStatic static_val = static_vals_t::get(r); + return static_val == dyn_tag ? m_dyn_vals[dyn_map_t::get(r)] + : static_cast(static_val); + } + MDSPAN_INLINE_FUNCTION + constexpr TDynamic operator[](size_t r) const { return value(r); } + + + // observers + MDSPAN_INLINE_FUNCTION + constexpr static size_t size() { return m_size; } + MDSPAN_INLINE_FUNCTION + constexpr static size_t size_dynamic() { return m_size_dynamic; } +}; + +} // namespace detail +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +// ------------------------------------------------------------------ +// ------------ extents --------------------------------------------- +// ------------------------------------------------------------------ + +// Class to describe the extents of a multi dimensional array. +// Used by mdspan, mdarray and layout mappings. +// See ISO C++ standard [mdspan.extents] + +template class extents { +public: + // typedefs for integral types used + using index_type = IndexType; + using size_type = std::make_unsigned_t; + using rank_type = size_t; + + static_assert(std::is_integral::value && !std::is_same::value, + MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::extents::index_type must be a signed or unsigned integer type"); +private: + constexpr static rank_type m_rank = sizeof...(Extents); + constexpr static rank_type m_rank_dynamic = + _MDSPAN_FOLD_PLUS_RIGHT((Extents == dynamic_extent), /* + ... + */ 0); + + // internal storage type using maybe_static_array + using vals_t = + detail::maybe_static_array; + _MDSPAN_NO_UNIQUE_ADDRESS vals_t m_vals; + +public: + // [mdspan.extents.obs], observers of multidimensional index space + MDSPAN_INLINE_FUNCTION + constexpr static rank_type rank() noexcept { return m_rank; } + MDSPAN_INLINE_FUNCTION + constexpr static rank_type rank_dynamic() noexcept { return m_rank_dynamic; } + + MDSPAN_INLINE_FUNCTION + constexpr index_type extent(rank_type r) const noexcept { return m_vals.value(r); } + MDSPAN_INLINE_FUNCTION + constexpr static size_t static_extent(rank_type r) noexcept { + return vals_t::static_value(r); + } + + // [mdspan.extents.cons], constructors + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr extents() noexcept = default; + + // Construction from just dynamic or all values. + // Precondition check is deferred to maybe_static_array constructor + MDSPAN_TEMPLATE_REQUIRES( + class... OtherIndexTypes, + /* requires */ ( + _MDSPAN_FOLD_AND(_MDSPAN_TRAIT(std::is_convertible, OtherIndexTypes, + index_type) /* && ... */) && + _MDSPAN_FOLD_AND(_MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, + OtherIndexTypes) /* && ... */) && + (sizeof...(OtherIndexTypes) == m_rank || + sizeof...(OtherIndexTypes) == m_rank_dynamic))) + MDSPAN_INLINE_FUNCTION + constexpr explicit extents(OtherIndexTypes... dynvals) noexcept + : m_vals(static_cast(dynvals)...) {} + + MDSPAN_TEMPLATE_REQUIRES( + class OtherIndexType, size_t N, + /* requires */ + ( + _MDSPAN_TRAIT(std::is_convertible, const OtherIndexType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, + const OtherIndexType&) && + (N == m_rank || N == m_rank_dynamic))) + MDSPAN_INLINE_FUNCTION + MDSPAN_CONDITIONAL_EXPLICIT(N != m_rank_dynamic) + constexpr extents(const std::array &exts) noexcept + : m_vals(std::move(exts)) {} + +#ifdef __cpp_lib_span + MDSPAN_TEMPLATE_REQUIRES( + class OtherIndexType, size_t N, + /* requires */ + (_MDSPAN_TRAIT(std::is_convertible, const OtherIndexType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, const OtherIndexType&) && + (N == m_rank || N == m_rank_dynamic))) + MDSPAN_INLINE_FUNCTION + MDSPAN_CONDITIONAL_EXPLICIT(N != m_rank_dynamic) + constexpr extents(const std::span &exts) noexcept + : m_vals(std::move(exts)) {} +#endif + +private: + // Function to construct extents storage from other extents. + // With C++ 17 the first two variants could be collapsed using if constexpr + // in which case you don't need all the requires clauses. + // in C++ 14 mode that doesn't work due to infinite recursion + MDSPAN_TEMPLATE_REQUIRES( + size_t DynCount, size_t R, class OtherExtents, class... DynamicValues, + /* requires */ ((R < m_rank) && (static_extent(R) == dynamic_extent))) + MDSPAN_INLINE_FUNCTION + constexpr + vals_t __construct_vals_from_extents(std::integral_constant, + std::integral_constant, + const OtherExtents &exts, + DynamicValues... dynamic_values) noexcept { + return __construct_vals_from_extents( + std::integral_constant(), + std::integral_constant(), exts, dynamic_values..., + exts.extent(R)); + } + + MDSPAN_TEMPLATE_REQUIRES( + size_t DynCount, size_t R, class OtherExtents, class... DynamicValues, + /* requires */ ((R < m_rank) && (static_extent(R) != dynamic_extent))) + MDSPAN_INLINE_FUNCTION + constexpr + vals_t __construct_vals_from_extents(std::integral_constant, + std::integral_constant, + const OtherExtents &exts, + DynamicValues... dynamic_values) noexcept { + return __construct_vals_from_extents( + std::integral_constant(), + std::integral_constant(), exts, dynamic_values...); + } + + MDSPAN_TEMPLATE_REQUIRES( + size_t DynCount, size_t R, class OtherExtents, class... DynamicValues, + /* requires */ ((R == m_rank) && (DynCount == m_rank_dynamic))) + MDSPAN_INLINE_FUNCTION + constexpr + vals_t __construct_vals_from_extents(std::integral_constant, + std::integral_constant, + const OtherExtents &, + DynamicValues... dynamic_values) noexcept { + return vals_t{static_cast(dynamic_values)...}; + } + +public: + + // Converting constructor from other extents specializations + MDSPAN_TEMPLATE_REQUIRES( + class OtherIndexType, size_t... OtherExtents, + /* requires */ + ( + /* multi-stage check to protect from invalid pack expansion when sizes + don't match? */ + decltype(detail::__check_compatible_extents( + // using: sizeof...(Extents) == sizeof...(OtherExtents) as the second argument fails with MSVC+NVCC with some obscure expansion error + // MSVC: 19.38.33133 NVCC: 12.0 + std::integral_constant::rank() == extents::rank()>{}, + std::integer_sequence{}, + std::integer_sequence{}))::value + ) + ) + MDSPAN_INLINE_FUNCTION + MDSPAN_CONDITIONAL_EXPLICIT((((Extents != dynamic_extent) && + (OtherExtents == dynamic_extent)) || + ...) || + (std::numeric_limits::max() < + std::numeric_limits::max())) + constexpr extents(const extents &other) noexcept + : m_vals(__construct_vals_from_extents( + std::integral_constant(), + std::integral_constant(), other)) {} + + // Comparison operator + template + MDSPAN_INLINE_FUNCTION friend constexpr bool + operator==(const extents &lhs, + const extents &rhs) noexcept { + return + rank() == extents::rank() && + detail::rankwise_equal(detail::with_rank{}, rhs, lhs, detail::extent); + } + +#if !(MDSPAN_HAS_CXX_20) + template + MDSPAN_INLINE_FUNCTION friend constexpr bool + operator!=(extents const &lhs, + extents const &rhs) noexcept { + return !(lhs == rhs); + } +#endif +}; + +// Recursive helper classes to implement dextents alias for extents +namespace detail { + +template > +struct __make_dextents; + +template +struct __make_dextents< + IndexType, Rank, ::MDSPAN_IMPL_STANDARD_NAMESPACE::extents> +{ + using type = typename __make_dextents< + IndexType, Rank - 1, + ::MDSPAN_IMPL_STANDARD_NAMESPACE::extents>::type; +}; + +template +struct __make_dextents< + IndexType, 0, ::MDSPAN_IMPL_STANDARD_NAMESPACE::extents> +{ + using type = ::MDSPAN_IMPL_STANDARD_NAMESPACE::extents; +}; + +} // end namespace detail + +// [mdspan.extents.dextents], alias template +template +using dextents = typename detail::__make_dextents::type; + +// Deduction guide for extents +#if defined(_MDSPAN_USE_CLASS_TEMPLATE_ARGUMENT_DEDUCTION) +template +extents(IndexTypes...) + -> extents; +#endif + +// Helper type traits for identifying a class as extents. +namespace detail { + +template struct __is_extents : ::std::false_type {}; + +template +struct __is_extents<::MDSPAN_IMPL_STANDARD_NAMESPACE::extents> + : ::std::true_type {}; + +template +#if MDSPAN_HAS_CXX_17 +inline +#else +static +#endif +constexpr bool __is_extents_v = __is_extents::value; + +template +MDSPAN_INLINE_FUNCTION +constexpr void +check_lower_bound(InputIndexType user_index, + ExtentsIndexType /* current_extent */, + std::true_type /* is_signed */) +{ + (void) user_index; // prevent unused variable warning +#ifdef _MDSPAN_DEBUG + assert(static_cast(user_index) >= 0); +#endif +} + +template +MDSPAN_INLINE_FUNCTION +constexpr void +check_lower_bound(InputIndexType /* user_index */, + ExtentsIndexType /* current_extent */, + std::false_type /* is_signed */) +{} + +template +MDSPAN_INLINE_FUNCTION +constexpr void +check_upper_bound(InputIndexType user_index, + ExtentsIndexType current_extent) +{ + (void) user_index; // prevent unused variable warnings + (void) current_extent; +#ifdef _MDSPAN_DEBUG + assert(static_cast(user_index) < current_extent); +#endif +} + +// Returning true to use AND fold instead of comma +// CPP14 mode doesn't like the use of void expressions +// with the way the _MDSPAN_FOLD_AND is set up +template +MDSPAN_INLINE_FUNCTION +constexpr bool +check_one_index(InputIndex user_index, + ExtentsIndexType current_extent) +{ + check_lower_bound(user_index, current_extent, + std::integral_constant::value>{}); + check_upper_bound(user_index, current_extent); + return true; +} + +template +MDSPAN_INLINE_FUNCTION +constexpr void +check_all_indices_helper(std::index_sequence, + const extents& exts, + Indices... indices) +{ + // Suppress warning about statement has no effect + (void) _MDSPAN_FOLD_AND( + (check_one_index(indices, exts.extent(RankIndices))) + ); +} + +template +MDSPAN_INLINE_FUNCTION +constexpr void +check_all_indices(const extents& exts, + Indices... indices) +{ + check_all_indices_helper(std::make_index_sequence(), + exts, indices...); +} + +} // namespace detail +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/extents.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/layout_stride.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/compressed_pair.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +#if !defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/no_unique_address.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace detail { + +//============================================================================== + +template +struct __no_unique_address_emulation { + using __stored_type = _T; + _T __v; + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T const &__ref() const noexcept { + return __v; + } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T &__ref() noexcept { + return __v; + } +}; + +// Empty case +// This doesn't work if _T is final, of course, but we're not using anything +// like that currently. That kind of thing could be added pretty easily though +template +struct __no_unique_address_emulation< + _T, _Disambiguator, + std::enable_if_t<_MDSPAN_TRAIT(std::is_empty, _T) && + // If the type isn't trivially destructible, its destructor + // won't be called at the right time, so don't use this + // specialization + _MDSPAN_TRAIT(std::is_trivially_destructible, _T)>> : +#ifdef _MDSPAN_COMPILER_MSVC + // MSVC doesn't allow you to access public static member functions of a type + // when you *happen* to privately inherit from that type. + protected +#else + // But we still want this to be private if possible so that we don't accidentally + // access members of _T directly rather than calling __ref() first, which wouldn't + // work if _T happens to be stateful and thus we're using the unspecialized definition + // of __no_unique_address_emulation above. + private +#endif + _T { + using __stored_type = _T; + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T const &__ref() const noexcept { + return *static_cast<_T const *>(this); + } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T &__ref() noexcept { + return *static_cast<_T *>(this); + } + + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __no_unique_address_emulation() noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __no_unique_address_emulation( + __no_unique_address_emulation const &) noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __no_unique_address_emulation( + __no_unique_address_emulation &&) noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __no_unique_address_emulation & + operator=(__no_unique_address_emulation const &) noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __no_unique_address_emulation & + operator=(__no_unique_address_emulation &&) noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + ~__no_unique_address_emulation() noexcept = default; + + // Explicitly make this not a reference so that the copy or move + // constructor still gets called. + MDSPAN_INLINE_FUNCTION + explicit constexpr __no_unique_address_emulation(_T const& __v) noexcept : _T(__v) {} + MDSPAN_INLINE_FUNCTION + explicit constexpr __no_unique_address_emulation(_T&& __v) noexcept : _T(::std::move(__v)) {} +}; + +//============================================================================== + +} // end namespace detail +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/no_unique_address.hpp +#endif + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace detail { + +// For no unique address emulation, this is the case taken when neither are empty. +// For real `[[no_unique_address]]`, this case is always taken. +template struct __compressed_pair { + _MDSPAN_NO_UNIQUE_ADDRESS _T1 __t1_val{}; + _MDSPAN_NO_UNIQUE_ADDRESS _T2 __t2_val{}; + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T1 &__first() noexcept { return __t1_val; } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T1 const &__first() const noexcept { + return __t1_val; + } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T2 &__second() noexcept { return __t2_val; } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T2 const &__second() const noexcept { + return __t2_val; + } + + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair() = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + ~__compressed_pair() = default; + template + MDSPAN_INLINE_FUNCTION constexpr __compressed_pair(_T1Like &&__t1, _T2Like &&__t2) + : __t1_val((_T1Like &&) __t1), __t2_val((_T2Like &&) __t2) {} +}; + +#if !defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + +// First empty. +template +struct __compressed_pair< + _T1, _T2, + std::enable_if_t<_MDSPAN_TRAIT(std::is_empty, _T1) && !_MDSPAN_TRAIT(std::is_empty, _T2)>> + : private _T1 { + _T2 __t2_val{}; + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T1 &__first() noexcept { + return *static_cast<_T1 *>(this); + } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T1 const &__first() const noexcept { + return *static_cast<_T1 const *>(this); + } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T2 &__second() noexcept { return __t2_val; } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T2 const &__second() const noexcept { + return __t2_val; + } + + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair() = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + ~__compressed_pair() = default; + template + MDSPAN_INLINE_FUNCTION constexpr __compressed_pair(_T1Like &&__t1, _T2Like &&__t2) + : _T1((_T1Like &&) __t1), __t2_val((_T2Like &&) __t2) {} +}; + +// Second empty. +template +struct __compressed_pair< + _T1, _T2, + std::enable_if_t> + : private _T2 { + _T1 __t1_val{}; + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T1 &__first() noexcept { return __t1_val; } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T1 const &__first() const noexcept { + return __t1_val; + } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T2 &__second() noexcept { + return *static_cast<_T2 *>(this); + } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T2 const &__second() const noexcept { + return *static_cast<_T2 const *>(this); + } + + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair() = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + ~__compressed_pair() = default; + + template + MDSPAN_INLINE_FUNCTION constexpr __compressed_pair(_T1Like &&__t1, _T2Like &&__t2) + : _T2((_T2Like &&) __t2), __t1_val((_T1Like &&) __t1) {} +}; + +// Both empty. +template +struct __compressed_pair< + _T1, _T2, + std::enable_if_t<_MDSPAN_TRAIT(std::is_empty, _T1) && _MDSPAN_TRAIT(std::is_empty, _T2)>> + // We need to use the __no_unique_address_emulation wrapper here to avoid + // base class ambiguities. +#ifdef _MDSPAN_COMPILER_MSVC +// MSVC doesn't allow you to access public static member functions of a type +// when you *happen* to privately inherit from that type. + : protected __no_unique_address_emulation<_T1, 0>, + protected __no_unique_address_emulation<_T2, 1> +#else + : private __no_unique_address_emulation<_T1, 0>, + private __no_unique_address_emulation<_T2, 1> +#endif +{ + using __first_base_t = __no_unique_address_emulation<_T1, 0>; + using __second_base_t = __no_unique_address_emulation<_T2, 1>; + + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T1 &__first() noexcept { + return this->__first_base_t::__ref(); + } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T1 const &__first() const noexcept { + return this->__first_base_t::__ref(); + } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 _T2 &__second() noexcept { + return this->__second_base_t::__ref(); + } + MDSPAN_FORCE_INLINE_FUNCTION constexpr _T2 const &__second() const noexcept { + return this->__second_base_t::__ref(); + } + + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair() = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr __compressed_pair(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair const &) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + _MDSPAN_CONSTEXPR_14_DEFAULTED __compressed_pair & + operator=(__compressed_pair &&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED + ~__compressed_pair() = default; + template + MDSPAN_INLINE_FUNCTION constexpr __compressed_pair(_T1Like &&__t1, _T2Like &&__t2) noexcept + : __first_base_t(_T1((_T1Like &&) __t1)), + __second_base_t(_T2((_T2Like &&) __t2)) + { } +}; + +#endif // !defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + +} // end namespace detail +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/compressed_pair.hpp + +#if !defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) +#endif + +#include +#include +#include + +#ifdef __cpp_lib_span +#include +#endif +#if defined(_MDSPAN_USE_CONCEPTS) && MDSPAN_HAS_CXX_20 && defined(__cpp_lib_concepts) +# include +#endif + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +struct layout_left { + template + class mapping; +}; +struct layout_right { + template + class mapping; +}; + +namespace detail { + template + constexpr bool __is_mapping_of = + std::is_same, Mapping>::value; + +#if defined(_MDSPAN_USE_CONCEPTS) && MDSPAN_HAS_CXX_20 +# if !defined(__cpp_lib_concepts) + namespace internal { + namespace detail { + template + concept __same_as = std::is_same_v<_Tp, _Up>; + } // namespace detail + template + concept __same_as = detail::__same_as && detail::__same_as; + } // namespace internal +# endif + + template + concept __layout_mapping_alike = requires { + requires __is_extents::value; +#if defined(__cpp_lib_concepts) + { M::is_always_strided() } -> std::same_as; + { M::is_always_exhaustive() } -> std::same_as; + { M::is_always_unique() } -> std::same_as; +#else + { M::is_always_strided() } -> internal::__same_as; + { M::is_always_exhaustive() } -> internal::__same_as; + { M::is_always_unique() } -> internal::__same_as; +#endif + std::bool_constant::value; + std::bool_constant::value; + std::bool_constant::value; + }; +#endif + +} // namespace detail + +struct layout_stride { + template + class mapping +#if !defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + : private detail::__no_unique_address_emulation< + detail::__compressed_pair< + Extents, + detail::possibly_empty_array + > + > +#endif + { + public: + using extents_type = Extents; + using index_type = typename extents_type::index_type; + using size_type = typename extents_type::size_type; + using rank_type = typename extents_type::rank_type; + using layout_type = layout_stride; + + // This could be a `requires`, but I think it's better and clearer as a `static_assert`. + static_assert(detail::__is_extents_v, + MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::layout_stride::mapping must be instantiated with a specialization of " MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::extents."); + + + private: + + //---------------------------------------------------------------------------- + + using __strides_storage_t = detail::possibly_empty_array; + using __member_pair_t = detail::__compressed_pair; + +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + _MDSPAN_NO_UNIQUE_ADDRESS __member_pair_t __members; +#else + using __base_t = detail::__no_unique_address_emulation<__member_pair_t>; +#endif + + MDSPAN_FORCE_INLINE_FUNCTION constexpr __strides_storage_t const& + __strides_storage() const noexcept { +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + return __members.__second(); +#else + return this->__base_t::__ref().__second(); +#endif + } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 __strides_storage_t& + __strides_storage() noexcept { +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + return __members.__second(); +#else + return this->__base_t::__ref().__second(); +#endif + } + + template + _MDSPAN_HOST_DEVICE + constexpr index_type __get_size(::MDSPAN_IMPL_STANDARD_NAMESPACE::extents,std::integer_sequence) const { + return _MDSPAN_FOLD_TIMES_RIGHT( static_cast(extents().extent(Idx)), 1 ); + } + + //---------------------------------------------------------------------------- + + template + friend class mapping; + + //---------------------------------------------------------------------------- + + // Workaround for non-deducibility of the index sequence template parameter if it's given at the top level + template + struct __deduction_workaround; + + template + struct __deduction_workaround> + { + template + MDSPAN_INLINE_FUNCTION + static constexpr bool _eq_impl(mapping const& self, mapping const& other) noexcept { + using common_t = std::common_type_t; + return _MDSPAN_FOLD_AND((static_cast(self.stride(Idxs)) == static_cast(other.stride(Idxs))) /* && ... */) + && _MDSPAN_FOLD_AND((static_cast(self.extents().extent(Idxs)) == static_cast(other.extents().extent(Idxs))) /* || ... */); + } + template + MDSPAN_INLINE_FUNCTION + static constexpr bool _not_eq_impl(mapping const& self, mapping const& other) noexcept { + using common_t = std::common_type_t; + return _MDSPAN_FOLD_OR((static_cast(self.stride(Idxs)) != static_cast(other.stride(Idxs))) /* || ... */) + || _MDSPAN_FOLD_OR((static_cast(self.extents().extent(Idxs)) != static_cast(other.extents().extent(Idxs))) /* || ... */); + } + + template + MDSPAN_FORCE_INLINE_FUNCTION + static constexpr size_t _call_op_impl(mapping const& self, Integral... idxs) noexcept { + return _MDSPAN_FOLD_PLUS_RIGHT((idxs * self.stride(Idxs)), /* + ... + */ 0); + } + + MDSPAN_INLINE_FUNCTION + static constexpr size_t _req_span_size_impl(mapping const& self) noexcept { + // assumes no negative strides; not sure if I'm allowed to assume that or not + return __impl::_call_op_impl(self, (self.extents().template __extent() - 1)...) + 1; + } + + template + MDSPAN_INLINE_FUNCTION + static constexpr const __strides_storage_t fill_strides(const OtherMapping& map) { + return __strides_storage_t{static_cast(map.stride(Idxs))...}; + } + + MDSPAN_INLINE_FUNCTION + static constexpr const __strides_storage_t& fill_strides(const __strides_storage_t& s) { + return s; + } + + template + MDSPAN_INLINE_FUNCTION + static constexpr const __strides_storage_t fill_strides(const std::array& s) { + return __strides_storage_t{static_cast(s[Idxs])...}; + } + + template + MDSPAN_INLINE_FUNCTION + static constexpr const __strides_storage_t fill_strides(mdspan_non_standard_tag, const IntegralType (&s)[extents_type::rank()]) { + return __strides_storage_t{static_cast(s[Idxs])...}; + } + +#ifdef __cpp_lib_span + template + MDSPAN_INLINE_FUNCTION + static constexpr const __strides_storage_t fill_strides(const std::span& s) { + return __strides_storage_t{static_cast(s[Idxs])...}; + } +#endif + + MDSPAN_INLINE_FUNCTION + static constexpr std::array return_strides(const __strides_storage_t& s) { + return std::array{s[Idxs]...}; + } + + template + MDSPAN_INLINE_FUNCTION + static constexpr size_t __return_zero() { return 0; } + + template + MDSPAN_INLINE_FUNCTION + static constexpr typename Mapping::index_type + __OFFSET(const Mapping& m) { return m(__return_zero()...); } + }; + + // Can't use defaulted parameter in the __deduction_workaround template because of a bug in MSVC warning C4348. + using __impl = __deduction_workaround>; + + static constexpr __strides_storage_t strides_storage(detail::with_rank<0>) { + return {}; + } + template + static constexpr __strides_storage_t strides_storage(detail::with_rank) { + __strides_storage_t s{}; + + extents_type e; + index_type stride = 1; + for(int r = static_cast(extents_type::rank() - 1); r >= 0; r--) { + s[r] = stride; + stride *= e.extent(r); + } + + return s; + } + + //---------------------------------------------------------------------------- + +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + MDSPAN_INLINE_FUNCTION constexpr explicit + mapping(__member_pair_t&& __m) : __members(::std::move(__m)) {} +#else + MDSPAN_INLINE_FUNCTION constexpr explicit + mapping(__base_t&& __b) : __base_t(::std::move(__b)) {} +#endif + + public: + + //-------------------------------------------------------------------------------- + + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping() noexcept +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + : __members{ +#else + : __base_t(__base_t{__member_pair_t( +#endif + extents_type(), + __strides_storage_t(strides_storage(detail::with_rank{})) +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + } +#else + )}) +#endif + {} + + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping(mapping const&) noexcept = default; + + MDSPAN_TEMPLATE_REQUIRES( + class IntegralTypes, + /* requires */ ( + // MSVC 19.32 does not like using index_type here, requires the typename Extents::index_type + // error C2641: cannot deduce template arguments for 'MDSPAN_IMPL_STANDARD_NAMESPACE::layout_stride::mapping' + _MDSPAN_TRAIT(std::is_convertible, const std::remove_const_t&, typename Extents::index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, typename Extents::index_type, const std::remove_const_t&) + ) + ) + MDSPAN_INLINE_FUNCTION + constexpr + mapping( + extents_type const& e, + std::array const& s + ) noexcept +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + : __members{ +#else + : __base_t(__base_t{__member_pair_t( +#endif + e, __strides_storage_t(__impl::fill_strides(s)) +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + } +#else + )}) +#endif + { + /* + * TODO: check preconditions + * - s[i] > 0 is true for all i in the range [0, rank_ ). + * - REQUIRED-SPAN-SIZE(e, s) is a representable value of type index_type ([basic.fundamental]). + * - If rank_ is greater than 0, then there exists a permutation P of the integers in the + * range [0, rank_), such that s[ pi ] >= s[ pi − 1 ] * e.extent( pi − 1 ) is true for + * all i in the range [1, rank_ ), where pi is the ith element of P. + */ + } + + MDSPAN_TEMPLATE_REQUIRES( + class IntegralTypes, + /* requires */ ( + // MSVC 19.32 does not like using index_type here, requires the typename Extents::index_type + // error C2641: cannot deduce template arguments for 'MDSPAN_IMPL_STANDARD_NAMESPACE::layout_stride::mapping' + _MDSPAN_TRAIT(std::is_convertible, const std::remove_const_t&, typename Extents::index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, typename Extents::index_type, const std::remove_const_t&) + ) + ) + MDSPAN_INLINE_FUNCTION + constexpr + mapping( + mdspan_non_standard_tag, + extents_type const& e, + IntegralTypes (&s)[extents_type::rank()] + ) noexcept +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + : __members{ +#else + : __base_t(__base_t{__member_pair_t( +#endif + e, __strides_storage_t(__impl::fill_strides(mdspan_non_standard, s)) +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + } +#else + )}) +#endif + { + /* + * TODO: check preconditions + * - s[i] > 0 is true for all i in the range [0, rank_ ). + * - REQUIRED-SPAN-SIZE(e, s) is a representable value of type index_type ([basic.fundamental]). + * - If rank_ is greater than 0, then there exists a permutation P of the integers in the + * range [0, rank_), such that s[ pi ] >= s[ pi − 1 ] * e.extent( pi − 1 ) is true for + * all i in the range [1, rank_ ), where pi is the ith element of P. + */ + } + +#ifdef __cpp_lib_span + MDSPAN_TEMPLATE_REQUIRES( + class IntegralTypes, + /* requires */ ( + // MSVC 19.32 does not like using index_type here, requires the typename Extents::index_type + // error C2641: cannot deduce template arguments for 'MDSPAN_IMPL_STANDARD_NAMESPACE::layout_stride::mapping' + _MDSPAN_TRAIT(std::is_convertible, const std::remove_const_t&, typename Extents::index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, typename Extents::index_type, const std::remove_const_t&) + ) + ) + MDSPAN_INLINE_FUNCTION + constexpr + mapping( + extents_type const& e, + std::span const& s + ) noexcept +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + : __members{ +#else + : __base_t(__base_t{__member_pair_t( +#endif + e, __strides_storage_t(__impl::fill_strides(s)) +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + } +#else + )}) +#endif + { + /* + * TODO: check preconditions + * - s[i] > 0 is true for all i in the range [0, rank_ ). + * - REQUIRED-SPAN-SIZE(e, s) is a representable value of type index_type ([basic.fundamental]). + * - If rank_ is greater than 0, then there exists a permutation P of the integers in the + * range [0, rank_), such that s[ pi ] >= s[ pi − 1 ] * e.extent( pi − 1 ) is true for + * all i in the range [1, rank_ ), where pi is the ith element of P. + */ + } +#endif // __cpp_lib_span + +#if !(defined(_MDSPAN_USE_CONCEPTS) && MDSPAN_HAS_CXX_20) + MDSPAN_TEMPLATE_REQUIRES( + class StridedLayoutMapping, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, extents_type, typename StridedLayoutMapping::extents_type) && + detail::__is_mapping_of && + StridedLayoutMapping::is_always_unique() && + StridedLayoutMapping::is_always_strided() + ) + ) +#else + template + requires( + detail::__layout_mapping_alike && + _MDSPAN_TRAIT(std::is_constructible, extents_type, typename StridedLayoutMapping::extents_type) && + StridedLayoutMapping::is_always_unique() && + StridedLayoutMapping::is_always_strided() + ) +#endif + MDSPAN_CONDITIONAL_EXPLICIT( + !(std::is_convertible::value && + (detail::__is_mapping_of || + detail::__is_mapping_of || + detail::__is_mapping_of)) + ) // needs two () due to comma + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 + mapping(StridedLayoutMapping const& other) noexcept // NOLINT(google-explicit-constructor) +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + : __members{ +#else + : __base_t(__base_t{__member_pair_t( +#endif + other.extents(), __strides_storage_t(__impl::fill_strides(other)) +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + } +#else + )}) +#endif + { + /* + * TODO: check preconditions + * - other.stride(i) > 0 is true for all i in the range [0, rank_ ). + * - other.required_span_size() is a representable value of type index_type ([basic.fundamental]). + * - OFFSET(other) == 0 + */ + } + + //-------------------------------------------------------------------------------- + + MDSPAN_INLINE_FUNCTION_DEFAULTED _MDSPAN_CONSTEXPR_14_DEFAULTED + mapping& operator=(mapping const&) noexcept = default; + + MDSPAN_INLINE_FUNCTION constexpr const extents_type& extents() const noexcept { +#if defined(_MDSPAN_USE_ATTRIBUTE_NO_UNIQUE_ADDRESS) + return __members.__first(); +#else + return this->__base_t::__ref().__first(); +#endif + }; + + MDSPAN_INLINE_FUNCTION + constexpr std::array< index_type, extents_type::rank() > strides() const noexcept { + return __impl::return_strides(__strides_storage()); + } + + MDSPAN_INLINE_FUNCTION + constexpr index_type required_span_size() const noexcept { + index_type span_size = 1; + for(unsigned r = 0; r < extents_type::rank(); r++) { + // Return early if any of the extents are zero + if(extents().extent(r)==0) return 0; + span_size += ( static_cast(extents().extent(r) - 1 ) * __strides_storage()[r]); + } + return span_size; + } + + + MDSPAN_TEMPLATE_REQUIRES( + class... Indices, + /* requires */ ( + sizeof...(Indices) == Extents::rank() && + (detail::are_valid_indices()) + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr index_type operator()(Indices... idxs) const noexcept { +#if ! defined(NDEBUG) + detail::check_all_indices(this->extents(), idxs...); +#endif // ! NDEBUG + return static_cast(__impl::_call_op_impl(*this, static_cast(idxs)...)); + } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_unique() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_exhaustive() noexcept { + return false; + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_strided() noexcept { return true; } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_unique() noexcept { return true; } + + private: + constexpr bool exhaustive_for_nonzero_span_size() const + { + return required_span_size() == __get_size(extents(), std::make_index_sequence()); + } + + constexpr bool is_exhaustive_impl(detail::with_rank<0>) const + { + return true; + } + constexpr bool is_exhaustive_impl(detail::with_rank<1>) const + { + if (required_span_size() != static_cast(0)) { + return exhaustive_for_nonzero_span_size(); + } + return stride(0) == 1; + } + template + constexpr bool is_exhaustive_impl(detail::with_rank) const + { + if (required_span_size() != static_cast(0)) { + return exhaustive_for_nonzero_span_size(); + } + + rank_type r_largest = 0; + for (rank_type r = 1; r < extents_type::rank(); r++) { + if (stride(r) > stride(r_largest)) { + r_largest = r; + } + } + for (rank_type r = 0; r < extents_type::rank(); r++) { + if (extents().extent(r) == 0 && r != r_largest) { + return false; + } + } + return true; + } + + public: + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 bool is_exhaustive() const noexcept { + return is_exhaustive_impl(detail::with_rank{}); + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_strided() noexcept { return true; } + + + MDSPAN_INLINE_FUNCTION + constexpr index_type stride(rank_type r) const noexcept { + return __strides_storage()[r]; + } + +#if !(defined(_MDSPAN_USE_CONCEPTS) && MDSPAN_HAS_CXX_20) + MDSPAN_TEMPLATE_REQUIRES( + class StridedLayoutMapping, + /* requires */ ( + detail::__is_mapping_of && + (extents_type::rank() == StridedLayoutMapping::extents_type::rank()) && + StridedLayoutMapping::is_always_strided() + ) + ) +#else + template + requires( + detail::__layout_mapping_alike && + (extents_type::rank() == StridedLayoutMapping::extents_type::rank()) && + StridedLayoutMapping::is_always_strided() + ) +#endif + MDSPAN_INLINE_FUNCTION + friend constexpr bool operator==(const mapping& x, const StridedLayoutMapping& y) noexcept { + return (x.extents() == y.extents()) && + (__impl::__OFFSET(y) == static_cast(0)) && + detail::rankwise_equal(detail::with_rank{}, x, y, detail::stride); + } + + // This one is not technically part of the proposal. Just here to make implementation a bit more optimal hopefully + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + (extents_type::rank() == OtherExtents::rank()) + ) + ) + MDSPAN_INLINE_FUNCTION + friend constexpr bool operator==(mapping const& lhs, mapping const& rhs) noexcept { + return __impl::_eq_impl(lhs, rhs); + } + +#if !MDSPAN_HAS_CXX_20 + MDSPAN_TEMPLATE_REQUIRES( + class StridedLayoutMapping, + /* requires */ ( + detail::__is_mapping_of && + (extents_type::rank() == StridedLayoutMapping::extents_type::rank()) && + StridedLayoutMapping::is_always_strided() + ) + ) + MDSPAN_INLINE_FUNCTION + friend constexpr bool operator!=(const mapping& x, const StridedLayoutMapping& y) noexcept { + return not (x == y); + } + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + (extents_type::rank() == OtherExtents::rank()) + ) + ) + MDSPAN_INLINE_FUNCTION + friend constexpr bool operator!=(mapping const& lhs, mapping const& rhs) noexcept { + return __impl::_not_eq_impl(lhs, rhs); + } +#endif + + // [mdspan.submdspan.mapping], submdspan mapping specialization + template + MDSPAN_INLINE_FUNCTION + constexpr auto submdspan_mapping_impl( + SliceSpecifiers... slices) const; + + template + friend constexpr auto submdspan_mapping( + const mapping& src, SliceSpecifiers... slices) { + return src.submdspan_mapping_impl(slices...); + } + }; +}; + +namespace detail { + +template +constexpr void validate_strides(with_rank<0>, Layout, const Extents&, const Mapping&) +{} + +template +constexpr void validate_strides(with_rank, Layout, const Extents& ext, const Mapping& other) +{ + static_assert(std::is_same::value and + (std::is_same::value or + std::is_same::value) + , "This function is only intended to validate construction of " + "a layout_left or layout_right mapping from a layout_stride mapping."); + + constexpr auto is_left = std::is_same::value; + + typename Extents::index_type stride = 1; + + for (std::size_t r = 0; r < N; r++) { + const std::size_t s = is_left ? r : N - 1 - r; + + MDSPAN_IMPL_PRECONDITION(common_integral_compare(stride, other.stride(s)) + and "invalid strides for layout_{left,right}"); + + stride *= ext.extent(s); + } +} + +} // namespace detail +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/layout_stride.hpp +#if MDSPAN_HAS_CXX_17 +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p2642_bits/layout_padded_fwd.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +#include + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace MDSPAN_IMPL_PROPOSED_NAMESPACE { + +template +struct layout_left_padded { + template + class mapping; +}; + +template +struct layout_right_padded { + template + class mapping; +}; + +namespace detail { +// The layout_padded_constants structs are only useful if rank > 1, otherwise they may wrap +template +struct layout_padded_constants; + +template +struct layout_padded_constants, _ExtentsType> +{ + using rank_type = typename _ExtentsType::rank_type; + static constexpr rank_type padded_stride_idx = 1; + static constexpr rank_type extent_to_pad_idx = 0; +}; + +template +struct layout_padded_constants, _ExtentsType> +{ + using rank_type = typename _ExtentsType::rank_type; + static constexpr rank_type padded_stride_idx = _ExtentsType::rank() - 2; + static constexpr rank_type extent_to_pad_idx = _ExtentsType::rank() - 1; +}; + +template +struct is_layout_left_padded : std::false_type {}; + +template +struct is_layout_left_padded> : std::true_type {}; + +template +struct is_layout_left_padded_mapping : std::false_type {}; + +template +struct is_layout_left_padded_mapping<_Mapping, + std::enable_if_t::template mapping>::value>> + : std::true_type {}; + +template +struct is_layout_right_padded : std::false_type {}; + +template +struct is_layout_right_padded> : std::true_type {}; + +template +struct is_layout_right_padded_mapping : std::false_type {}; + +template +struct is_layout_right_padded_mapping<_Mapping, + std::enable_if_t::template mapping>::value>> + : std::true_type {}; + + +template +constexpr void check_padded_layout_converting_constructor_mandates(MDSPAN_IMPL_STANDARD_NAMESPACE::detail::with_rank<0>) {} + +template +constexpr void check_padded_layout_converting_constructor_mandates(MDSPAN_IMPL_STANDARD_NAMESPACE::detail::with_rank<1>) {} + +template +constexpr void check_padded_layout_converting_constructor_mandates(MDSPAN_IMPL_STANDARD_NAMESPACE::detail::with_rank) +{ + using extents_type = typename _PaddedLayoutMappingType::extents_type; + constexpr auto padding_value = _PaddedLayoutMappingType::padding_value; + constexpr auto idx = layout_padded_constants::extent_to_pad_idx; + + constexpr auto statically_determinable = + (_LayoutExtentsType::static_extent(idx) != dynamic_extent) && + (extents_type::static_extent(idx) != dynamic_extent) && + (padding_value != dynamic_extent); + + static_assert(not statically_determinable or + (padding_value == 0 + ? _LayoutExtentsType::static_extent(idx) == 0 + : _LayoutExtentsType::static_extent(idx) % padding_value == 0), + ""); +} + +template +constexpr void check_padded_layout_converting_constructor_preconditions(MDSPAN_IMPL_STANDARD_NAMESPACE::detail::with_rank<0>, + const _OtherMapping&) {} +template +constexpr void check_padded_layout_converting_constructor_preconditions(MDSPAN_IMPL_STANDARD_NAMESPACE::detail::with_rank<1>, + const _OtherMapping&) {} +template +constexpr void check_padded_layout_converting_constructor_preconditions(MDSPAN_IMPL_STANDARD_NAMESPACE::detail::with_rank, + const _OtherMapping &other_mapping) { + constexpr auto padded_stride_idx = + layout_padded_constants::padded_stride_idx; + constexpr auto extent_to_pad_idx = layout_padded_constants::extent_to_pad_idx; + MDSPAN_IMPL_PRECONDITION(other_mapping.stride(padded_stride_idx) == other_mapping.extents().extent(extent_to_pad_idx)); +} + + +} +} +} +//END_FILE_INCLUDE: mdspan/include/experimental/__p2642_bits/layout_padded_fwd.hpp +#endif + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +//============================================================================== +template +class layout_right::mapping { + public: + using extents_type = Extents; + using index_type = typename extents_type::index_type; + using size_type = typename extents_type::size_type; + using rank_type = typename extents_type::rank_type; + using layout_type = layout_right; + private: + + static_assert(detail::__is_extents_v, + MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::layout_right::mapping must be instantiated with a specialization of " MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::extents."); + + template + friend class mapping; + + // i0+(i1 + E(1)*(i2 + E(2)*i3)) + template + struct __rank_count {}; + + template + _MDSPAN_HOST_DEVICE + constexpr index_type __compute_offset( + index_type offset, __rank_count, const I& i, Indices... idx) const { + return __compute_offset(offset * __extents.extent(r) + i,__rank_count(), idx...); + } + + template + _MDSPAN_HOST_DEVICE + constexpr index_type __compute_offset( + __rank_count<0,extents_type::rank()>, const I& i, Indices... idx) const { + return __compute_offset(i,__rank_count<1,extents_type::rank()>(),idx...); + } + + _MDSPAN_HOST_DEVICE + constexpr index_type __compute_offset(size_t offset, __rank_count) const { + return static_cast(offset); + } + + _MDSPAN_HOST_DEVICE + constexpr index_type __compute_offset(__rank_count<0,0>) const { return 0; } + + public: + + //-------------------------------------------------------------------------------- + + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping() noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping(mapping const&) noexcept = default; + + _MDSPAN_HOST_DEVICE + constexpr mapping(extents_type const& __exts) noexcept + :__extents(__exts) + { } + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, extents_type, OtherExtents) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT((!std::is_convertible::value)) // needs two () due to comma + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 + mapping(mapping const& other) noexcept // NOLINT(google-explicit-constructor) + :__extents(other.extents()) + { + /* + * TODO: check precondition + * other.required_span_size() is a representable value of type index_type + */ + } + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, extents_type, OtherExtents) && + (extents_type::rank() <= 1) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT((!std::is_convertible::value)) // needs two () due to comma + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 + mapping(layout_left::mapping const& other) noexcept // NOLINT(google-explicit-constructor) + :__extents(other.extents()) + { + /* + * TODO: check precondition + * other.required_span_size() is a representable value of type index_type + */ + } + + /** + * Converting constructor from `layout_right_padded::mapping`. + * + * This overload participates in overload resolution only if _Mapping is a layout_right_padded mapping and + * extents_type is constructible from _Mapping::extents_type. + * + * \note There is currently a difference from p2642r2, where this function is specified as taking + * `layout_right_padded< padding_value >::mapping< Extents>`. However, this makes `padding_value` non-deducible. + */ +#if MDSPAN_HAS_CXX_17 + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ ( + MDSPAN_IMPL_PROPOSED_NAMESPACE::detail::is_layout_right_padded_mapping<_Mapping>::value + && std::is_constructible_v)) + MDSPAN_CONDITIONAL_EXPLICIT((!std::is_convertible_v)) + mapping(const _Mapping &__other) noexcept + : __extents(__other.extents()) + { + MDSPAN_IMPL_PROPOSED_NAMESPACE::detail:: + check_padded_layout_converting_constructor_mandates< + extents_type, _Mapping>(detail::with_rank{}); + MDSPAN_IMPL_PROPOSED_NAMESPACE::detail:: + check_padded_layout_converting_constructor_preconditions< + extents_type>(detail::with_rank{}, __other); + } +#endif + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, extents_type, OtherExtents) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT((extents_type::rank() > 0)) + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 + mapping(layout_stride::mapping const& other) noexcept // NOLINT(google-explicit-constructor) + :__extents(other.extents()) + { + /* + * TODO: check precondition + * other.required_span_size() is a representable value of type index_type + */ + detail::validate_strides(detail::with_rank{}, layout_right{}, __extents, other); + } + + MDSPAN_INLINE_FUNCTION_DEFAULTED _MDSPAN_CONSTEXPR_14_DEFAULTED mapping& operator=(mapping const&) noexcept = default; + + MDSPAN_INLINE_FUNCTION + constexpr const extents_type& extents() const noexcept { + return __extents; + } + + MDSPAN_INLINE_FUNCTION + constexpr index_type required_span_size() const noexcept { + index_type value = 1; + for(rank_type r=0; r != extents_type::rank(); ++r) value*=__extents.extent(r); + return value; + } + + //-------------------------------------------------------------------------------- + + MDSPAN_TEMPLATE_REQUIRES( + class ... Indices, + /* requires */ ( + (sizeof...(Indices) == extents_type::rank()) && + (detail::are_valid_indices()) + ) + ) + _MDSPAN_HOST_DEVICE + constexpr index_type operator()(Indices... idxs) const noexcept { +#if ! defined(NDEBUG) + detail::check_all_indices(this->extents(), idxs...); +#endif // ! NDEBUG + return __compute_offset(__rank_count<0, extents_type::rank()>(), static_cast(idxs)...); + } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_unique() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_exhaustive() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_strided() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_unique() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_exhaustive() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_strided() noexcept { return true; } + + MDSPAN_INLINE_FUNCTION + constexpr index_type stride(rank_type i) const noexcept +#if MDSPAN_HAS_CXX_20 + requires ( Extents::rank() > 0 ) +#endif + { + index_type value = 1; + for(rank_type r=extents_type::rank()-1; r>i; r--) value*=__extents.extent(r); + return value; + } + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( Extents::rank() == OtherExtents::rank()) + ) + MDSPAN_INLINE_FUNCTION + friend constexpr bool operator==(mapping const& lhs, mapping const& rhs) noexcept { + return lhs.extents() == rhs.extents(); + } + + // In C++ 20 the not equal exists if equal is found +#if !(MDSPAN_HAS_CXX_20) + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ (Extents::rank() == OtherExtents::rank()) + ) + MDSPAN_INLINE_FUNCTION + friend constexpr bool operator!=(mapping const& lhs, mapping const& rhs) noexcept { + return lhs.extents() != rhs.extents(); + } +#endif + + // Not really public, but currently needed to implement fully constexpr useable submdspan: + template + constexpr index_type __get_stride(MDSPAN_IMPL_STANDARD_NAMESPACE::extents,std::integer_sequence) const { + return _MDSPAN_FOLD_TIMES_RIGHT((Idx>N? __extents.template __extent():1),1); + } + template + constexpr index_type __stride() const noexcept { + return __get_stride(__extents, std::make_index_sequence()); + } + +private: + _MDSPAN_NO_UNIQUE_ADDRESS extents_type __extents{}; + + // [mdspan.submdspan.mapping], submdspan mapping specialization + template + MDSPAN_INLINE_FUNCTION + constexpr auto submdspan_mapping_impl( + SliceSpecifiers... slices) const; + + template + friend constexpr auto submdspan_mapping( + const mapping& src, SliceSpecifiers... slices) { + return src.submdspan_mapping_impl(slices...); + } +}; + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/layout_right.hpp + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +template < + class ElementType, + class Extents, + class LayoutPolicy = layout_right, + class AccessorPolicy = default_accessor +> +class mdspan +{ +private: + static_assert(detail::__is_extents_v, + MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::mdspan's Extents template parameter must be a specialization of " MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::extents."); + static_assert(std::is_same::value, + MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::mdspan's ElementType template parameter must be the same as its AccessorPolicy::element_type."); + + // Workaround for non-deducibility of the index sequence template parameter if it's given at the top level + template + struct __deduction_workaround; + + template + struct __deduction_workaround> + { + MDSPAN_FORCE_INLINE_FUNCTION static constexpr + size_t __size(mdspan const& __self) noexcept { + return _MDSPAN_FOLD_TIMES_RIGHT((__self.__mapping_ref().extents().extent(Idxs)), /* * ... * */ size_t(1)); + } + MDSPAN_FORCE_INLINE_FUNCTION static constexpr + bool __empty(mdspan const& __self) noexcept { + return (__self.rank()>0) && _MDSPAN_FOLD_OR((__self.__mapping_ref().extents().extent(Idxs)==index_type(0))); + } + template + MDSPAN_FORCE_INLINE_FUNCTION static constexpr + ReferenceType __callop(mdspan const& __self, const std::array& indices) noexcept { + return __self.__accessor_ref().access(__self.__ptr_ref(), __self.__mapping_ref()(indices[Idxs]...)); + } +#ifdef __cpp_lib_span + template + MDSPAN_FORCE_INLINE_FUNCTION static constexpr + ReferenceType __callop(mdspan const& __self, const std::span& indices) noexcept { + return __self.__accessor_ref().access(__self.__ptr_ref(), __self.__mapping_ref()(indices[Idxs]...)); + } +#endif + }; + +public: + + //-------------------------------------------------------------------------------- + // Domain and codomain types + + using extents_type = Extents; + using layout_type = LayoutPolicy; + using accessor_type = AccessorPolicy; + using mapping_type = typename layout_type::template mapping; + using element_type = ElementType; + using value_type = std::remove_cv_t; + using index_type = typename extents_type::index_type; + using size_type = typename extents_type::size_type; + using rank_type = typename extents_type::rank_type; + using data_handle_type = typename accessor_type::data_handle_type; + using reference = typename accessor_type::reference; + + MDSPAN_INLINE_FUNCTION static constexpr size_t rank() noexcept { return extents_type::rank(); } + MDSPAN_INLINE_FUNCTION static constexpr size_t rank_dynamic() noexcept { return extents_type::rank_dynamic(); } + MDSPAN_INLINE_FUNCTION static constexpr size_t static_extent(size_t r) noexcept { return extents_type::static_extent(r); } + MDSPAN_INLINE_FUNCTION constexpr index_type extent(size_t r) const noexcept { return __mapping_ref().extents().extent(r); }; + +private: + + // Can't use defaulted parameter in the __deduction_workaround template because of a bug in MSVC warning C4348. + using __impl = __deduction_workaround>; + + using __map_acc_pair_t = detail::__compressed_pair; + +public: + + //-------------------------------------------------------------------------------- + // [mdspan.basic.cons], mdspan constructors, assignment, and destructor + +#if !MDSPAN_HAS_CXX_20 + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mdspan() = default; +#else + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mdspan() + requires( + // nvhpc has a bug where using just rank_dynamic() here doesn't work ... + (extents_type::rank_dynamic() > 0) && + _MDSPAN_TRAIT(std::is_default_constructible, data_handle_type) && + _MDSPAN_TRAIT(std::is_default_constructible, mapping_type) && + _MDSPAN_TRAIT(std::is_default_constructible, accessor_type) + ) = default; +#endif + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mdspan(const mdspan&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mdspan(mdspan&&) = default; + + MDSPAN_TEMPLATE_REQUIRES( + class... SizeTypes, + /* requires */ ( + ((sizeof...(SizeTypes) == rank()) || (sizeof...(SizeTypes) == rank_dynamic())) && + (detail::are_valid_indices()) && + _MDSPAN_TRAIT(std::is_constructible, mapping_type, extents_type) && + _MDSPAN_TRAIT(std::is_default_constructible, accessor_type) + ) + ) + MDSPAN_INLINE_FUNCTION + explicit constexpr mdspan(data_handle_type p, SizeTypes... dynamic_extents) + // TODO @proposal-bug shouldn't I be allowed to do `move(p)` here? + : __members(std::move(p), __map_acc_pair_t(mapping_type(extents_type(static_cast(std::move(dynamic_extents))...)), accessor_type())) + { } + + MDSPAN_TEMPLATE_REQUIRES( + class SizeType, size_t N, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, const SizeType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, const SizeType&) && + ((N == rank()) || (N == rank_dynamic())) && + _MDSPAN_TRAIT(std::is_constructible, mapping_type, extents_type) && + _MDSPAN_TRAIT(std::is_default_constructible, accessor_type) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT(N != rank_dynamic()) + MDSPAN_INLINE_FUNCTION + constexpr mdspan(data_handle_type p, const std::array& dynamic_extents) + : __members(std::move(p), __map_acc_pair_t(mapping_type(extents_type(dynamic_extents)), accessor_type())) + { } + +#ifdef __cpp_lib_span + MDSPAN_TEMPLATE_REQUIRES( + class SizeType, size_t N, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, const SizeType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, const SizeType&) && + ((N == rank()) || (N == rank_dynamic())) && + _MDSPAN_TRAIT(std::is_constructible, mapping_type, extents_type) && + _MDSPAN_TRAIT(std::is_default_constructible, accessor_type) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT(N != rank_dynamic()) + MDSPAN_INLINE_FUNCTION + constexpr mdspan(data_handle_type p, std::span dynamic_extents) + : __members(std::move(p), __map_acc_pair_t(mapping_type(extents_type(as_const(dynamic_extents))), accessor_type())) + { } +#endif + + MDSPAN_FUNCTION_REQUIRES( + (MDSPAN_INLINE_FUNCTION constexpr), + mdspan, (data_handle_type p, const extents_type& exts), , + /* requires */ (_MDSPAN_TRAIT(std::is_default_constructible, accessor_type) && + _MDSPAN_TRAIT(std::is_constructible, mapping_type, const extents_type&)) + ) : __members(std::move(p), __map_acc_pair_t(mapping_type(exts), accessor_type())) + { } + + MDSPAN_FUNCTION_REQUIRES( + (MDSPAN_INLINE_FUNCTION constexpr), + mdspan, (data_handle_type p, const mapping_type& m), , + /* requires */ (_MDSPAN_TRAIT(std::is_default_constructible, accessor_type)) + ) : __members(std::move(p), __map_acc_pair_t(m, accessor_type())) + { } + + MDSPAN_INLINE_FUNCTION + constexpr mdspan(data_handle_type p, const mapping_type& m, const accessor_type& a) + : __members(std::move(p), __map_acc_pair_t(m, a)) + { } + + MDSPAN_TEMPLATE_REQUIRES( + class OtherElementType, class OtherExtents, class OtherLayoutPolicy, class OtherAccessor, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, mapping_type, const typename OtherLayoutPolicy::template mapping&) && + _MDSPAN_TRAIT(std::is_constructible, accessor_type, const OtherAccessor&) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT( + !_MDSPAN_TRAIT(std::is_convertible, const typename OtherLayoutPolicy::template mapping&, mapping_type) || + !_MDSPAN_TRAIT(std::is_convertible, const OtherAccessor&, accessor_type) + ) + MDSPAN_INLINE_FUNCTION + constexpr mdspan(const mdspan& other) + : __members(other.__ptr_ref(), __map_acc_pair_t(other.__mapping_ref(), other.__accessor_ref())) + { + static_assert(_MDSPAN_TRAIT(std::is_constructible, data_handle_type, typename OtherAccessor::data_handle_type),"Incompatible data_handle_type for mdspan construction"); + static_assert(_MDSPAN_TRAIT(std::is_constructible, extents_type, OtherExtents),"Incompatible extents for mdspan construction"); + /* + * TODO: Check precondition + * For each rank index r of extents_type, static_extent(r) == dynamic_extent || static_extent(r) == other.extent(r) is true. + */ + } + + /* Might need this on NVIDIA? + MDSPAN_INLINE_FUNCTION_DEFAULTED + ~mdspan() = default; + */ + + MDSPAN_INLINE_FUNCTION_DEFAULTED _MDSPAN_CONSTEXPR_14_DEFAULTED mdspan& operator=(const mdspan&) = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED _MDSPAN_CONSTEXPR_14_DEFAULTED mdspan& operator=(mdspan&&) = default; + + + //-------------------------------------------------------------------------------- + // [mdspan.basic.mapping], mdspan mapping domain multidimensional index to access codomain element + + #if MDSPAN_USE_BRACKET_OPERATOR + MDSPAN_TEMPLATE_REQUIRES( + class... SizeTypes, + /* requires */ ( + _MDSPAN_FOLD_AND(_MDSPAN_TRAIT(std::is_convertible, SizeTypes, index_type) /* && ... */) && + _MDSPAN_FOLD_AND(_MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, SizeTypes) /* && ... */) && + (rank() == sizeof...(SizeTypes)) + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference operator[](SizeTypes... indices) const + { + return __accessor_ref().access(__ptr_ref(), __mapping_ref()(static_cast(std::move(indices))...)); + } + #endif + + MDSPAN_TEMPLATE_REQUIRES( + class SizeType, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, const SizeType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, const SizeType&) + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference operator[](const std::array< SizeType, rank()>& indices) const + { + return __impl::template __callop(*this, indices); + } + + #ifdef __cpp_lib_span + MDSPAN_TEMPLATE_REQUIRES( + class SizeType, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, const SizeType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, const SizeType&) + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference operator[](std::span indices) const + { + return __impl::template __callop(*this, indices); + } + #endif // __cpp_lib_span + + #if !MDSPAN_USE_BRACKET_OPERATOR + MDSPAN_TEMPLATE_REQUIRES( + class Index, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, Index, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, Index) && + extents_type::rank() == 1 + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference operator[](Index idx) const + { + return __accessor_ref().access(__ptr_ref(), __mapping_ref()(static_cast(std::move(idx)))); + } + #endif + + #if MDSPAN_USE_PAREN_OPERATOR + MDSPAN_TEMPLATE_REQUIRES( + class... SizeTypes, + /* requires */ ( + extents_type::rank() == sizeof...(SizeTypes) && + (detail::are_valid_indices()) + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference operator()(SizeTypes... indices) const + { + return __accessor_ref().access(__ptr_ref(), __mapping_ref()(static_cast(std::move(indices))...)); + } + + MDSPAN_TEMPLATE_REQUIRES( + class SizeType, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, const SizeType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, const SizeType&) + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference operator()(const std::array& indices) const + { + return __impl::template __callop(*this, indices); + } + + #ifdef __cpp_lib_span + MDSPAN_TEMPLATE_REQUIRES( + class SizeType, + /* requires */ ( + _MDSPAN_TRAIT(std::is_convertible, const SizeType&, index_type) && + _MDSPAN_TRAIT(std::is_nothrow_constructible, index_type, const SizeType&) + ) + ) + MDSPAN_FORCE_INLINE_FUNCTION + constexpr reference operator()(std::span indices) const + { + return __impl::template __callop(*this, indices); + } + #endif // __cpp_lib_span + #endif // MDSPAN_USE_PAREN_OPERATOR + + MDSPAN_INLINE_FUNCTION constexpr size_type size() const noexcept { + return __impl::__size(*this); + }; + + MDSPAN_INLINE_FUNCTION constexpr bool empty() const noexcept { + return __impl::__empty(*this); + }; + + MDSPAN_INLINE_FUNCTION + friend constexpr void swap(mdspan& x, mdspan& y) noexcept { + // can't call the std::swap inside on HIP + #if !defined(_MDSPAN_HAS_HIP) && !defined(_MDSPAN_HAS_CUDA) + using std::swap; + swap(x.__ptr_ref(), y.__ptr_ref()); + swap(x.__mapping_ref(), y.__mapping_ref()); + swap(x.__accessor_ref(), y.__accessor_ref()); + #else + mdspan tmp = y; + y = x; + x = tmp; + #endif + } + + //-------------------------------------------------------------------------------- + // [mdspan.basic.domobs], mdspan observers of the domain multidimensional index space + + + MDSPAN_INLINE_FUNCTION constexpr const extents_type& extents() const noexcept { return __mapping_ref().extents(); }; + MDSPAN_INLINE_FUNCTION constexpr const data_handle_type& data_handle() const noexcept { return __ptr_ref(); }; + MDSPAN_INLINE_FUNCTION constexpr const mapping_type& mapping() const noexcept { return __mapping_ref(); }; + MDSPAN_INLINE_FUNCTION constexpr const accessor_type& accessor() const noexcept { return __accessor_ref(); }; + + //-------------------------------------------------------------------------------- + // [mdspan.basic.obs], mdspan observers of the mapping + + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_unique() { return mapping_type::is_always_unique(); }; + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_exhaustive() { return mapping_type::is_always_exhaustive(); }; + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_strided() { return mapping_type::is_always_strided(); }; + + MDSPAN_INLINE_FUNCTION constexpr bool is_unique() const { return __mapping_ref().is_unique(); }; + MDSPAN_INLINE_FUNCTION constexpr bool is_exhaustive() const { return __mapping_ref().is_exhaustive(); }; + MDSPAN_INLINE_FUNCTION constexpr bool is_strided() const { return __mapping_ref().is_strided(); }; + MDSPAN_INLINE_FUNCTION constexpr index_type stride(size_t r) const { return __mapping_ref().stride(r); }; + +private: + + detail::__compressed_pair __members{}; + + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 data_handle_type& __ptr_ref() noexcept { return __members.__first(); } + MDSPAN_FORCE_INLINE_FUNCTION constexpr data_handle_type const& __ptr_ref() const noexcept { return __members.__first(); } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 mapping_type& __mapping_ref() noexcept { return __members.__second().__first(); } + MDSPAN_FORCE_INLINE_FUNCTION constexpr mapping_type const& __mapping_ref() const noexcept { return __members.__second().__first(); } + MDSPAN_FORCE_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 accessor_type& __accessor_ref() noexcept { return __members.__second().__second(); } + MDSPAN_FORCE_INLINE_FUNCTION constexpr accessor_type const& __accessor_ref() const noexcept { return __members.__second().__second(); } + + template + friend class mdspan; + +}; + +#if defined(_MDSPAN_USE_CLASS_TEMPLATE_ARGUMENT_DEDUCTION) +MDSPAN_TEMPLATE_REQUIRES( + class ElementType, class... SizeTypes, + /* requires */ _MDSPAN_FOLD_AND(_MDSPAN_TRAIT(std::is_convertible, SizeTypes, size_t) /* && ... */) && + (sizeof...(SizeTypes) > 0) +) +MDSPAN_DEDUCTION_GUIDE explicit mdspan(ElementType*, SizeTypes...) + -> mdspan>; + +MDSPAN_TEMPLATE_REQUIRES( + class Pointer, + (_MDSPAN_TRAIT(std::is_pointer, std::remove_reference_t)) +) +MDSPAN_DEDUCTION_GUIDE mdspan(Pointer&&) -> mdspan>, extents>; + +MDSPAN_TEMPLATE_REQUIRES( + class CArray, + (_MDSPAN_TRAIT(std::is_array, CArray) && (std::rank_v == 1)) +) +MDSPAN_DEDUCTION_GUIDE mdspan(CArray&) -> mdspan, extents>>; + +template +MDSPAN_DEDUCTION_GUIDE mdspan(ElementType*, const ::std::array&) + -> mdspan>; + +#ifdef __cpp_lib_span +template +MDSPAN_DEDUCTION_GUIDE mdspan(ElementType*, ::std::span) + -> mdspan>; +#endif + +// This one is necessary because all the constructors take `data_handle_type`s, not +// `ElementType*`s, and `data_handle_type` is taken from `accessor_type::data_handle_type`, which +// seems to throw off automatic deduction guides. +template +MDSPAN_DEDUCTION_GUIDE mdspan(ElementType*, const extents&) + -> mdspan>; + +template +MDSPAN_DEDUCTION_GUIDE mdspan(ElementType*, const MappingType&) + -> mdspan; + +template +MDSPAN_DEDUCTION_GUIDE mdspan(const typename AccessorType::data_handle_type, const MappingType&, const AccessorType&) + -> mdspan; +#endif + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/mdspan.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/layout_left.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +#if MDSPAN_HAS_CXX_17 +#endif +#include + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +//============================================================================== + +template +class layout_left::mapping { + public: + using extents_type = Extents; + using index_type = typename extents_type::index_type; + using size_type = typename extents_type::size_type; + using rank_type = typename extents_type::rank_type; + using layout_type = layout_left; + private: + + static_assert(detail::__is_extents_v, + MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::layout_left::mapping must be instantiated with a specialization of " MDSPAN_IMPL_STANDARD_NAMESPACE_STRING "::extents."); + + template + friend class mapping; + + // i0+(i1 + E(1)*(i2 + E(2)*i3)) + template + struct __rank_count {}; + + template + _MDSPAN_HOST_DEVICE + constexpr index_type __compute_offset( + __rank_count, const I& i, Indices... idx) const { + return __compute_offset(__rank_count(), idx...) * + __extents.extent(r) + i; + } + + template + _MDSPAN_HOST_DEVICE + constexpr index_type __compute_offset( + __rank_count, const I& i) const { + return i; + } + + _MDSPAN_HOST_DEVICE + constexpr index_type __compute_offset(__rank_count<0,0>) const { return 0; } + + public: + + //-------------------------------------------------------------------------------- + + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping() noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping(mapping const&) noexcept = default; + + _MDSPAN_HOST_DEVICE + constexpr mapping(extents_type const& __exts) noexcept + :__extents(__exts) + { } + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, extents_type, OtherExtents) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT((!std::is_convertible::value)) // needs two () due to comma + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 + mapping(mapping const& other) noexcept // NOLINT(google-explicit-constructor) + :__extents(other.extents()) + { + /* + * TODO: check precondition + * other.required_span_size() is a representable value of type index_type + */ + } + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, extents_type, OtherExtents) && + (extents_type::rank() <= 1) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT((!std::is_convertible::value)) // needs two () due to comma + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 + mapping(layout_right::mapping const& other) noexcept // NOLINT(google-explicit-constructor) + :__extents(other.extents()) + { + /* + * TODO: check precondition + * other.required_span_size() is a representable value of type index_type + */ + } + +#if MDSPAN_HAS_CXX_17 + /** + * Converting constructor from `layout_left_padded::mapping`. + * + * This overload participates in overload resolution only if _Mapping is a layout_left_padded mapping and + * extents_type is constructible from _Mapping::extents_type. + * + * \note There is currently a difference from p2642r2, where this function is specified as taking + * `layout_left_padded< padding_value >::mapping< Extents>`. However, this makes `padding_value` non-deducible. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ ( + MDSPAN_IMPL_PROPOSED_NAMESPACE::detail::is_layout_left_padded_mapping<_Mapping>::value + && std::is_constructible_v + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT((!std::is_convertible_v)) + mapping(const _Mapping& __other) noexcept + : __extents(__other.extents()) + { + MDSPAN_IMPL_PROPOSED_NAMESPACE::detail:: + check_padded_layout_converting_constructor_mandates< + extents_type, _Mapping>(detail::with_rank{}); + MDSPAN_IMPL_PROPOSED_NAMESPACE::detail:: + check_padded_layout_converting_constructor_preconditions< + extents_type>(detail::with_rank{}, __other); + } +#endif + + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( + _MDSPAN_TRAIT(std::is_constructible, extents_type, OtherExtents) + ) + ) + MDSPAN_CONDITIONAL_EXPLICIT((extents_type::rank() > 0)) + MDSPAN_INLINE_FUNCTION _MDSPAN_CONSTEXPR_14 + mapping(layout_stride::mapping const& other) noexcept // NOLINT(google-explicit-constructor) + :__extents(other.extents()) + { + /* + * TODO: check precondition + * other.required_span_size() is a representable value of type index_type + */ + detail::validate_strides(detail::with_rank{}, layout_left{}, __extents, other); + } + + MDSPAN_INLINE_FUNCTION_DEFAULTED _MDSPAN_CONSTEXPR_14_DEFAULTED mapping& operator=(mapping const&) noexcept = default; + + MDSPAN_INLINE_FUNCTION + constexpr const extents_type& extents() const noexcept { + return __extents; + } + + MDSPAN_INLINE_FUNCTION + constexpr index_type required_span_size() const noexcept { + index_type value = 1; + for(rank_type r=0; r()) + ) + ) + _MDSPAN_HOST_DEVICE + constexpr index_type operator()(Indices... idxs) const noexcept { +#if ! defined(NDEBUG) + detail::check_all_indices(this->extents(), idxs...); +#endif // ! NDEBUG + return __compute_offset(__rank_count<0, extents_type::rank()>(), static_cast(idxs)...); + } + + + + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_unique() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_exhaustive() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_strided() noexcept { return true; } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_unique() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_exhaustive() noexcept { return true; } + MDSPAN_INLINE_FUNCTION static constexpr bool is_strided() noexcept { return true; } + + MDSPAN_INLINE_FUNCTION + constexpr index_type stride(rank_type i) const noexcept +#if MDSPAN_HAS_CXX_20 + requires ( Extents::rank() > 0 ) +#endif + { + index_type value = 1; + for(rank_type r=0; r const& rhs) noexcept { + return lhs.extents() == rhs.extents(); + } + + // In C++ 20 the not equal exists if equal is found +#if !(MDSPAN_HAS_CXX_20) + MDSPAN_TEMPLATE_REQUIRES( + class OtherExtents, + /* requires */ ( Extents::rank() == OtherExtents::rank()) + ) + MDSPAN_INLINE_FUNCTION + friend constexpr bool operator!=(mapping const& lhs, mapping const& rhs) noexcept { + return lhs.extents() != rhs.extents(); + } +#endif + + // Not really public, but currently needed to implement fully constexpr useable submdspan: + template + constexpr index_type __get_stride(MDSPAN_IMPL_STANDARD_NAMESPACE::extents,std::integer_sequence) const { + return _MDSPAN_FOLD_TIMES_RIGHT((Idx():1),1); + } + template + constexpr index_type __stride() const noexcept { + return __get_stride(__extents, std::make_index_sequence()); + } + +private: + _MDSPAN_NO_UNIQUE_ADDRESS extents_type __extents{}; + + // [mdspan.submdspan.mapping], submdspan mapping specialization + template + MDSPAN_INLINE_FUNCTION + constexpr auto submdspan_mapping_impl( + SliceSpecifiers... slices) const; + + template + friend constexpr auto submdspan_mapping( + const mapping& src, SliceSpecifiers... slices) { + return src.submdspan_mapping_impl(slices...); + } +}; + + +} // end namespace MDSPAN_IMPL_STANDARD_NAMESPACE + +//END_FILE_INCLUDE: mdspan/include/experimental/__p0009_bits/layout_left.hpp +#if MDSPAN_HAS_CXX_17 +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p2642_bits/layout_padded.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + +#include + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace MDSPAN_IMPL_PROPOSED_NAMESPACE { + +namespace detail { +template +MDSPAN_INLINE_FUNCTION +constexpr _T +find_next_multiple(_T alignment, _T offset) +{ + if ( alignment == 0 ) { + return _T(0); + } else { + return ( ( offset + alignment - 1 ) / alignment) * alignment; + } +} + +template +MDSPAN_INLINE_FUNCTION constexpr size_t get_actual_static_padding_value() { + constexpr auto rank = _ExtentsType::rank(); + + if constexpr (rank <= typename _ExtentsType::rank_type(1)) { + return 0; + } else if constexpr (_PaddingValue != dynamic_extent && + _ExtentsType::static_extent(_ExtentToPadIdx) != + dynamic_extent) { + static_assert( + (_PaddingValue != 0) || + (_ExtentsType::static_extent(_ExtentToPadIdx) == 0), + "padding stride can be 0 only if " + "extents_type::static_extent(extent-to-pad) is 0 or dynamic_extent"); + return find_next_multiple(_PaddingValue, + _ExtentsType::static_extent(_ExtentToPadIdx)); + } else { + return dynamic_extent; + } + // Missing return statement warning from NVCC +#ifdef __NVCC__ + return 0; +#endif +} + +template +struct static_array_type_for_padded_extent +{ + static constexpr size_t padding_value = _PaddingValue; + using index_type = typename _Extents::index_type; + using extents_type = _Extents; + using type = ::MDSPAN_IMPL_STANDARD_NAMESPACE::detail::maybe_static_array< + index_type, size_t, dynamic_extent, + detail::get_actual_static_padding_value()>; +}; + +template +struct static_array_type_for_padded_extent<_PaddingValue, _Extents, + _ExtentToPadIdx, Rank, std::enable_if_t> { + using index_type = typename _Extents::index_type; + using extents_type = _Extents; + using type = + ::MDSPAN_IMPL_STANDARD_NAMESPACE::detail::maybe_static_array< + index_type, size_t, dynamic_extent, 0>; +}; + +template +struct padded_extent { + static constexpr size_t padding_value = _PaddingValue; + using index_type = typename _Extents::index_type; + using extents_type = _Extents; + using static_array_type = typename static_array_type_for_padded_extent< + padding_value, _Extents, _ExtentToPadIdx, _Extents::rank()>::type; + + static constexpr auto static_value() { return static_array_type::static_value(0); } + + MDSPAN_INLINE_FUNCTION + static constexpr static_array_type + init_padding(const _Extents &exts) { + if constexpr ((_Extents::rank() > 1) && (padding_value == dynamic_extent)) { + return {exts.extent(_ExtentToPadIdx)}; + } else { + return init_padding(exts, padding_value); + } + // Missing return statement warning from NVCC +#ifdef __NVCC__ + return {}; +#endif + } + + MDSPAN_INLINE_FUNCTION static constexpr static_array_type + init_padding([[maybe_unused]] const _Extents &exts, + [[maybe_unused]] index_type pv) { + if constexpr (_Extents::rank() > 1) { + return {find_next_multiple(pv, + exts.extent(_ExtentToPadIdx))}; + } else { + return {}; + } + // Missing return statement warning from NVCC +#ifdef __NVCC__ + return {}; +#endif + } + + template + MDSPAN_INLINE_FUNCTION static constexpr static_array_type + init_padding([[maybe_unused]] const _Mapping &other_mapping, + std::integral_constant) { + if constexpr (_Extents::rank() > 1) { + return {other_mapping.stride(_PaddingStrideIdx)}; + } else { + return {}; + } + // Missing return statement warning from NVCC +#ifdef __NVCC__ + return {}; +#endif + } +}; +} // namespace detail + +template +template +class layout_left_padded::mapping { +public: + static constexpr size_t padding_value = PaddingValue; + + using extents_type = Extents; + using index_type = typename extents_type::index_type; + using size_type = typename extents_type::size_type; + using rank_type = typename extents_type::rank_type; + using layout_type = layout_left_padded; + +#ifndef MDSPAN_INTERNAL_TEST +private: +#endif // MDSPAN_INTERNAL_TEST + + static constexpr rank_type padded_stride_idx = detail::layout_padded_constants::padded_stride_idx; + static constexpr rank_type extent_to_pad_idx = detail::layout_padded_constants::extent_to_pad_idx; + + static_assert((padding_value != 0) + || (extents_type::static_extent(extent_to_pad_idx) == 0) + || (extents_type::static_extent(extent_to_pad_idx) == dynamic_extent), + "out of bounds access for rank 0"); + + using padded_stride_type = detail::padded_extent< padding_value, extents_type, extent_to_pad_idx >; + + static constexpr size_t static_padding_stride = padded_stride_type::static_value(); + + typename padded_stride_type::static_array_type padded_stride = {}; + extents_type exts = {}; + + MDSPAN_INLINE_FUNCTION constexpr index_type + compute_offset(std::index_sequence<>) const { + return 0; + } + + template + MDSPAN_INLINE_FUNCTION constexpr index_type + compute_offset(std::index_sequence, IndexOffset index_offset) const { + return index_offset; + } + + template + MDSPAN_INLINE_FUNCTION constexpr index_type + compute_offset(std::index_sequence, + IndexOffsets... index_offsets) const { + index_type indices[] = {static_cast(index_offsets)...}; + // self-recursive fold trick from + // https://github.com/llvm/llvm-project/blob/96e1914aa2e6d8966acbfbe2f4d184201f1aa318/libcxx/include/mdspan/layout_left.h#L144 + index_type res = 0; + ((res = indices[extents_type::rank() - 1 - Ranks] + + ((extents_type::rank() - 1 - Ranks) == extent_to_pad_idx + ? padded_stride.value(0) + : exts.extent(extents_type::rank() - 1 - Ranks)) * + res), + ...); + return res; + } + +public: +#if !MDSPAN_HAS_CXX_20 + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr mapping() + : mapping(extents_type{}) + {} +#else + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr mapping() + requires(static_padding_stride != dynamic_extent) = default; + + MDSPAN_INLINE_FUNCTION + constexpr mapping() + requires(static_padding_stride == dynamic_extent) + : mapping(extents_type{}) + {} +#endif + + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping(const mapping&) noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED mapping& operator=(const mapping&) noexcept = default; + + /** + * Initializes the mapping with the given extents. + * + * \param ext the given extents + */ + MDSPAN_INLINE_FUNCTION + constexpr mapping(const extents_type& ext) + : padded_stride(padded_stride_type::init_padding(ext)), exts(ext) + {} + + /** + * Initializes the mapping with the given extents and the specified padding value. + * + * This overload participates in overload resolution only if `is_convertible_v` + * is `true` and `is_nothrow_constructible_v` is `true` + * + * \param ext the given extents + * \param padding_value the padding value + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Size, + /* requires */ ( + std::is_convertible_v<_Size, index_type> + && std::is_nothrow_constructible_v + ) + ) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const extents_type &ext, _Size dynamic_padding_value) + : padded_stride(padded_stride_type::init_padding(ext, dynamic_padding_value)), exts(ext) + { + assert((padding_value == dynamic_extent) || (static_cast(padding_value) == static_cast(dynamic_padding_value))); + } + + /** + * Converting constructor from `layout_left::mapping`. + * + * This overload participates in overload resolution only if + * `is_constructible_v` is true. If + * `OtherExtents::rank() > 1` then one of `padding_value`, `static_extent(0)`, + * or `OtherExtents::static_extent(0)` must be `dynamic_extent`; otherwise, + * `OtherExtents::static_extent(0)` must be equal to the least multiple of + * `padding_value` greater than or equal to `extents_type::static_extent(0)` + */ + MDSPAN_TEMPLATE_REQUIRES( + class _OtherExtents, + /* requires */ (std::is_constructible_v)) + MDSPAN_CONDITIONAL_EXPLICIT( + (!std::is_convertible_v<_OtherExtents, extents_type>)) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const layout_left::mapping<_OtherExtents> &other_mapping) + : padded_stride(padded_stride_type::init_padding( + other_mapping, + std::integral_constant{})), + exts(other_mapping.extents()) { + static_assert( + (_OtherExtents::rank() > 1) || + (static_padding_stride != dynamic_extent) || + (_OtherExtents::static_extent(extent_to_pad_idx) != dynamic_extent) || + (static_padding_stride == + _OtherExtents::static_extent(extent_to_pad_idx))); + } + + /** + * Converting constructor from `layout_stride::mapping`. + * + * This overload participates in overload resolution only if + * `is_constructible_v` is true + */ + MDSPAN_TEMPLATE_REQUIRES( + class _OtherExtents, + /* requires */ (std::is_constructible_v)) + MDSPAN_CONDITIONAL_EXPLICIT((extents_type::rank() > 0)) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const layout_stride::mapping<_OtherExtents> &other_mapping) + : padded_stride(padded_stride_type::init_padding( + other_mapping, + std::integral_constant{})), + exts(other_mapping.extents()) {} + + /** + * Converting constructor from `layout_left_padded::mapping`. + * + * This overload participates in overload resolution only if + * `is_constructible_v` is true. Either + * `padding_value` or `OtherPaddingStride` must be `std::dynamic_extent`, or + * `padding_value == OtherPaddingStride`. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_left_padded_mapping<_Mapping>::value + &&std::is_constructible_v< + extents_type, typename _Mapping::extents_type>)) + MDSPAN_CONDITIONAL_EXPLICIT((extents_type::rank() > 1 && + (padding_value == dynamic_extent || + _Mapping::padding_value == dynamic_extent))) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const _Mapping &other_mapping) + : padded_stride(padded_stride_type::init_padding( + other_mapping, + std::integral_constant{})), + exts(other_mapping.extents()) { + static_assert(padding_value == dynamic_extent || + _Mapping::padding_value == dynamic_extent || + padding_value == _Mapping::padding_value); + } + + /** + * Converting constructor from `layout_right_padded::mapping`. + * + * This overload participates in overload resolution only if + * `extents_type::rank()` is 0 or 1 and `is_constructible_v` is `true`. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_right_padded_mapping<_Mapping>::value + &&extents_type::rank() <= 1 && + std::is_constructible_v)) + MDSPAN_CONDITIONAL_EXPLICIT( + (!std::is_convertible_v)) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const _Mapping &other_mapping) noexcept + : padded_stride(padded_stride_type::init_padding( + other_mapping.extents(), + other_mapping.extents().extent(extent_to_pad_idx))), + exts(other_mapping.extents()) {} + + MDSPAN_INLINE_FUNCTION constexpr const extents_type & + extents() const noexcept { + return exts; + } + + MDSPAN_INLINE_FUNCTION constexpr std::array + strides() const noexcept { + if constexpr (extents_type::rank() == 0) { + return {}; + } else if constexpr (extents_type::rank() == 1) { + return {1}; + } else { + index_type value = 1; + std::array s{}; + s[extent_to_pad_idx] = value; + value *= padded_stride.value(0); + for (rank_type r = extent_to_pad_idx + 1; r < extents_type::rank() - 1; + ++r) { + s[r] = value; + value *= exts.extent(r); + } + s[extents_type::rank() - 1] = value; + return s; + } + } + + MDSPAN_INLINE_FUNCTION constexpr index_type + required_span_size() const noexcept { + if constexpr (extents_type::rank() == 0) { + return 1; + } else if constexpr (extents_type::rank() == 1) { + return exts.extent(0); + } else { + index_type value = padded_stride.value(0); + for (rank_type r = 1; r < extents_type::rank(); ++r) { + value *= exts.extent(r); + } + return value; + } + } + + /** + * Return the mapping given the provided indices per rank. + * + * This overload participates in overload resolution only if: + * - `sizeof...(Indices) == extents_type::rank()`, + * - `(is_convertible_v && ...) is true`, and + * - (is_nothrow_constructible_v && ...) is true. + */ + MDSPAN_TEMPLATE_REQUIRES( + class... _Indices, + /* requires */ (sizeof...(_Indices) == extents_type::rank() && + (::MDSPAN_IMPL_STANDARD_NAMESPACE::detail:: + are_valid_indices()))) + MDSPAN_INLINE_FUNCTION constexpr size_t + operator()(_Indices... idxs) const noexcept { +#if !defined(NDEBUG) + ::MDSPAN_IMPL_STANDARD_NAMESPACE::detail::check_all_indices(this->extents(), + idxs...); +#endif // ! NDEBUG + return compute_offset(std::index_sequence_for<_Indices...>{}, idxs...); + } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_unique() noexcept { + return true; + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_exhaustive() noexcept { + return (extents_type::rank() <= rank_type(1)) || + (extents_type::static_extent(extent_to_pad_idx) != dynamic_extent && + extents_type::static_extent(extent_to_pad_idx) == + padded_stride_type::static_value()); + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_strided() noexcept { + return true; + } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_unique() noexcept { + return true; + } + MDSPAN_INLINE_FUNCTION constexpr bool is_exhaustive() const noexcept { + return (extents_type::rank() < 2) || + (exts.extent(extent_to_pad_idx) == padded_stride.value(0)); + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_strided() noexcept { + return true; + } + + MDSPAN_INLINE_FUNCTION + constexpr index_type stride(rank_type r) const noexcept { + assert(r < extents_type::rank()); + if (r == 0) + return index_type(1); + + index_type value = padded_stride.value(0); + for (rank_type k = 1; k < r; k++) + value *= exts.extent(k); + + return value; + } + + /** + * Equality operator between `layout_left_padded`s + * + * This overload only participates in overload resolution if + * `OtherExtents::rank() == extents_type::rank()`. + * + * \note There is currently a difference from p2642r2, where this function is + * specified as taking `layout_left_padded< padding_value >::mapping< + * Extents>`. However, this makes `padding_value` non-deducible. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_left_padded_mapping<_Mapping>::value && + (_Mapping::extents_type::rank() == extents_type::rank()))) + MDSPAN_INLINE_FUNCTION friend constexpr bool + operator==(const mapping &left, const _Mapping &right) noexcept { + // Workaround for some compilers not short-circuiting properly with + // compile-time checks i.e. we can't access stride(_padding_stride_idx) of a + // rank 0 mapping + bool strides_equal = true; + if constexpr (extents_type::rank() > rank_type(1)) { + strides_equal = + left.stride(padded_stride_idx) == right.stride(padded_stride_idx); + } + return (left.extents() == right.extents()) && strides_equal; + } + +#if !MDSPAN_HAS_CXX_20 + /** + * Inequality operator between `layout_left_padded`s + * + * This overload only participates in overload resolution if + * `OtherExtents::rank() == extents_type::rank()`. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_left_padded_mapping<_Mapping>::value && + (_Mapping::extents_type::rank() == extents_type::rank()))) + MDSPAN_INLINE_FUNCTION friend constexpr bool + operator!=(const mapping &left, const _Mapping &right) noexcept { + return !(left == right); + } +#endif +}; + +template +template +class layout_right_padded::mapping { +public: + static constexpr size_t padding_value = PaddingValue; + + using extents_type = Extents; + using index_type = typename extents_type::index_type; + using size_type = typename extents_type::size_type; + using rank_type = typename extents_type::rank_type; + using layout_type = layout_right_padded; + +#ifndef MDSPAN_INTERNAL_TEST + private: +#endif // MDSPAN_INTERNAL_TEST + + static constexpr rank_type padded_stride_idx = detail::layout_padded_constants::padded_stride_idx; + static constexpr rank_type extent_to_pad_idx = detail::layout_padded_constants::extent_to_pad_idx; + + static_assert((padding_value != 0) + || (extents_type::static_extent(extent_to_pad_idx) == 0) + || (extents_type::static_extent(extent_to_pad_idx) == dynamic_extent), + "if padding stride is 0, static_extent(extent-to-pad-rank) must also be 0 or dynamic_extent"); + + using padded_stride_type = detail::padded_extent< padding_value, extents_type, extent_to_pad_idx >; + static constexpr size_t static_padding_stride = padded_stride_type::static_value(); + + typename padded_stride_type::static_array_type padded_stride = {}; + extents_type exts = {}; + + MDSPAN_INLINE_FUNCTION constexpr index_type + compute_offset(std::index_sequence<>) const { + return 0; + } + + template + MDSPAN_INLINE_FUNCTION constexpr index_type + compute_offset(std::index_sequence, IndexOffset index_offset) const { + return index_offset; + } + + template + MDSPAN_INLINE_FUNCTION constexpr index_type + compute_offset(std::index_sequence, + IndexOffsets... index_offsets) const { + // self-recursive fold trick from + // https://github.com/llvm/llvm-project/blob/4d9771741d40cc9cfcccb6b033f43689d36b705a/libcxx/include/mdspan/layout_right.h#L141 + index_type res = 0; + ((res = static_cast(index_offsets) + + (Ranks == extent_to_pad_idx ? padded_stride.value(0) + : exts.extent(Ranks)) * + res), + ...); + return res; + } + +public: +#if !MDSPAN_HAS_CXX_20 + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr mapping() + : mapping(extents_type{}) + {} +#else + MDSPAN_INLINE_FUNCTION_DEFAULTED + constexpr mapping() + requires(static_padding_stride != dynamic_extent) = default; + + MDSPAN_INLINE_FUNCTION + constexpr mapping() + requires(static_padding_stride == dynamic_extent) + : mapping(extents_type{}) + {} +#endif + + MDSPAN_INLINE_FUNCTION_DEFAULTED constexpr mapping(const mapping&) noexcept = default; + MDSPAN_INLINE_FUNCTION_DEFAULTED mapping& operator=(const mapping&) noexcept = default; + + /** + * Initializes the mapping with the given extents. + * + * \param ext the given extents + */ + MDSPAN_INLINE_FUNCTION + constexpr mapping(const extents_type &ext) + : padded_stride(padded_stride_type::init_padding(ext)), exts(ext) {} + + /** + * Initializes the mapping with the given extents and the specified padding value. + * + * This overload participates in overload resolution only if `is_convertible_v` + * is `true` and `is_nothrow_constructible_v` is `true` + * + * \param ext the given extents + * \param padding_value the padding value + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Size, + /* requires */ ( + std::is_convertible_v<_Size, index_type> + && std::is_nothrow_constructible_v + ) + ) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const extents_type &ext, _Size dynamic_padding_value) + : padded_stride(padded_stride_type::init_padding(ext, static_cast(dynamic_padding_value))), + exts(ext) { + assert((padding_value == dynamic_extent) || + (static_cast(padding_value) == static_cast(dynamic_padding_value))); + } + + /** + * Converting constructor from `layout_right::mapping`. + * + * This overload participates in overload resolution only if `is_constructible_v` is true. + * If `OtherExtents::rank() > 1` then one of `padding_value`, `static_extent(0)`, or `OtherExtents::static_extent(0)` must be `dynamic_extent`; + * otherwise, `OtherExtents::static_extent(0)` must be equal to the least multiple of `padding_value` greater than or equal to `extents_type::static_extent(0)` + */ + MDSPAN_TEMPLATE_REQUIRES( + class _OtherExtents, + /* requires */ (std::is_constructible_v)) + MDSPAN_CONDITIONAL_EXPLICIT( + (!std::is_convertible_v<_OtherExtents, extents_type>)) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const layout_right::mapping<_OtherExtents> &other_mapping) + : padded_stride(padded_stride_type::init_padding( + other_mapping, + std::integral_constant{})), + exts(other_mapping.extents()) { + static_assert( + (_OtherExtents::rank() > 1) || + (padded_stride_type::static_value() != dynamic_extent) || + (_OtherExtents::static_extent(extent_to_pad_idx) != dynamic_extent) || + (padded_stride_type::static_value() == + _OtherExtents::static_extent(extent_to_pad_idx))); + } + + /** + * Converting constructor from `layout_stride::mapping`. + * + * This overload participates in overload resolution only if + * `is_constructible_v` is true + */ + MDSPAN_TEMPLATE_REQUIRES( + class _OtherExtents, + /* requires */ (std::is_constructible_v)) + MDSPAN_CONDITIONAL_EXPLICIT((extents_type::rank() > 0)) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const layout_stride::mapping<_OtherExtents> &other_mapping) + : padded_stride(padded_stride_type::init_padding( + other_mapping, + std::integral_constant{})), + exts(other_mapping.extents()) {} + + /** + * Converting constructor from `layout_right_padded::mapping`. + * + * This overload participates in overload resolution only if + * `is_constructible_v` is true. Either + * `padding_value` or `OtherPaddingStride` must be `std::dynamic_extent`, or + * `padding_value == OtherPaddingStride`. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_right_padded_mapping<_Mapping>::value + &&std::is_constructible_v< + extents_type, typename _Mapping::extents_type>)) + MDSPAN_CONDITIONAL_EXPLICIT((extents_type::rank() > 1 && + (padding_value == dynamic_extent || + _Mapping::padding_value == dynamic_extent))) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const _Mapping &other_mapping) + : padded_stride(padded_stride_type::init_padding( + other_mapping, + std::integral_constant{})), + exts(other_mapping.extents()) { + static_assert(padding_value == dynamic_extent || + _Mapping::padding_value == dynamic_extent || + padding_value == _Mapping::padding_value); + } + + /** + * Converting constructor from `layout_left_padded::mapping`. + * + * This overload participates in overload resolution only if + * `extents_type::rank()` is 0 or 1 and `is_constructible_v` is `true`. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_left_padded_mapping<_Mapping>::value + &&extents_type::rank() <= 1 && + std::is_constructible_v)) + MDSPAN_CONDITIONAL_EXPLICIT( + (!std::is_convertible_v)) + MDSPAN_INLINE_FUNCTION + constexpr mapping(const _Mapping &other_mapping) noexcept + : padded_stride(padded_stride_type::init_padding( + other_mapping.extents(), + other_mapping.extents().extent(extent_to_pad_idx))), + exts(other_mapping.extents()) {} + + MDSPAN_INLINE_FUNCTION constexpr const extents_type & + extents() const noexcept { + return exts; + } + + MDSPAN_INLINE_FUNCTION constexpr std::array + strides() const noexcept { + if constexpr (extents_type::rank() == 0) { + return {}; + } else if constexpr (extents_type::rank() == 1) { + return {1}; + } else { + index_type value = 1; + std::array s{}; + s[extent_to_pad_idx] = value; + value *= padded_stride.value(0); + for (rank_type r = extent_to_pad_idx - 1; r > 0; --r) { + s[r] = value; + value *= exts.extent(r); + } + s[0] = value; + return s; + } + } + + MDSPAN_INLINE_FUNCTION constexpr index_type + required_span_size() const noexcept { + if constexpr (extents_type::rank() == 0) { + return 1; + } else if constexpr (extents_type::rank() == 1) { + return exts.extent(0); + } else { + index_type value = 1; + for (rank_type r = 0; r < extent_to_pad_idx; ++r) { + value *= exts.extent(r); + } + return value * padded_stride.value(0); + } + } + + /** + * Return the mapping given the provided indices per rank. + * + * This overload participates in overload resolution only if: + * - `sizeof...(Indices) == extents_type::rank()`, + * - `(is_convertible_v && ...) is true`, and + * - (is_nothrow_constructible_v && ...) is true. + */ + MDSPAN_TEMPLATE_REQUIRES( + class... _Indices, + /* requires */ (sizeof...(_Indices) == extents_type::rank() && + (::MDSPAN_IMPL_STANDARD_NAMESPACE::detail:: + are_valid_indices()))) + MDSPAN_INLINE_FUNCTION constexpr size_t + operator()(_Indices... idxs) const noexcept { + return compute_offset(std::index_sequence_for<_Indices...>{}, idxs...); + } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_unique() noexcept { + return true; + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_exhaustive() noexcept { + return (extents_type::rank() <= rank_type(1)) || + (extents_type::static_extent(extent_to_pad_idx) != dynamic_extent && + extents_type::static_extent(extent_to_pad_idx) == + padded_stride_type::static_value()); + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_always_strided() noexcept { + return true; + } + + MDSPAN_INLINE_FUNCTION static constexpr bool is_unique() noexcept { + return true; + } + MDSPAN_INLINE_FUNCTION constexpr bool is_exhaustive() const noexcept { + return (extents_type::rank() < 2) || + (exts.extent(extent_to_pad_idx) == padded_stride.value(0)); + } + MDSPAN_INLINE_FUNCTION static constexpr bool is_strided() noexcept { + return true; + } + + MDSPAN_INLINE_FUNCTION constexpr index_type + stride(rank_type r) const noexcept { + assert(r < extents_type::rank()); + if (r == extents_type::rank() - 1) + return index_type(1); + + index_type value = padded_stride.value(0); + for (rank_type k = extents_type::rank() - 2; k > r; k--) + value *= exts.extent(k); + + return value; + } + + /** + * Equality operator between `layout_right_padded`s + * + * This overload only participates in overload resolution if + * `OtherExtents::rank() == extents_type::rank()`. + * + * \note There is currently a difference from p2642r2, where this function is + * specified as taking `layout_right_padded< padding_value >::mapping< + * Extents>`. However, this makes `padding_value` non-deducible. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_right_padded_mapping<_Mapping>::value && + (_Mapping::extents_type::rank() == extents_type::rank()))) + MDSPAN_INLINE_FUNCTION friend constexpr bool + operator==(const mapping &left, const _Mapping &right) noexcept { + // Workaround for some compilers not short-circuiting properly with + // compile-time checks i.e. we can't access stride(_padding_stride_idx) of a + // rank 0 mapping + bool strides_equal = true; + if constexpr (extents_type::rank() > rank_type(1)) { + strides_equal = + left.stride(padded_stride_idx) == right.stride(padded_stride_idx); + } + return (left.extents() == right.extents()) && strides_equal; + } + +#if !MDSPAN_HAS_CXX_20 + /** + * Inequality operator between `layout_right_padded`s + * + * This overload only participates in overload resolution if + * `OtherExtents::rank() == extents_type::rank()`. + */ + MDSPAN_TEMPLATE_REQUIRES( + class _Mapping, + /* requires */ (detail::is_layout_right_padded_mapping<_Mapping>::value && + (_Mapping::extents_type::rank() == extents_type::rank()))) + MDSPAN_INLINE_FUNCTION friend constexpr bool + operator!=(const mapping &left, const _Mapping &right) noexcept { + return !(left == right); + } +#endif +}; +} +} +//END_FILE_INCLUDE: mdspan/include/experimental/__p2642_bits/layout_padded.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/submdspan.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/submdspan_extents.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +#include + +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/strided_slice.hpp + +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +#include + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { + +namespace { + template + struct __mdspan_is_integral_constant: std::false_type {}; + + template + struct __mdspan_is_integral_constant>: std::true_type {}; +} + +// Slice Specifier allowing for strides and compile time extent +template +struct strided_slice { + using offset_type = OffsetType; + using extent_type = ExtentType; + using stride_type = StrideType; + + _MDSPAN_NO_UNIQUE_ADDRESS OffsetType offset{}; + _MDSPAN_NO_UNIQUE_ADDRESS ExtentType extent{}; + _MDSPAN_NO_UNIQUE_ADDRESS StrideType stride{}; + + static_assert(std::is_integral_v || __mdspan_is_integral_constant::value); + static_assert(std::is_integral_v || __mdspan_is_integral_constant::value); + static_assert(std::is_integral_v || __mdspan_is_integral_constant::value); +}; + +} // MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/strided_slice.hpp +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace detail { + +// Mapping from submapping ranks to srcmapping ranks +// InvMapRank is an index_sequence, which we build recursively +// to contain the mapped indices. +// end of recursion specialization containing the final index_sequence +template +MDSPAN_INLINE_FUNCTION +constexpr auto inv_map_rank(std::integral_constant, std::index_sequence) { + return std::index_sequence(); +} + +// specialization reducing rank by one (i.e., integral slice specifier) +template +MDSPAN_INLINE_FUNCTION +constexpr auto inv_map_rank(std::integral_constant, std::index_sequence, Slice, + SliceSpecifiers... slices) { + using next_idx_seq_t = std::conditional_t, + std::index_sequence, + std::index_sequence>; + + return inv_map_rank(std::integral_constant(), next_idx_seq_t(), + slices...); +} + +// Helper for identifying strided_slice +template struct is_strided_slice : std::false_type {}; + +template +struct is_strided_slice< + strided_slice> : std::true_type {}; + +// first_of(slice): getting begin of slice specifier range +MDSPAN_TEMPLATE_REQUIRES( + class Integral, + /* requires */(std::is_convertible_v) +) +MDSPAN_INLINE_FUNCTION +constexpr Integral first_of(const Integral &i) { + return i; +} + +MDSPAN_INLINE_FUNCTION +constexpr std::integral_constant +first_of(const ::MDSPAN_IMPL_STANDARD_NAMESPACE::full_extent_t &) { + return std::integral_constant(); +} + +MDSPAN_TEMPLATE_REQUIRES( + class Slice, + /* requires */(std::is_convertible_v>) +) +MDSPAN_INLINE_FUNCTION +constexpr auto first_of(const Slice &i) { + return std::get<0>(i); +} + +template +MDSPAN_INLINE_FUNCTION +constexpr OffsetType +first_of(const strided_slice &r) { + return r.offset; +} + +// last_of(slice): getting end of slice specifier range +// We need however not just the slice but also the extents +// of the original view and which rank from the extents. +// This is needed in the case of slice being full_extent_t. +MDSPAN_TEMPLATE_REQUIRES( + size_t k, class Extents, class Integral, + /* requires */(std::is_convertible_v) +) +MDSPAN_INLINE_FUNCTION +constexpr Integral + last_of(std::integral_constant, const Extents &, const Integral &i) { + return i; +} + +MDSPAN_TEMPLATE_REQUIRES( + size_t k, class Extents, class Slice, + /* requires */(std::is_convertible_v>) +) +MDSPAN_INLINE_FUNCTION +constexpr auto last_of(std::integral_constant, const Extents &, + const Slice &i) { + return std::get<1>(i); +} + +// Suppress spurious warning with NVCC about no return statement. +// This is a known issue in NVCC and NVC++ +// Depending on the CUDA and GCC version we need both the builtin +// and the diagnostic push. I tried really hard to find something shorter +// but no luck ... +#if defined __NVCC__ + #ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ + #pragma nv_diagnostic push + #pragma nv_diag_suppress = implicit_return_from_non_void_function + #else + #ifdef __CUDA_ARCH__ + #pragma diagnostic push + #pragma diag_suppress implicit_return_from_non_void_function + #endif + #endif +#elif defined __NVCOMPILER + #pragma diagnostic push + #pragma diag_suppress = implicit_return_from_non_void_function +#endif +template +MDSPAN_INLINE_FUNCTION +constexpr auto last_of(std::integral_constant, const Extents &ext, + ::MDSPAN_IMPL_STANDARD_NAMESPACE::full_extent_t) { + if constexpr (Extents::static_extent(k) == dynamic_extent) { + return ext.extent(k); + } else { + return std::integral_constant(); + } +#if defined(__NVCC__) && !defined(__CUDA_ARCH__) && defined(__GNUC__) + // Even with CUDA_ARCH protection this thing warns about calling host function + __builtin_unreachable(); +#endif +} +#if defined __NVCC__ + #ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ + #pragma nv_diagnostic pop + #else + #ifdef __CUDA_ARCH__ + #pragma diagnostic pop + #endif + #endif +#elif defined __NVCOMPILER + #pragma diagnostic pop +#endif + +template +MDSPAN_INLINE_FUNCTION +constexpr OffsetType +last_of(std::integral_constant, const Extents &, + const strided_slice &r) { + return r.extent; +} + +// get stride of slices +template +MDSPAN_INLINE_FUNCTION +constexpr auto stride_of(const T &) { + return std::integral_constant(); +} + +template +MDSPAN_INLINE_FUNCTION +constexpr auto +stride_of(const strided_slice &r) { + return r.stride; +} + +// divide which can deal with integral constant preservation +template +MDSPAN_INLINE_FUNCTION +constexpr auto divide(const T0 &v0, const T1 &v1) { + return IndexT(v0) / IndexT(v1); +} + +template +MDSPAN_INLINE_FUNCTION +constexpr auto divide(const std::integral_constant &, + const std::integral_constant &) { + // cutting short division by zero + // this is used for strided_slice with zero extent/stride + return std::integral_constant(); +} + +// multiply which can deal with integral constant preservation +template +MDSPAN_INLINE_FUNCTION +constexpr auto multiply(const T0 &v0, const T1 &v1) { + return IndexT(v0) * IndexT(v1); +} + +template +MDSPAN_INLINE_FUNCTION +constexpr auto multiply(const std::integral_constant &, + const std::integral_constant &) { + return std::integral_constant(); +} + +// compute new static extent from range, preserving static knowledge +template struct StaticExtentFromRange { + constexpr static size_t value = dynamic_extent; +}; + +template +struct StaticExtentFromRange, + std::integral_constant> { + constexpr static size_t value = val1 - val0; +}; + +// compute new static extent from strided_slice, preserving static +// knowledge +template struct StaticExtentFromStridedRange { + constexpr static size_t value = dynamic_extent; +}; + +template +struct StaticExtentFromStridedRange, + std::integral_constant> { + constexpr static size_t value = val0 > 0 ? 1 + (val0 - 1) / val1 : 0; +}; + +// creates new extents through recursive calls to next_extent member function +// next_extent has different overloads for different types of stride specifiers +template +struct extents_constructor { + MDSPAN_TEMPLATE_REQUIRES( + class Slice, class... SlicesAndExtents, + /* requires */(!std::is_convertible_v && + !is_strided_slice::value) + ) + MDSPAN_INLINE_FUNCTION + constexpr static auto next_extent(const Extents &ext, const Slice &sl, + SlicesAndExtents... slices_and_extents) { + constexpr size_t new_static_extent = StaticExtentFromRange< + decltype(first_of(std::declval())), + decltype(last_of(std::integral_constant(), + std::declval(), + std::declval()))>::value; + + using next_t = + extents_constructor; + using index_t = typename Extents::index_type; + return next_t::next_extent( + ext, slices_and_extents..., + index_t(last_of(std::integral_constant(), ext, + sl)) - + index_t(first_of(sl))); + } + + MDSPAN_TEMPLATE_REQUIRES( + class Slice, class... SlicesAndExtents, + /* requires */ (std::is_convertible_v) + ) + MDSPAN_INLINE_FUNCTION + constexpr static auto next_extent(const Extents &ext, const Slice &, + SlicesAndExtents... slices_and_extents) { + using next_t = extents_constructor; + return next_t::next_extent(ext, slices_and_extents...); + } + + template + MDSPAN_INLINE_FUNCTION + constexpr static auto + next_extent(const Extents &ext, + const strided_slice &r, + SlicesAndExtents... slices_and_extents) { + using index_t = typename Extents::index_type; + using new_static_extent_t = + StaticExtentFromStridedRange; + if constexpr (new_static_extent_t::value == dynamic_extent) { + using next_t = + extents_constructor; + return next_t::next_extent( + ext, slices_and_extents..., + r.extent > 0 ? 1 + divide(r.extent - 1, r.stride) : 0); + } else { + constexpr size_t new_static_extent = new_static_extent_t::value; + using next_t = + extents_constructor; + return next_t::next_extent( + ext, slices_and_extents..., index_t(divide(ExtentType(), StrideType()))); + } + } +}; + +template +struct extents_constructor<0, Extents, NewStaticExtents...> { + + template + MDSPAN_INLINE_FUNCTION + constexpr static auto next_extent(const Extents &, NewExtents... new_exts) { + return extents( + new_exts...); + } +}; + +} // namespace detail + +// submdspan_extents creates new extents given src extents and submdspan slice +// specifiers +template +MDSPAN_INLINE_FUNCTION +constexpr auto submdspan_extents(const extents &src_exts, + SliceSpecifiers... slices) { + + using ext_t = extents; + return detail::extents_constructor::next_extent( + src_exts, slices...); +} +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/submdspan_extents.hpp +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/submdspan_mapping.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +#include +#include +#include +#include // index_sequence + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +//****************************************** +// Return type of submdspan_mapping overloads +//****************************************** +template struct submdspan_mapping_result { + _MDSPAN_NO_UNIQUE_ADDRESS LayoutMapping mapping{}; + size_t offset; +}; + +namespace detail { + +// We use const Slice& and not Slice&& because the various +// submdspan_mapping_impl overloads use their slices arguments +// multiple times. This makes perfect forwarding not useful, but we +// still don't want to pass those (possibly of size 64 x 3 bits) +// objects by value. +template +MDSPAN_INLINE_FUNCTION +constexpr bool +one_slice_out_of_bounds(const IndexType& extent, const Slice& slice) +{ + using common_t = std::common_type_t; + return static_cast(detail::first_of(slice)) == static_cast(extent); +} + +template +MDSPAN_INLINE_FUNCTION +constexpr bool +any_slice_out_of_bounds_helper(std::index_sequence, + const extents& exts, + const Slices& ... slices) +{ + return _MDSPAN_FOLD_OR( + (one_slice_out_of_bounds(exts.extent(RankIndices), slices)) + ); +} + +template +MDSPAN_INLINE_FUNCTION +constexpr bool +any_slice_out_of_bounds(const extents& exts, + const Slices& ... slices) +{ + return any_slice_out_of_bounds_helper( + std::make_index_sequence(), + exts, slices...); +} + +// constructs sub strides +template +MDSPAN_INLINE_FUNCTION +constexpr auto +construct_sub_strides(const SrcMapping &src_mapping, + std::index_sequence, + const std::tuple &slices_stride_factor) { + using index_type = typename SrcMapping::index_type; + return std::array{ + (static_cast(src_mapping.stride(InvMapIdxs)) * + static_cast(std::get(slices_stride_factor)))...}; +} +} // namespace detail + +//********************************** +// layout_left submdspan_mapping +//********************************* +namespace detail { + +// Figure out whether to preserve layout_left +template +struct preserve_layout_left_mapping; + +template +struct preserve_layout_left_mapping, SubRank, + SliceSpecifiers...> { + constexpr static bool value = + // Preserve layout for rank 0 + (SubRank == 0) || + ( + // Slice specifiers up to subrank need to be full_extent_t - except + // for the last one which could also be tuple but not a strided index + // range slice specifiers after subrank are integrals + ((Idx > SubRank - 1) || // these are only integral slice specifiers + (std::is_same_v) || + ((Idx == SubRank - 1) && + std::is_convertible_v>)) && + ...); +}; +} // namespace detail + +// Suppress spurious warning with NVCC about no return statement. +// This is a known issue in NVCC and NVC++ +// Depending on the CUDA and GCC version we need both the builtin +// and the diagnostic push. I tried really hard to find something shorter +// but no luck ... +#if defined __NVCC__ + #ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ + #pragma nv_diagnostic push + #pragma nv_diag_suppress = implicit_return_from_non_void_function + #else + #ifdef __CUDA_ARCH__ + #pragma diagnostic push + #pragma diag_suppress implicit_return_from_non_void_function + #endif + #endif +#elif defined __NVCOMPILER + #pragma diagnostic push + #pragma diag_suppress = implicit_return_from_non_void_function +#endif +// Actual submdspan mapping call +template +template +MDSPAN_INLINE_FUNCTION +constexpr auto +layout_left::mapping::submdspan_mapping_impl(SliceSpecifiers... slices) const { + + // compute sub extents + using src_ext_t = Extents; + auto dst_ext = submdspan_extents(extents(), slices...); + using dst_ext_t = decltype(dst_ext); + + // figure out sub layout type + constexpr bool preserve_layout = detail::preserve_layout_left_mapping< + decltype(std::make_index_sequence()), dst_ext_t::rank(), + SliceSpecifiers...>::value; + using dst_layout_t = + std::conditional_t; + using dst_mapping_t = typename dst_layout_t::template mapping; + + // Figure out if any slice's lower bound equals the corresponding extent. + // If so, bypass evaluating the layout mapping. This fixes LWG Issue 4060. + const bool out_of_bounds = + detail::any_slice_out_of_bounds(this->extents(), slices...); + auto offset = static_cast( + out_of_bounds ? + this->required_span_size() : + this->operator()(detail::first_of(slices)...) + ); + + if constexpr (std::is_same_v) { + // layout_left case + return submdspan_mapping_result{dst_mapping_t(dst_ext), offset}; + } else { + // layout_stride case + auto inv_map = detail::inv_map_rank( + std::integral_constant(), + std::index_sequence<>(), + slices...); + return submdspan_mapping_result{ + dst_mapping_t(dst_ext, detail::construct_sub_strides( + *this, inv_map, + // HIP needs deduction guides to have markups so we need to be explicit + // NVCC 11.0 has a bug with deduction guide here, tested that 11.2 does not have the issue + // But Clang-CUDA also doesn't accept the use of deduction guide so disable it for CUDA alltogether + #if defined(_MDSPAN_HAS_HIP) || defined(_MDSPAN_HAS_CUDA) + std::tuple{detail::stride_of(slices)...})), + #else + std::tuple{detail::stride_of(slices)...})), + #endif + offset}; + } +#if defined(__NVCC__) && !defined(__CUDA_ARCH__) && defined(__GNUC__) + __builtin_unreachable(); +#endif +} +#if defined __NVCC__ + #ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ + #pragma nv_diagnostic pop + #else + #ifdef __CUDA_ARCH__ + #pragma diagnostic pop + #endif + #endif +#elif defined __NVCOMPILER + #pragma diagnostic pop +#endif + +//********************************** +// layout_right submdspan_mapping +//********************************* +namespace detail { + +// Figure out whether to preserve layout_right +template +struct preserve_layout_right_mapping; + +template +struct preserve_layout_right_mapping, SubRank, + SliceSpecifiers...> { + constexpr static size_t SrcRank = sizeof...(SliceSpecifiers); + constexpr static bool value = + // Preserve layout for rank 0 + (SubRank == 0) || + ( + // The last subrank slice specifiers need to be full_extent_t - except + // for the srcrank-subrank one which could also be tuple but not a + // strided index range slice specifiers before srcrank-subrank are + // integrals + ((Idx < + SrcRank - SubRank) || // these are only integral slice specifiers + (std::is_same_v) || + ((Idx == SrcRank - SubRank) && + std::is_convertible_v>)) && + ...); +}; +} // namespace detail + +// Suppress spurious warning with NVCC about no return statement. +// This is a known issue in NVCC and NVC++ +// Depending on the CUDA and GCC version we need both the builtin +// and the diagnostic push. I tried really hard to find something shorter +// but no luck ... +#if defined __NVCC__ + #ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ + #pragma nv_diagnostic push + #pragma nv_diag_suppress = implicit_return_from_non_void_function + #else + #ifdef __CUDA_ARCH__ + #pragma diagnostic push + #pragma diag_suppress implicit_return_from_non_void_function + #endif + #endif +#elif defined __NVCOMPILER + #pragma diagnostic push + #pragma diag_suppress = implicit_return_from_non_void_function +#endif +template +template +MDSPAN_INLINE_FUNCTION +constexpr auto +layout_right::mapping::submdspan_mapping_impl( + SliceSpecifiers... slices) const { + // get sub extents + using src_ext_t = Extents; + auto dst_ext = submdspan_extents(extents(), slices...); + using dst_ext_t = decltype(dst_ext); + + // determine new layout type + constexpr bool preserve_layout = detail::preserve_layout_right_mapping< + decltype(std::make_index_sequence()), dst_ext_t::rank(), + SliceSpecifiers...>::value; + using dst_layout_t = + std::conditional_t; + using dst_mapping_t = typename dst_layout_t::template mapping; + + // Figure out if any slice's lower bound equals the corresponding extent. + // If so, bypass evaluating the layout mapping. This fixes LWG Issue 4060. + const bool out_of_bounds = + detail::any_slice_out_of_bounds(this->extents(), slices...); + auto offset = static_cast( + out_of_bounds ? + this->required_span_size() : + this->operator()(detail::first_of(slices)...) + ); + + if constexpr (std::is_same_v) { + // layout_right case + return submdspan_mapping_result{dst_mapping_t(dst_ext), offset}; + } else { + // layout_stride case + auto inv_map = detail::inv_map_rank( + std::integral_constant(), + std::index_sequence<>(), + slices...); + return submdspan_mapping_result{ + dst_mapping_t(dst_ext, detail::construct_sub_strides( + *this, inv_map, + // HIP needs deduction guides to have markups so we need to be explicit + // NVCC 11.0 has a bug with deduction guide here, tested that 11.2 does not have the issue + // But Clang-CUDA also doesn't accept the use of deduction guide so disable it for CUDA alltogether + #if defined(_MDSPAN_HAS_HIP) || defined(_MDSPAN_HAS_CUDA) + std::tuple{detail::stride_of(slices)...})), + #else + std::tuple{detail::stride_of(slices)...})), + #endif + offset}; + } +#if defined(__NVCC__) && !defined(__CUDA_ARCH__) && defined(__GNUC__) + __builtin_unreachable(); +#endif +} +#if defined __NVCC__ + #ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ + #pragma nv_diagnostic pop + #else + #ifdef __CUDA_ARCH__ + #pragma diagnostic pop + #endif + #endif +#elif defined __NVCOMPILER + #pragma diagnostic pop +#endif + +//********************************** +// layout_stride submdspan_mapping +//********************************* +template +template +MDSPAN_INLINE_FUNCTION +constexpr auto +layout_stride::mapping::submdspan_mapping_impl( + SliceSpecifiers... slices) const { + auto dst_ext = submdspan_extents(extents(), slices...); + using dst_ext_t = decltype(dst_ext); + auto inv_map = detail::inv_map_rank( + std::integral_constant(), + std::index_sequence<>(), + slices...); + using dst_mapping_t = typename layout_stride::template mapping; + + // Figure out if any slice's lower bound equals the corresponding extent. + // If so, bypass evaluating the layout mapping. This fixes LWG Issue 4060. + const bool out_of_bounds = + detail::any_slice_out_of_bounds(this->extents(), slices...); + auto offset = static_cast( + out_of_bounds ? + this->required_span_size() : + this->operator()(detail::first_of(slices)...) + ); + + return submdspan_mapping_result{ + dst_mapping_t(dst_ext, detail::construct_sub_strides( + *this, inv_map, + // HIP needs deduction guides to have markups so we need to be explicit + // NVCC 11.0 has a bug with deduction guide here, tested that 11.2 does not have the issue + #if defined(_MDSPAN_HAS_HIP) || (defined(__NVCC__) && (__CUDACC_VER_MAJOR__ * 100 + __CUDACC_VER_MINOR__ * 10) < 1120) + std::tuple(detail::stride_of(slices)...))), +#else + std::tuple(detail::stride_of(slices)...))), +#endif + offset}; +} + +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/submdspan_mapping.hpp + +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +template +MDSPAN_INLINE_FUNCTION +constexpr auto +submdspan(const mdspan &src, + SliceSpecifiers... slices) { + const auto sub_submdspan_mapping_result = submdspan_mapping(src.mapping(), slices...); + // NVCC has a problem with the deduction so lets figure out the type + using sub_mapping_t = std::remove_cv_t; + using sub_extents_t = typename sub_mapping_t::extents_type; + using sub_layout_t = typename sub_mapping_t::layout_type; + using sub_accessor_t = typename AccessorPolicy::offset_policy; + return mdspan( + src.accessor().offset(src.data_handle(), sub_submdspan_mapping_result.offset), + sub_submdspan_mapping_result.mapping, + sub_accessor_t(src.accessor())); +} +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p2630_bits/submdspan.hpp +#endif +//BEGIN_FILE_INCLUDE: mdspan/include/experimental/__p2389_bits/dims.hpp +//@HEADER +// ************************************************************************ +// +// Kokkos v. 4.0 +// Copyright (2022) National Technology & Engineering +// Solutions of Sandia, LLC (NTESS). +// +// Under the terms of Contract DE-NA0003525 with NTESS, +// the U.S. Government retains certain rights in this software. +// +// Part of Kokkos, under the Apache License v2.0 with LLVM Exceptions. +// See https://kokkos.org/LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//@HEADER + + +// backward compatibility import into experimental +namespace MDSPAN_IMPL_STANDARD_NAMESPACE { +namespace MDSPAN_IMPL_PROPOSED_NAMESPACE { + +template< ::std::size_t Rank, class IndexType = std::size_t> +using dims = + :: MDSPAN_IMPL_STANDARD_NAMESPACE :: dextents; + +} // namespace MDSPAN_IMPL_PROPOSED_NAMESPACE +} // namespace MDSPAN_IMPL_STANDARD_NAMESPACE +//END_FILE_INCLUDE: mdspan/include/experimental/__p2389_bits/dims.hpp + +#endif // MDSPAN_HPP_ +//END_FILE_INCLUDE: mdspan/include/mdspan/mdspan.hpp +#endif // _MDSPAN_SINGLE_HEADER_INCLUDE_GUARD_ + diff --git a/scipy/special/tests/test_basic.py b/scipy/special/tests/test_basic.py index ca8845f7a3e2..179d9842d976 100644 --- a/scipy/special/tests/test_basic.py +++ b/scipy/special/tests/test_basic.py @@ -1063,7 +1063,7 @@ def test_ai_zeros(self): array([0.5357]), array([0.7012])),4) - @pytest.mark.fail_slow(2) + @pytest.mark.fail_slow(5) def test_ai_zeros_big(self): z, zp, ai_zpx, aip_zx = special.ai_zeros(50000) ai_z, aip_z, _, _ = special.airy(z) @@ -1088,7 +1088,7 @@ def test_ai_zeros_big(self): [-1.0187929716, -3.2481975822, -4.8200992112, -6.1633073556, -7.3721772550, -8.4884867340], rtol=1e-10) - @pytest.mark.fail_slow(2) + @pytest.mark.fail_slow(5) def test_bi_zeros_big(self): z, zp, bi_zpx, bip_zx = special.bi_zeros(50000) _, _, bi_z, bip_z = special.airy(z) @@ -2202,10 +2202,9 @@ def test_factorial_float_reference(self): def _check(n, expected): assert_allclose(special.factorial(n), expected) assert_allclose(special.factorial([n])[0], expected) - # using floats with exact=True is deprecated for scalars... - with pytest.deprecated_call(match="Non-integer values.*"): + # using floats with `exact=True` raises an error for scalars and arrays + with pytest.raises(ValueError, match="Non-integer values.*"): assert_allclose(special.factorial(n, exact=True), expected) - # ... and already an error for arrays with pytest.raises(ValueError, match="factorial with `exact=Tr.*"): special.factorial([n], exact=True) @@ -2270,15 +2269,14 @@ def assert_really_equal(x, y): def test_factorial_scalar_corner_cases(self, n, exact): if (n is None or n is np.nan or np.issubdtype(type(n), np.integer) or np.issubdtype(type(n), np.floating)): - # no error if (np.issubdtype(type(n), np.floating) and exact and n is not np.nan): - with pytest.deprecated_call(match="Non-integer values.*"): - result = special.factorial(n, exact=exact) + with pytest.raises(ValueError, match="Non-integer values.*"): + special.factorial(n, exact=exact) else: result = special.factorial(n, exact=exact) - exp = np.nan if n is np.nan or n is None else special.factorial(n) - assert_equal(result, exp) + exp = np.nan if n is np.nan or n is None else special.factorial(n) + assert_equal(result, exp) else: with pytest.raises(ValueError, match="Unsupported datatype*"): special.factorial(n, exact=exact) @@ -4152,6 +4150,46 @@ def xfunc(x, y): assert_func_equal(special.rel_entr, w, z, rtol=1e-13, atol=1e-13) +def test_rel_entr_gh_20710_near_zero(): + # Check accuracy of inputs which are very close + inputs = np.array([ + # x, y + (0.9456657713430001, 0.9456657713430094), + (0.48066098564791515, 0.48066098564794774), + (0.786048657854401, 0.7860486578542367), + ]) + # Known values produced using `x * mpmath.log(x / y)` with dps=30 + expected = [ + -9.325873406851269e-15, + -3.258504577274724e-14, + 1.6431300764454033e-13, + ] + x = inputs[:, 0] + y = inputs[:, 1] + assert_allclose(special.rel_entr(x, y), expected, rtol=1e-13, atol=0) + + +def test_rel_entr_gh_20710_overflow(): + inputs = np.array([ + # x, y + # Overflow + (4, 2.22e-308), + # Underflow + (1e-200, 1e+200), + # Subnormal + (2.22e-308, 1e15), + ]) + # Known values produced using `x * mpmath.log(x / y)` with dps=30 + expected = [ + 2839.139983229607, + -9.210340371976183e-198, + -1.6493212008074475e-305, + ] + x = inputs[:, 0] + y = inputs[:, 1] + assert_allclose(special.rel_entr(x, y), expected, rtol=1e-13, atol=0) + + def test_huber(): assert_equal(special.huber(-1, 1.5), np.inf) assert_allclose(special.huber(2, 1.5), 0.5 * np.square(1.5)) diff --git a/scipy/special/tests/test_cython_special.py b/scipy/special/tests/test_cython_special.py index bfd28744747c..c5fd4530ee91 100644 --- a/scipy/special/tests/test_cython_special.py +++ b/scipy/special/tests/test_cython_special.py @@ -320,7 +320,7 @@ def test_cython_api_completeness(): raise RuntimeError(f"{name} missing from tests!") -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(20) @pytest.mark.parametrize("param", PARAMS, ids=IDS) def test_cython_api(param): pyfunc, cyfunc, specializations, knownfailure = param diff --git a/scipy/special/tests/test_extending.py b/scipy/special/tests/test_extending.py index 57ab39a9d489..f5e941da953d 100644 --- a/scipy/special/tests/test_extending.py +++ b/scipy/special/tests/test_extending.py @@ -7,7 +7,7 @@ from scipy.special import beta, gamma -@pytest.mark.fail_slow(20) +@pytest.mark.fail_slow(40) # essential per https://github.com/scipy/scipy/pull/20487#discussion_r1567057247 @pytest.mark.skipif(IS_EDITABLE, reason='Editable install cannot find .pxd headers.') diff --git a/scipy/special/tests/test_round.py b/scipy/special/tests/test_round.py index 07d3850f1084..877cb4a9dd22 100644 --- a/scipy/special/tests/test_round.py +++ b/scipy/special/tests/test_round.py @@ -4,14 +4,14 @@ from scipy.special import _test_internal -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(20) @pytest.mark.skipif(not _test_internal.have_fenv(), reason="no fenv()") def test_add_round_up(): np.random.seed(1234) _test_internal.test_add_round(10**5, 'up') -@pytest.mark.fail_slow(5) +@pytest.mark.fail_slow(20) @pytest.mark.skipif(not _test_internal.have_fenv(), reason="no fenv()") def test_add_round_down(): np.random.seed(1234) diff --git a/scipy/special/tests/test_support_alternative_backends.py b/scipy/special/tests/test_support_alternative_backends.py index 6d0798409e51..8485826f6668 100644 --- a/scipy/special/tests/test_support_alternative_backends.py +++ b/scipy/special/tests/test_support_alternative_backends.py @@ -47,10 +47,11 @@ def test_rel_entr_generic(dtype): xp_assert_close(res, xp.asarray(ref), xp=xp) -@pytest.mark.fail_slow(2) +@pytest.mark.fail_slow(5) @array_api_compatible @given(data=strategies.data()) -@pytest.mark.parametrize('f_name_n_args', array_special_func_map.items()) +# `reversed` is for developer convenience: test new function first = less waiting +@pytest.mark.parametrize('f_name_n_args', reversed(array_special_func_map.items())) def test_support_alternative_backends(xp, data, f_name_n_args): f_name, n_args = f_name_n_args diff --git a/scipy/special/ufunc.h b/scipy/special/ufunc.h index 86e3dd13a75c..c097a18197a0 100644 --- a/scipy/special/ufunc.h +++ b/scipy/special/ufunc.h @@ -15,7 +15,7 @@ #include #include "sf_error.h" -#include "special/mdspan.h" +#include "special/third_party/kokkos/mdspan.hpp" // This is std::accumulate, but that is not constexpr until C++20 diff --git a/scipy/stats/__init__.py b/scipy/stats/__init__.py index 180f50a1756f..580475940ce1 100644 --- a/scipy/stats/__init__.py +++ b/scipy/stats/__init__.py @@ -32,6 +32,17 @@ Probability distributions ========================= +Random Variables +---------------- + +.. autosummary:: + :toctree: generated/ + + ContinuousDistribution + Normal + Uniform + LogUniform + Each univariate distribution is an instance of a subclass of `rv_continuous` (`rv_discrete` for discrete distributions): @@ -521,14 +532,6 @@ stats.sampling -Random variate generation / CDF Inversion ------------------------------------------ - -.. autosummary:: - :toctree: generated/ - - rvs_ratio_uniforms - Fitting / Survival Analysis --------------------------- @@ -625,7 +628,6 @@ MonteCarloMethod, PermutationMethod, BootstrapMethod) from ._entropy import * from ._hypotests import * -from ._rvs_sampling import rvs_ratio_uniforms from ._page_trend_test import page_trend_test from ._mannwhitneyu import mannwhitneyu from ._bws_test import bws_test @@ -633,6 +635,9 @@ from ._covariance import Covariance from ._sensitivity_analysis import * from ._survival import * +from ._new_distributions import Normal, Uniform, LogUniform +from ._distribution_infrastructure import (ContinuousDistribution, + ShiftedScaledDistribution) from ._mgc import multiscale_graphcorr diff --git a/scipy/stats/_ansari_swilk_statistics.pyx b/scipy/stats/_ansari_swilk_statistics.pyx index 43558a661bd5..7fd302288cd4 100644 --- a/scipy/stats/_ansari_swilk_statistics.pyx +++ b/scipy/stats/_ansari_swilk_statistics.pyx @@ -150,7 +150,7 @@ cdef inline void _start2(float[::1] a, int n) noexcept nogil: a[(ndo*2)-1] = 2 -cdef inline int _frqadd(float[::1] a, float[::1] b, int lenb, +cdef inline int _frqadd(float[::1] a, const float[::1] b, int lenb, int offset) noexcept nogil: """ Helper function for gscale function, see gscale docstring. @@ -206,7 +206,7 @@ cdef int _imply(float[::1] a, int curlen, int reslen, float[::1] b, return nextlenb -def swilk(double[::1] x, double[::1] a, bint init=False, int n1=-1): +def swilk(const double[::1] x, double[::1] a, bint init=False, int n1=-1): """ Calculates the Shapiro-Wilk W test and its significance level @@ -521,7 +521,7 @@ cdef double _ppnd(double p) noexcept: return (-temp if q < 0 else temp) #, 0 -cdef double _poly(double[::1]c, int nord, double x) noexcept nogil: +cdef double _poly(const double[::1] c, int nord, double x) noexcept nogil: """ Helper function for swilk function that evaluates polynomials. For some reason, the coefficients are given as diff --git a/scipy/stats/_axis_nan_policy.py b/scipy/stats/_axis_nan_policy.py index 24e36bfa7d14..f6427c38eb63 100644 --- a/scipy/stats/_axis_nan_policy.py +++ b/scipy/stats/_axis_nan_policy.py @@ -4,13 +4,39 @@ # that support `axis` and `nan_policy`, including a decorator that # automatically adds `axis` and `nan_policy` arguments to a function. +import warnings import numpy as np from functools import wraps from scipy._lib._docscrape import FunctionDoc, Parameter from scipy._lib._util import _contains_nan, AxisError, _get_nan from scipy._lib._array_api import array_namespace, is_numpy + import inspect +too_small_1d_not_omit = ( + "One or more sample arguments is too small; all " + "returned values will be NaN. " + "See documentation for sample size requirements.") + +too_small_1d_omit = ( + "After omitting NaNs, one or more sample arguments " + "is too small; all returned values will be NaN. " + "See documentation for sample size requirements.") + +too_small_nd_not_omit = ( + "All axis-slices of one or more sample arguments are " + "too small; all elements of returned arrays will be NaN. " + "See documentation for sample size requirements.") + +too_small_nd_omit = ( + "After omitting NaNs, one or more axis-slices of one " + "or more sample arguments is too small; corresponding " + "elements of returned arrays will be NaN. " + "See documentation for sample size requirements.") + +class SmallSampleWarning(RuntimeWarning): + pass + def _broadcast_arrays(arrays, axis=None, xp=None): """ @@ -242,7 +268,8 @@ def _add_reduced_axes(res, reduced_axes, keepdims): Add reduced axes back to all the arrays in the result object if keepdims = True. """ - return ([np.expand_dims(output, reduced_axes) for output in res] + return ([np.expand_dims(output, reduced_axes) + if not isinstance(output, int) else output for output in res] if keepdims else res) @@ -363,7 +390,6 @@ def _axis_nan_policy_factory(tuple_to_result, default_axis=0, decorator overrides the function's behavior for multimensional input. Use ``'nan_propagation': False`` to ensure that the decorator does not override the function's behavior for ``nan_policy='propagate'``. - (See `scipy.stats.mode`, for example.) """ # Specify which existing behaviors the decorator must override temp = override or {} @@ -529,33 +555,36 @@ def hypotest_fun_out(*samples, **kwds): return tuple_to_result(*res) # Addresses nan_policy == "omit" + too_small_msg = too_small_1d_not_omit if any(contains_nan) and nan_policy == 'omit': # consider passing in contains_nan samples = _remove_nans(samples, paired) - - # ideally, this is what the behavior would be: - # if is_too_small(samples): - # return tuple_to_result(NaN, NaN) - # but some existing functions raise exceptions, and changing - # behavior of those would break backward compatibility. + too_small_msg = too_small_1d_omit if sentinel: samples = _remove_sentinel(samples, paired, sentinel) + + if is_too_small(samples, kwds): + warnings.warn(too_small_msg, SmallSampleWarning, stacklevel=2) + res = np.full(n_out, NaN) + res = _add_reduced_axes(res, reduced_axes, keepdims) + return tuple_to_result(*res) + res = hypotest_fun_out(*samples, **kwds) res = result_to_tuple(res) res = _add_reduced_axes(res, reduced_axes, keepdims) return tuple_to_result(*res) # check for empty input - # ideally, move this to the top, but some existing functions raise - # exceptions for empty input, so overriding it would break - # backward compatibility. empty_output = _check_empty_inputs(samples, axis) # only return empty output if zero sized input is too small. if ( empty_output is not None and (is_too_small(samples, kwds) or empty_output.size == 0) ): + if is_too_small(samples, kwds) and empty_output.size != 0: + warnings.warn(too_small_nd_not_omit, SmallSampleWarning, + stacklevel=2) res = [empty_output.copy() for i in range(n_out)] res = _add_reduced_axes(res, reduced_axes, keepdims) return tuple_to_result(*res) @@ -586,6 +615,8 @@ def hypotest_fun(x): if sentinel: samples = _remove_sentinel(samples, paired, sentinel) if is_too_small(samples, kwds): + warnings.warn(too_small_nd_omit, SmallSampleWarning, + stacklevel=4) return np.full(n_out, NaN) return result_to_tuple(hypotest_fun_out(*samples, **kwds)) diff --git a/scipy/stats/_continuous_distns.py b/scipy/stats/_continuous_distns.py index b06abfa7f926..a4e1dcd27a19 100644 --- a/scipy/stats/_continuous_distns.py +++ b/scipy/stats/_continuous_distns.py @@ -9683,12 +9683,51 @@ def _entropy(self, c, d): return 0.5 * (1.0-d+c) / (1.0+d-c) + np.log(0.5 * (1.0+d-c)) +# deprecation of trapz, see #20486 +deprmsg = ("`trapz` is deprecated in favour of `trapezoid` " + "and will be removed in SciPy 1.16.0.") + + +class trapz_gen(trapezoid_gen): + # override __call__ protocol from rv_generic to also + # deprecate instantiation of frozen distributions + """ + + .. deprecated:: 1.14.0 + `trapz` is deprecated and will be removed in SciPy 1.16. + Plese use `trapezoid` instead! + """ + def __call__(self, *args, **kwds): + warnings.warn(deprmsg, DeprecationWarning, stacklevel=2) + return self.freeze(*args, **kwds) + + trapezoid = trapezoid_gen(a=0.0, b=1.0, name="trapezoid") -# Note: alias kept for backwards compatibility. Rename was done -# because trapz is a slur in colloquial English (see gh-12924). -trapz = trapezoid_gen(a=0.0, b=1.0, name="trapz") -if trapz.__doc__: - trapz.__doc__ = "trapz is an alias for `trapezoid`" +trapz = trapz_gen(a=0.0, b=1.0, name="trapz") + +# since the deprecated class gets intantiated upon import (and we only want to +# warn upon use), add the deprecation to each class method +_method_names = [ + "cdf", "entropy", "expect", "fit", "interval", "isf", "logcdf", "logpdf", + "logsf", "mean", "median", "moment", "pdf", "ppf", "rvs", "sf", "stats", + "std", "var" +] + + +class _DeprecationWrapper: + def __init__(self, method): + self.msg = (f"`trapz.{method}` is deprecated in favour of trapezoid.{method}. " + "Please replace all uses of the distribution class " + "`trapz` with `trapezoid`. `trapz` will be removed in SciPy 1.16.") + self.method = getattr(trapezoid, method) + + def __call__(self, *args, **kwargs): + warnings.warn(self.msg, DeprecationWarning, stacklevel=2) + return self.method(*args, **kwargs) + + +for m in _method_names: + setattr(trapz, m, _DeprecationWrapper(m)) class triang_gen(rv_continuous): diff --git a/scipy/stats/_distribution_infrastructure.py b/scipy/stats/_distribution_infrastructure.py new file mode 100644 index 000000000000..388a00b8abe8 --- /dev/null +++ b/scipy/stats/_distribution_infrastructure.py @@ -0,0 +1,5453 @@ +import functools +from abc import ABC, abstractmethod +from functools import cached_property + +import numpy as np + +from scipy._lib._util import _lazywhere +from scipy._lib._docscrape import ClassDoc, NumpyDocString +from scipy import special, optimize +from scipy.integrate._tanhsinh import _tanhsinh +from scipy.optimize._bracket import _bracket_root, _bracket_minimum +from scipy.optimize._chandrupatla import _chandrupatla, _chandrupatla_minimize + +oo = np.inf +# in case we need to distinguish between None and not specified +_null = object() +def _isnull(x): + return type(x) == object or x is None + +__all__ = ['ContinuousDistribution'] + +# Could add other policies for broadcasting and edge/out-of-bounds case handling +# For instance, when edge case handling is known not to be needed, it's much +# faster to turn it off, but it might still be nice to have array conversion +# and shaping done so the user doesn't need to be so carefuly. +_SKIP_ALL = "skip_all" +# Other cache policies would be useful, too. +_NO_CACHE = "no_cache" + +# TODO: +# Test sample dtypes +# Add dtype kwarg (especially for distributions with no parameters) +# When drawing endpoint/out-of-bounds values of a parameter, draw them from +# the endpoints/out-of-bounds region of the full `domain`, not `typical`. +# Distributions without shape parameters probably need to accept a `dtype` parameter; +# right now they default to float64. If we have them default to float16, they will +# need to determine result_type when input is not float16 (overhead). +# Test _solve_bounded bracket logic, and decide what to do about warnings +# Get test coverage to 100% +# Raise when distribution method returns wrong shape/dtype? +# Consider ensuring everything is at least 1D for calculations? Would avoid needing +# to sprinkle `np.asarray` throughout due to indescriminate conversion of 0D arrays +# to scalars +# Break up `test_basic`: test each method separately +# Fix `sample` for QMCEngine (implementation does not match documentation) +# When a parameter is invalid, set only the offending parameter to NaN (if possible)? +# `_tanhsinh` special case when there are no abscissae between the limits +# example: cdf of uniform betweeen 1.0 and np.nextafter(1.0, np.inf) +# check behavior of moment methods when moments are undefined/infinite - +# basically OK but needs tests +# investigate use of median +# implement symmetric distribution +# implement composite distribution +# implement wrapped distribution +# implement folded distribution +# implement double distribution +# profile/optimize +# general cleanup (choose keyword-only parameters) +# compare old/new distribution timing +# make video +# PR +# add array API support +# why does dist.ilogcdf(-100) not converge to bound? Check solver response to inf +# _chandrupatla_minimize should not report xm = fm = NaN when it fails +# integrate `logmoment` into `moment`? (Not hard, but enough time and code +# complexity to wait for reviewer feedback before adding.) +# Eliminate bracket_root error "`min <= a < b <= max` must be True" +# Test repr? +# use `median` information to improve integration? In some cases this will +# speed things up. If it's not needed, it may be about twice as slow. I think +# it should depend on the accuracy setting. +# in tests, check reference value against that produced using np.vectorize? +# add `axis` to `ks_1samp` +# Getting `default_rng` takes forever! OK to do it only when support is called? +# User tips for faster execution: +# - pass NumPy arrays +# - pass inputs of floating point type (not integers) +# - prefer NumPy scalars or 0d arrays over other size 1 arrays +# - pass no invalid parameters and disable invalid parameter checks with iv_profile +# - provide a Generator if you're going to do sampling +# add options for drawing parameters: log-spacing +# accuracy benchmark suite +# Should caches be attributes so we can more easily ensure that they are not +# modified when caching is turned off? +# Make ShiftedScaledDistribution more efficient - only process underlying +# distribution parameters as necessary. +# Reconsider `all_inclusive` +# Should process_parameters update kwargs rather than returning? Should we +# update parameters rather than setting to what process_parameters returns? + +# Questions: +# 1. I override `__getattr__` so that distribution parameters can be read as +# attributes. We don't want uses to try to change them. +# - To prevent replacements (dist.a = b), I could override `__setattr__`. +# - To prevent in-place modifications, `__getattr__` could return a copy, +# or it could set the WRITEABLE flag of the array to false. +# Which should I do? +# 2. `cache_policy` is supported in several methods where I imagine it being +# useful, but it needs to be tested. Before doing that: +# - What should the default value be? +# - What should the other values be? +# Or should we just eliminate this policy? +# 3. `iv_policy` is supported in a few places, but it should be checked for +# consistency. I have the same questions as for `cache_policy`. +# 4. `tol` is currently notional. I think there needs to be way to set +# separate `atol` and `rtol`. Some ways I imagine it being used: +# - Values can be passed to iterative functions (quadrature, root-finder). +# - To control which "method" of a distribution function is used. For +# example, if `atol` is set to `1e-12`, it may be acceptable to compute +# the complementary CDF as 1 - CDF even when CDF is nearly 1; otherwise, +# a (potentially more time-consuming) method would need to be used. +# I'm looking for unified suggestions for the interface, not ad hoc ideas +# for using tolerances. Suppose the user wants to have more control over +# the tolerances used for each method - how do they specify it? It would +# probably be easiest for the user if they could pass tolerances into each +# method, but it's easiest for us if they can only set it as a property of +# the class. Perhaps a dictionary of tolerance settings? +# 5. I also envision that accuracy estimates should be reported to the user +# somehow. I think my preference would be to return a subclass of an array +# with an `error` attribute - yes, really. But this is unlikely to be +# popular, so what are other ideas? Again, we need a unified vision here, +# not just pointing out difficulties (not all errors are known or easy +# to estimate, what to do when errors could compound, etc.). +# 6. The term "method" is used to refer to public instance functions, +# private instance functions, the "method" string argument, and the means +# of calculating the desired quantity (represented by the string argument). +# For the sake of disambiguation, shall I rename the "method" string to +# "strategy" and refer to the means of calculating the quantity as the +# "strategy"? + +# Originally, I planned to filter out invalid distribution parameters; +# distribution implementation functions would always work with "compressed", +# 1D arrays containing only valid distribution parameters. There are two +# problems with this: +# - This essentially requires copying all arrays, even if there is only a +# single invalid parameter combination. This is expensive. Then, to output +# the original size data to the user, we need to "decompress" the arrays +# and fill in the NaNs, so more copying. Unless we branch the code when +# there are no invalid data, these copies happen even in the normal case, +# where there are no invalid parameter combinations. We should not incur +# all this overhead in the normal case. +# - For methods that accept arguments other than distribution parameters, the +# user will pass in arrays that are broadcastable with the original arrays, +# not the compressed arrays. This means that this same sort of invalid +# value detection needs to be repeated every time one of these methods is +# called. +# The much simpler solution is to keep the data uncompressed but to replace +# the invalid parameters and arguments with NaNs (and only if some are +# invalid). With this approach, the copying happens only if/when it is +# needed. Most functions involved in stats distribution calculations don't +# mind NaNs; they just return NaN. The behavior "If x_i is NaN, the result +# is NaN" is explicit in the array API. So this should be fine. +# +# Currently, I am still leaving the parameters and function arguments +# in their broadcasted shapes rather than, say, raveling. The intent +# is to avoid back and forth reshaping. If authors of distributions have +# trouble dealing with N-D arrays, we can reconsider this. +# +# Another important decision is that the *private* methods must accept +# the distribution parameters as inputs rather than relying on these +# cached properties directly (although the public methods typically pass +# the cached values to the private methods). This is because the elementwise +# algorithms for quadrature, differentiation, root-finding, and minimization +# prefer that the input functions are strictly elementwise in the sense +# that the value output for a given input element does not depend on the +# shape of the input or that element's location within the input array. +# When the computation has converged for an element, it is removed from +# the computation entirely. As a result, the shape of the arrays passed to +# the function will almost never be broadcastable with the shape of the +# cached parameter arrays. +# +# I've sprinkled in some optimizations for scalars and same-shape/type arrays +# throughout. The biggest time sinks before were: +# - broadcast_arrays +# - result_dtype +# - is_subdtype +# It is much faster to check whether these are necessary than to do them. + + +class _Domain(ABC): + r""" Representation of the applicable domain of a parameter or variable. + + A `_Domain` object is responsible for storing information about the + domain of a parameter or variable, determining whether a value is within + the domain (`contains`), and providing a text/mathematical representation + of itself (`__str__`). Because the domain of a parameter/variable can have + a complicated relationship with other parameters and variables of a + distribution, `_Domain` itself does not try to represent all possibilities; + in fact, it has no implementation and is meant for subclassing. + + Attributes + ---------- + symbols : dict + A map from special numerical values to symbols for use in `__str__` + + Methods + ------- + contains(x) + Determine whether the argument is contained within the domain (True) + or not (False). Used for input validation. + get_numerical_endpoints() + Gets the numerical values of the domain endpoints, which may have been + defined symbolically. + __str__() + Returns a text representation of the domain (e.g. `[-π, ∞)`). + Used for generating documentation. + + """ + symbols = {np.inf: "∞", -np.inf: "-∞", np.pi: "π", -np.pi: "-π"} + + @abstractmethod + def contains(self, x): + raise NotImplementedError() + + @abstractmethod + def get_numerical_endpoints(self, x): + raise NotImplementedError() + + @abstractmethod + def __str__(self): + raise NotImplementedError() + + +class _SimpleDomain(_Domain): + r""" Representation of a simply-connected domain defined by two endpoints. + + Each endpoint may be a finite scalar, positive or negative infinity, or + be given by a single parameter. The domain may include the endpoints or + not. + + This class still does not provide an implementation of the __str__ method, + so it is meant for subclassing (e.g. a subclass for domains on the real + line). + + Attributes + ---------- + symbols : dict + Inherited. A map from special values to symbols for use in `__str__`. + endpoints : 2-tuple of float(s) and/or str(s) + A tuple with two values. Each may be either a float (the numerical + value of the endpoints of the domain) or a string (the name of the + parameters that will define the endpoint). + inclusive : 2-tuple of bools + A tuple with two boolean values; each indicates whether the + corresponding endpoint is included within the domain or not. + + Methods + ------- + define_parameters(*parameters) + Records any parameters used to define the endpoints of the domain + get_numerical_endpoints(parameter_values) + Gets the numerical values of the domain endpoints, which may have been + defined symbolically. + contains(item, parameter_values) + Determines whether the argument is contained within the domain + + """ + def __init__(self, endpoints=(-oo, oo), inclusive=(False, False)): + a, b = endpoints + self.endpoints = np.asarray(a)[()], np.asarray(b)[()] + self.inclusive = inclusive + # self.all_inclusive = (endpoints == (-oo, oo) + # and inclusive == (True, True)) + + def define_parameters(self, *parameters): + r""" Records any parameters used to define the endpoints of the domain. + + Adds the keyword name of each parameter and its text representation + to the `symbols` attribute as key:value pairs. + For instance, a parameter may be passed into to a distribution's + initializer using the keyword `log_a`, and the corresponding + string representation may be '\log(a)'. To form the text + representation of the domain for use in documentation, the + _Domain object needs to map from the keyword name used in the code + to the string representation. + + Returns None, but updates the `symbols` attribute. + + Parameters + ---------- + *parameters : _Parameter objects + Parameters that may define the endpoints of the domain. + + """ + new_symbols = {param.name: param.symbol for param in parameters} + self.symbols.update(new_symbols) + + def get_numerical_endpoints(self, parameter_values): + r""" Get the numerical values of the domain endpoints. + + Domain endpoints may be defined symbolically. This returns numerical + values of the endpoints given numerical values for any variables. + + Parameters + ---------- + parameter_values : dict + A dictionary that maps between string variable names and numerical + values of parameters, which may define the endpoints. + + Returns + ------- + a, b : ndarray + Numerical values of the endpoints + + """ + # TODO: ensure outputs are floats + a, b = self.endpoints + # If `a` (`b`) is a string - the name of the parameter that defines + # the endpoint of the domain - then corresponding numerical values + # will be found in the `parameter_values` dictionary. Otherwise, it is + # itself the array of numerical values of the endpoint. + try: + a = np.asarray(parameter_values.get(a, a)) + b = np.asarray(parameter_values.get(b, b)) + except TypeError as e: + message = ("The endpoints of the distribution are defined by " + "parameters, but their values were not provided. When " + f"using a private method of {self.__class__}, pass " + "all required distribution parameters as keyword " + "arguments.") + raise TypeError(message) from e + + return a, b + + def contains(self, item, parameter_values=None): + r"""Determine whether the argument is contained within the domain. + + Parameters + ---------- + item : ndarray + The argument + parameter_values : dict + A dictionary that maps between string variable names and numerical + values of parameters, which may define the endpoints. + + Returns + ------- + out : bool + True if `item` is within the domain; False otherwise. + + """ + parameter_values = parameter_values or {} + # if self.all_inclusive: + # # Returning a 0d value here makes things much faster. + # # I'm not sure if it's safe, though. If it causes a bug someday, + # # I guess it wasn't. + # # Even if there is no bug because of the shape, it is incorrect for + # # `contains` to return True when there are invalid (e.g. NaN) + # # parameters. + # return np.asarray(True) + + a, b = self.get_numerical_endpoints(parameter_values) + left_inclusive, right_inclusive = self.inclusive + + in_left = item >= a if left_inclusive else item > a + in_right = item <= b if right_inclusive else item < b + return in_left & in_right + + +class _RealDomain(_SimpleDomain): + r""" Represents a simply-connected subset of the real line. + + Completes the implementation of the `_SimpleDomain` class for simple + domains on the real line. + + Methods + ------- + define_parameters(*parameters) + (Inherited) Records any parameters used to define the endpoints of the + domain. + get_numerical_endpoints(parameter_values) + (Inherited) Gets the numerical values of the domain endpoints, which + may have been defined symbolically. + contains(item, parameter_values) + (Inherited) Determines whether the argument is contained within the + domain + __str__() + Returns a string representation of the domain, e.g. "[a, b)". + draw(size, rng, proportions, parameter_values) + Draws random values based on the domain. Proportions of values within + the domain, on the endpoints of the domain, outside the domain, + and having value NaN are specified by `proportions`. + + """ + + def __str__(self): + a, b = self.endpoints + left_inclusive, right_inclusive = self.inclusive + + left = "[" if left_inclusive else "(" + a = self.symbols.get(a, f"{a}") + right = "]" if right_inclusive else ")" + b = self.symbols.get(b, f"{b}") + + return f"{left}{a}, {b}{right}" + + def draw(self, size=None, rng=None, proportions=None, parameter_values=None): + r""" Draw random values from the domain. + + Parameters + ---------- + size : tuple of ints + The shape of the array of valid values to be drawn. + rng : np.Generator + The Generator used for drawing random values. + proportions : tuple of numbers + A tuple of four non-negative numbers that indicate the expected + relative proportion of elements that: + + - are strictly within the domain, + - are at one of the two endpoints, + - are strictly outside the domain, and + - are NaN, + + respectively. Default is (1, 0, 0, 0). The number of elements in + each category is drawn from the multinomial distribution with + `np.prod(size)` as the number of trials and `proportions` as the + event probabilities. The values in `proportions` are automatically + normalized to sum to 1. + parameter_values : dict + Map between the names of parameters (that define the endpoints) + and numerical values (arrays). + + """ + parameter_values = parameter_values or {} + rng = rng or np.random.default_rng() + proportions = (1, 0, 0, 0) if proportions is None else proportions + pvals = np.abs(proportions)/np.sum(proportions) + + a, b = self.get_numerical_endpoints(parameter_values) + a, b = np.broadcast_arrays(a, b) + min = np.maximum(a, _fiinfo(a).min/10) if np.any(np.isinf(a)) else a + max = np.minimum(b, _fiinfo(b).max/10) if np.any(np.isinf(b)) else b + + base_shape = min.shape + extended_shape = np.broadcast_shapes(size, base_shape) + n_extended = np.prod(extended_shape) + n_base = np.prod(base_shape) + n = int(n_extended / n_base) if n_extended else 0 + + n_in, n_on, n_out, n_nan = rng.multinomial(n, pvals) + + # `min` and `max` can have singleton dimensions that correspond with + # non-singleton dimensions in `size`. We need to be careful to avoid + # shuffling results (e.g. a value that was generated for the domain + # [min[i], max[i]] ends up at index j). To avoid this: + # - Squeeze the singleton dimensions out of `min`/`max`. Squeezing is + # often not the right thing to do, but here is equivalent to moving + # all the dimensions that are singleton in `min`/`max` (which may be + # non-singleton in the result) to the left. This is what we want. + # - Now all the non-singleton dimensions of the result are on the left. + # Ravel them to a single dimension of length `n`, which is now along + # the 0th axis. + # - Reshape the 0th axis back to the required dimensions, and move + # these axes back to their original places. + base_shape_padded = ((1,)*(len(extended_shape) - len(base_shape)) + + base_shape) + base_singletons = np.where(np.asarray(base_shape_padded)==1)[0] + new_base_singletons = tuple(range(len(base_singletons))) + # Base singleton dimensions are going to get expanded to these lengths + shape_expansion = np.asarray(extended_shape)[base_singletons] + + # assert(np.prod(shape_expansion) == n) # check understanding + # min = np.reshape(min, base_shape_padded) + # max = np.reshape(max, base_shape_padded) + # min = np.moveaxis(min, base_singletons, new_base_singletons) + # max = np.moveaxis(max, base_singletons, new_base_singletons) + # squeezed_base_shape = max.shape[len(base_singletons):] + # assert np.all(min.reshape(squeezed_base_shape) == min.squeeze()) + # assert np.all(max.reshape(squeezed_base_shape) == max.squeeze()) + + min = min.squeeze() + max = max.squeeze() + squeezed_base_shape = max.shape + + # get copies of min and max with no nans so that uniform doesn't fail + min_nn, max_nn = min.copy(), max.copy() + i = np.isnan(min_nn) | np.isnan(max_nn) + min_nn[i] = 0 + max_nn[i] = 1 + z_in = rng.uniform(min_nn, max_nn, size=(n_in,) + squeezed_base_shape) + + z_on_shape = (n_on,) + squeezed_base_shape + z_on = np.ones(z_on_shape) + i = rng.random(size=n_on) < 0.5 + z_on[i] = min + z_on[~i] = max + + z_out = rng.uniform(min_nn-10, max_nn+10, + size=(n_out,) + squeezed_base_shape) + + z_nan = np.full((n_nan,) + squeezed_base_shape, np.nan) + + z = np.concatenate((z_in, z_on, z_out, z_nan), axis=0) + z = rng.permuted(z, axis=0) + + z = np.reshape(z, tuple(shape_expansion) + squeezed_base_shape) + z = np.moveaxis(z, new_base_singletons, base_singletons) + return z + + +class _IntegerDomain(_SimpleDomain): + r""" Representation of a domain of consecutive integers. + + Completes the implementation of the `_SimpleDomain` class for domains + composed of consecutive integer values. + + To be completed when needed. + """ + pass + + +class _Parameter(ABC): + r""" Representation of a distribution parameter or variable. + + A `_Parameter` object is responsible for storing information about a + parameter or variable, providing input validation/standardization of + values passed for that parameter, providing a text/mathematical + representation of the parameter for the documentation (`__str__`), and + drawing random values of itself for testing and benchmarking. It does + not provide a complete implementation of this functionality and is meant + for subclassing. + + Attributes + ---------- + name : str + The keyword used to pass numerical values of the parameter into the + initializer of the distribution + symbol : str + The text representation of the variable in the documentation. May + include LaTeX. + domain : _Domain + The domain of the parameter for which the distribution is valid. + typical : 2-tuple of floats or strings (consider making a _Domain) + Defines the endpoints of a typical range of values of the parameter. + Used for sampling. + + Methods + ------- + __str__(): + Returns a string description of the variable for use in documentation, + including the keyword used to represent it in code, the symbol used to + represent it mathemtatically, and a description of the valid domain. + draw(size, *, rng, domain, proportions) + Draws random values of the parameter. Proportions of values within + the valid domain, on the endpoints of the domain, outside the domain, + and having value NaN are specified by `proportions`. + validate(x): + Validates and standardizes the argument for use as numerical values + of the parameter. + + """ + def __init__(self, name, *, domain, symbol=None, typical=None): + self.name = name + self.symbol = symbol or name + self.domain = domain + if typical is not None and not isinstance(typical, _Domain): + typical = _RealDomain(typical) + self.typical = typical or domain + + def __str__(self): + r""" String representation of the parameter for use in documentation.""" + return f"`{self.name}` for :math:`{self.symbol} ∈ {str(self.domain)}`" + + def draw(self, size=None, *, rng=None, domain='typical', proportions=None, + parameter_values=None): + r""" Draw random values of the parameter for use in testing. + + Parameters + ---------- + size : tuple of ints + The shape of the array of valid values to be drawn. + rng : np.Generator + The Generator used for drawing random values. + domain : str + The domain of the `_Parameter` from which to draw. Default is + "domain" (the *full* domain); alternative is "typical". An + enhancement would give a way to interpolate between the two. + proportions : tuple of numbers + A tuple of four non-negative numbers that indicate the expected + relative proportion of elements that: + + - are strictly within the domain, + - are at one of the two endpoints, + - are strictly outside the domain, and + - are NaN, + + respectively. Default is (1, 0, 0, 0). The number of elements in + each category is drawn from the multinomial distribution with + `np.prod(size)` as the number of trials and `proportions` as the + event probabilities. The values in `proportions` are automatically + normalized to sum to 1. + parameter_values : dict + Map between the names of parameters (that define the endpoints of + `typical`) and numerical values (arrays). + + """ + parameter_values = parameter_values or {} + domain = getattr(self, domain) + proportions = (1, 0, 0, 0) if proportions is None else proportions + + return domain.draw(size=size, rng=rng, proportions=proportions, + parameter_values=parameter_values) + + @abstractmethod + def validate(self, arr): + raise NotImplementedError() + + +class _RealParameter(_Parameter): + r""" Represents a real-valued parameter. + + Implements the remaining methods of _Parameter for real parameters. + All attributes are inherited. + + """ + def validate(self, arr, parameter_values): + r""" Input validation/standardization of numerical values of a parameter. + + Checks whether elements of the argument `arr` are reals, ensuring that + the dtype reflects this. Also produces a logical array that indicates + which elements meet the requirements. + + Parameters + ---------- + arr : ndarray + The argument array to be validated and standardized. + parameter_values : dict + Map of parameter names to parameter value arrays. + + Returns + ------- + arr : ndarray + The argument array that has been validated and standardized + (converted to an appropriate dtype, if necessary). + dtype : NumPy dtype + The appropriate floating point dtype of the parameter. + valid : boolean ndarray + Logical array indicating which elements are valid (True) and + which are not (False). The arrays of all distribution parameters + will be broadcasted, and elements for which any parameter value + does not meet the requirements will be replaced with NaN. + + """ + arr = np.asarray(arr) + + valid_dtype = None + # minor optimization - fast track the most common types to avoid + # overhead of np.issubdtype. Checking for `in {...}` doesn't work : / + if arr.dtype == np.float64 or arr.dtype == np.float32: + pass + elif arr.dtype == np.int32 or arr.dtype == np.int64: + arr = np.asarray(arr, dtype=np.float64) + elif np.issubdtype(arr.dtype, np.floating): + pass + elif np.issubdtype(arr.dtype, np.integer): + arr = np.asarray(arr, dtype=np.float64) + elif np.issubdtype(arr.dtype, np.complexfloating): + real_arr = np.real(arr) + valid_dtype = (real_arr == arr) + arr = real_arr + else: + message = f"Parameter `{self.name}` must be of real dtype." + raise ValueError(message) + + valid = self.domain.contains(arr, parameter_values) + valid = valid & valid_dtype if valid_dtype is not None else valid + + return arr[()], arr.dtype, valid + + +class _Parameterization: + r""" Represents a parameterization of a distribution. + + Distributions can have multiple parameterizations. A `_Parameterization` + object is responsible for recording the parameters used by the + parameterization, checking whether keyword arguments passed to the + distribution match the parameterization, and performing input validation + of the numerical values of these parameters. + + Attributes + ---------- + parameters : dict + String names (of keyword arguments) and the corresponding _Parameters. + + Methods + ------- + __len__() + Returns the number of parameters in the parameterization. + __str__() + Returns a string representation of the parameterization. + copy + Returns a copy of the parameterization. This is needed for transformed + distributions that add parameters to the parameterization. + matches(parameters) + Checks whether the keyword arguments match the parameterization. + validation(parameter_values) + Input validation / standardization of parameterization. Validates the + numerical values of all parameters. + draw(sizes, rng, proportions) + Draw random values of all parameters of the parameterization for use + in testing. + """ + def __init__(self, *parameters): + self.parameters = {param.name: param for param in parameters} + + def __len__(self): + return len(self.parameters) + + def copy(self): + return _Parameterization(*self.parameters.values()) + + def matches(self, parameters): + r""" Checks whether the keyword arguments match the parameterization. + + Parameters + ---------- + parameters : set + Set of names of parameters passed into the distribution as keyword + arguments. + + Returns + ------- + out : bool + True if the keyword arguments names match the names of the + parameters of this parameterization. + """ + return parameters == set(self.parameters.keys()) + + def validation(self, parameter_values): + r""" Input validation / standardization of parameterization. + + Parameters + ---------- + parameter_values : dict + The keyword arguments passed as parameter values to the + distribution. + + Returns + ------- + all_valid : ndarray + Logical array indicating the elements of the broadcasted arrays + for which all parameter values are valid. + dtype : dtype + The common dtype of the parameter arrays. This will determine + the dtype of the output of distribution methods. + """ + all_valid = True + dtypes = set() # avoid np.result_type if there's only one type + for name, arr in parameter_values.items(): + parameter = self.parameters[name] + arr, dtype, valid = parameter.validate(arr, parameter_values) + dtypes.add(dtype) + all_valid = all_valid & valid + parameter_values[name] = arr + dtype = arr.dtype if len(dtypes)==1 else np.result_type(*list(dtypes)) + + return all_valid, dtype + + def __str__(self): + r"""Returns a string representation of the parameterization.""" + messages = [str(param) for name, param in self.parameters.items()] + return ", ".join(messages) + + def draw(self, sizes=None, rng=None, proportions=None): + r"""Draw random values of all parameters for use in testing. + + Parameters + ---------- + sizes : iterable of shape tuples + The size of the array to be generated for each parameter in the + parameterization. Note that the order of sizes is arbitary; the + size of the array generated for a specific parameter is not + controlled individually as written. + rng : NumPy Generator + The generator used to draw random values. + proportions : tuple + A tuple of four non-negative numbers that indicate the expected + relative proportion of elements that are within the parameter's + domain, are on the boundary of the parameter's domain, are outside + the parameter's domain, and have value NaN. For more information, + see the `draw` method of the _Parameter subclasses. + + Returns + ------- + parameter_values : dict (string: array) + A dictionary of parameter name/value pairs. + """ + # ENH: be smart about the order. The domains of some parameters + # depend on others. If the relationshp is simple (e.g. a < b < c), + # we can draw values in order a, b, c. + parameter_values = {} + + if not len(sizes) or not np.iterable(sizes[0]): + sizes = [sizes]*len(self.parameters) + + for size, param in zip(sizes, self.parameters.values()): + parameter_values[param.name] = param.draw( + size, rng=rng, proportions=proportions, + parameter_values=parameter_values) + + return parameter_values + + +def _set_invalid_nan(f): + # Wrapper for input / output validation and standardization of distribution + # functions that accept either the quantile or percentile as an argument: + # logpdf, pdf + # logcdf, cdf + # logccdf, ccdf + # ilogcdf, icdf + # ilogccdf, iccdf + # Arguments that are outside the required range are replaced by NaN before + # passing them into the underlying function. The corresponding outputs + # are replaced by the appropriate value before being returned to the user. + # For example, when the argument of `cdf` exceeds the right end of the + # distribution's support, the wrapper replaces the argument with NaN, + # ignores the output of the underlying function, and returns 1.0. It also + # ensures that output is of the appropriate shape and dtype. + + endpoints = {'icdf': (0, 1), 'iccdf': (0, 1), + 'ilogcdf': (-np.inf, 0), 'ilogccdf': (-np.inf, 0)} + replacements = {'logpdf': (-oo, -oo), 'pdf': (0, 0), + '_logcdf1': (-oo, 0), '_logccdf1': (0, -oo), + '_cdf1': (0, 1), '_ccdf1': (1, 0)} + replace_strict = {'pdf', 'logpdf'} + replace_exact = {'icdf', 'iccdf', 'ilogcdf', 'ilogccdf'} + clip = {'_cdf1', '_ccdf1'} + clip_log = {'_logcdf1', '_logccdf1'} + + @functools.wraps(f) + def filtered(self, x, *args, **kwargs): + if self.iv_policy == _SKIP_ALL: + return f(self, x, *args, **kwargs) + + method_name = f.__name__ + x = np.asarray(x) + dtype = self._dtype + shape = self._shape + + # Ensure that argument is at least as precise as distribution + # parameters, which are already at least floats. This will avoid issues + # with raising integers to negative integer powers and failure to replace + # invalid integers with NaNs. + if x.dtype != dtype: + dtype = np.result_type(x.dtype, dtype) + x = np.asarray(x, dtype=dtype) + + # Broadcasting is slow. Do it only if necessary. + if not x.shape == shape: + try: + shape = np.broadcast_shapes(x.shape, shape) + x = np.broadcast_to(x, shape) + # Should we broadcast the distribution parameters to match shape of x? + # Should we copy if we broadcast to avoid passing a view to developer + # functions? + except ValueError as e: + message = ( + f"The argument provided to `{self.__class__.__name__}" + f".{method_name}` cannot be be broadcast to the same " + "shape as the distribution parameters.") + raise ValueError(message) from e + + low, high = endpoints.get(method_name, self.support()) + + # Check for arguments outside of domain. They'll be replaced with NaNs, + # and the result will be set to the appropriate value. + left_inc, right_inc = self._variable.domain.inclusive + mask_low = (x < low if (method_name in replace_strict and left_inc) + else x <= low) + mask_high = (x > high if (method_name in replace_strict and right_inc) + else x >= high) + mask_invalid = (mask_low | mask_high) + any_invalid = (mask_invalid if mask_invalid.shape == () + else np.any(mask_invalid)) + + # Check for arguments at domain endpoints, whether they + # are part of the domain or not. + any_endpoint = False + if method_name in replace_exact: + mask_low_endpoint = (x == low) + mask_high_endpoint = (x == high) + mask_endpoint = (mask_low_endpoint | mask_high_endpoint) + any_endpoint = (mask_endpoint if mask_endpoint.shape == () + else np.any(mask_endpoint)) + + # Set out-of-domain arguments to NaN. The result will be set to the + # appropriate value later. + if any_invalid: + x = np.array(x, dtype=dtype, copy=True) + x[mask_invalid] = np.nan + + res = np.asarray(f(self, x, *args, **kwargs)) + + # Ensure that the result is the correct dtype and shape, + # copying (only once) if necessary. + res_needs_copy = False + if res.dtype != dtype: + dtype = np.result_type(dtype, self._dtype) + res_needs_copy = True + + if res.shape != shape: # faster to check first + res = np.broadcast_to(res, self._shape) + res_needs_copy = res_needs_copy or any_invalid or any_endpoint + + if res_needs_copy: + res = np.array(res, dtype=dtype, copy=True) + + # For arguments outside the function domain, replace results + if any_invalid: + replace_low, replace_high = ( + replacements.get(method_name, (np.nan, np.nan))) + res[mask_low] = replace_low + res[mask_high] = replace_high + + # For arguments at the endpoints of the domain, replace results + if any_endpoint: + a, b = self.support() + if a.shape != shape: + a = np.array(np.broadcast_to(a, shape), copy=True) + b = np.array(np.broadcast_to(b, shape), copy=True) + + replace_low_endpoint = ( + b[mask_low_endpoint] if method_name.endswith('ccdf') + else a[mask_low_endpoint]) + replace_high_endpoint = ( + a[mask_high_endpoint] if method_name.endswith('ccdf') + else b[mask_high_endpoint]) + + res[mask_low_endpoint] = replace_low_endpoint + res[mask_high_endpoint] = replace_high_endpoint + + # Clip probabilities to [0, 1] + if method_name in clip: + res = np.clip(res, 0., 1.) + elif method_name in clip_log: + res = res.real # exp(res) > 0 + res = np.clip(res, None, 0.) # exp(res) < 1 + + return res[()] + + return filtered + + +def _set_invalid_nan_property(f): + # Wrapper for input / output validation and standardization of distribution + # functions that represent properties of the distribution itself: + # logentropy, entropy + # median, mode + # moment + # It ensures that the output is of the correct shape and dtype and that + # there are NaNs wherever the distribution parameters were invalid. + + @functools.wraps(f) + def filtered(self, *args, **kwargs): + if self.iv_policy == _SKIP_ALL: + return f(self, *args, **kwargs) + + res = f(self, *args, **kwargs) + if res is None: + # message could be more appropriate + raise NotImplementedError(self._not_implemented) + + res = np.asarray(res) + needs_copy = False + dtype = res.dtype + + if dtype != self._dtype: # this won't work for logmoments (complex) + dtype = np.result_type(dtype, self._dtype) + needs_copy = True + + if res.shape != self._shape: # faster to check first + res = np.broadcast_to(res, self._shape) + needs_copy = needs_copy or self._any_invalid + + if needs_copy: + res = res.astype(dtype=dtype, copy=True) + + if self._any_invalid: + # may be redundant when quadrature is used, but not necessarily + # when formulas are used. + res[self._invalid] = np.nan + + return res[()] + + return filtered + + +def _dispatch(f): + # For each public method (instance function) of a distribution (e.g. ccdf), + # there may be several ways ("method"s) that it can be computed (e.g. a + # formula, as the complement of the CDF, or via numerical integration). + # Each "method" is implemented by a different private method (instance + # function). + # This wrapper calls the appropriate private method based on the public + # method and any specified `method` keyword option. + # - If `method` is specified as a string (by the user), the appropriate + # private method is called. + # - If `method` is None: + # - The appropriate private method for the public method is looked up + # in a cache. + # - If the cache does not have an entry for the public method, the + # appropriate "dispatch " function is called to determine which method + # is most appropriate given the available private methods and + # settings (e.g. tolerance). + + @functools.wraps(f) + def wrapped(self, *args, method=None, **kwargs): + func_name = f.__name__ + method = method or self._method_cache.get(func_name, None) + if callable(method): + pass + elif method is not None: + method = 'logexp' if method == 'log/exp' else method + method_name = func_name.replace('dispatch', method) + method = getattr(self, method_name) + else: + method = f(self, *args, method=method, **kwargs) + if self.cache_policy != _NO_CACHE: + self._method_cache[func_name] = method + + try: + return method(*args, **kwargs) + except KeyError as e: + raise NotImplementedError(self._not_implemented) from e + + return wrapped + + +def _cdf2_input_validation(f): + # Wrapper that does the job of `_set_invalid_nan` when `cdf` or `logcdf` + # is called with two quantile arguments. + # Let's keep it simple; no special cases for speed right now. + # The strategy is a bit different than for 1-arg `cdf` (and other methods + # covered by `_set_invalid_nan`). For 1-arg `cdf`, elements of `x` that + # are outside (or at the edge of) the support get replaced by `nan`, + # and then the results get replaced by the appropriate value (0 or 1). + # We *could* do something similar, dispatching to `_cdf1` in these + # cases. That would be a bit more robust, but it would also be quite + # a bit more complex, since we'd have to do different things when + # `x` and `y` are both out of bounds, when just `x` is out of bounds, + # when just `y` is out of bounds, and when both are out of bounds. + # I'm not going to do that right now. Instead, simply replace values + # outside the support by those at the edge of the support. Here, we also + # omit some of the optimizations that make `_set_invalid_nan` faster for + # simple arguments (e.g. float64 scalars). + + @functools.wraps(f) + def wrapped(self, x, y, *args, **kwargs): + func_name = f.__name__ + + low, high = self.support() + x, y, low, high = np.broadcast_arrays(x, y, low, high) + dtype = np.result_type(x.dtype, y.dtype, self._dtype) + # yes, copy to avoid modifying input arrays + x, y = x.astype(dtype, copy=True), y.astype(dtype, copy=True) + + # Swap arguments to ensure that x < y, and replace + # out-of domain arguments with domain endpoints. We'll + # transform the result later. + i_swap = y < x + x[i_swap], y[i_swap] = y[i_swap], x[i_swap] + i = x < low + x[i] = low[i] + i = y < low + y[i] = low[i] + i = x > high + x[i] = high[i] + i = y > high + y[i] = high[i] + + res = f(self, x, y, *args, **kwargs) + + # Clipping probability to [0, 1] + if func_name in {'_cdf2', '_ccdf2'}: + res = np.clip(res, 0., 1.) + else: + res = res.real # exp(res) > 0 + res = np.clip(res, None, 0.) # exp(res) < 1 + + # Transform the result to account for swapped argument order + res = np.asarray(res) + if func_name == '_cdf2': + res[i_swap] *= -1. + elif func_name == '_ccdf2': + res[i_swap] *= -1 + res[i_swap] += 2. + elif func_name == '_logcdf2': + res = np.asarray(res + 0j) if np.any(i_swap) else res + res[i_swap] = res[i_swap] + np.pi*1j + else: + # res[i_swap] is always positive and less than 1, so it's + # safe to ensure that the result is real + res[i_swap] = _logexpxmexpy(np.log(2), res[i_swap]).real + return res[()] + + return wrapped + + +def _fiinfo(x): + if np.issubdtype(x.dtype, np.inexact): + return np.finfo(x.dtype) + else: + return np.iinfo(x) + + +def _kwargs2args(f, args=None, kwargs=None): + # Wraps a function that accepts a primary argument `x`, secondary + # arguments `args`, and secondary keyward arguments `kwargs` such that the + # wrapper accepts only `x` and `args`. The keyword arguments are extracted + # from `args` passed into the wrapper, and these are passed to the + # underlying function as `kwargs`. + # This is a temporary workaround until the scalar algorithms `_tanhsinh`, + # `_chandrupatla`, etc., support `kwargs` or can operate with compressing + # arguments to the callable. + args = args or [] + kwargs = kwargs or {} + names = list(kwargs.keys()) + n_args = len(args) + + def wrapped(x, *args): + return f(x, *args[:n_args], **dict(zip(names, args[n_args:]))) + + args = list(args) + list(kwargs.values()) + + return wrapped, args + + +def _log1mexp(x): + r"""Compute the log of the complement of the exponential. + + This function is equivalent to:: + + log1mexp(x) = np.log(1-np.exp(x)) + + but avoids loss of precision when ``np.exp(x)`` is nearly 0 or 1. + + Parameters + ---------- + x : array_like + Input array. + + Returns + ------- + y : ndarray + An array of the same shape as `x`. + + Examples + -------- + >>> import numpy as np + >>> from scipy.special import log1m + >>> x = 1e-300 # log of a number very close to 1 + >>> _log1mexp(x) # log of the complement of a number very close to 1 + -690.7755278982137 + >>> # p.log(1 - np.exp(x)) # -inf; emits warning + + """ + def f1(x): + # good for exp(x) close to 0 + return np.log1p(-np.exp(x)) + + def f2(x): + # good for exp(x) close to 1 + return np.real(np.log(-special.expm1(x + 0j))) + + return _lazywhere(x < -1, (x,), f=f1, f2=f2)[()] + + +def _logexpxmexpy(x, y): + """ Compute the log of the difference of the exponentials of two arguments. + + Avoids over/underflow, but does not prevent loss of precision otherwise. + """ + # TODO: properly avoid NaN when y is negative infinity + # TODO: silence warning with taking log of complex nan + # TODO: deal with x == y better + i = np.isneginf(np.real(y)) + if np.any(i): + y = y.copy() + y[i] = np.finfo(y.dtype).min + x, y = np.broadcast_arrays(x, y) + res = np.asarray(special.logsumexp([x, y+np.pi*1j], axis=0)) + i = (x == y) + res[i] = -np.inf + return res + + +def _log_real_standardize(x): + """" Standardizes the (complex) logarithm of a real number. + + The logarithm of a real number may be represented by a complex number with + imaginary part that is a multiple of pi*1j. Even multiples correspond with + a positive real and odd multiples correspond with a negative real. + + Given a logarithm of a real number `x`, this function returns an equivalent + representation in a standard form: the log of a positive real has imaginary + part `0` and the log of a negative real has imaginary part `pi`. + + """ + shape = x.shape + x = np.atleast_1d(x) + real = np.real(x).astype(x.dtype) + complex = np.imag(x) + y = real + negative = np.exp(complex*1j) < 0.5 + y[negative] = y[negative] + np.pi * 1j + return y.reshape(shape)[()] + + +def _combine_docs(dist_family): + fields = set(NumpyDocString.sections) + fields.remove('index') + + doc = ClassDoc(dist_family) + superdoc = ClassDoc(ContinuousDistribution) + for field in fields: + if field in {"Methods", "Attributes"}: + doc[field] = superdoc[field] + elif field in {"Summary"}: + pass + elif field == "Extended Summary": + doc[field].append(_generate_domain_support(dist_family)) + elif field == 'Examples': + doc[field] = [_generate_example(dist_family)] + else: + doc[field] += superdoc[field] + return str(doc) + + +def _generate_domain_support(dist_family): + n_parameterizations = len(dist_family._parameterizations) + + domain = f"\nfor :math:`x` in {dist_family._variable.domain}.\n" + + if n_parameterizations == 0: + support = """ + This class accepts no distribution parameters. + """ + elif n_parameterizations == 1: + support = f""" + This class accepts one parameterization: + {str(dist_family._parameterizations[0])}. + """ + else: + number = {2: 'two', 3: 'three', 4: 'four', 5: 'five'}[ + n_parameterizations] + parameterizations = [f"- {str(p)}" for p in + dist_family._parameterizations] + parameterizations = "\n".join(parameterizations) + support = f""" + This class accepts {number} parameterizations: + + {parameterizations} + """ + support = "\n".join([line.lstrip() for line in support.split("\n")][1:]) + return domain + support + + +def _generate_example(dist_family): + n_parameters = dist_family._num_parameters(0) + shapes = [()] * n_parameters + rng = np.random.default_rng(615681484984984) + i = 0 + dist = dist_family._draw(shapes, rng=rng, i_parameterization=i) + + rng = np.random.default_rng(2354873452) + name = dist_family.__name__ + if n_parameters: + parameter_names = list(dist._parameterizations[i].parameters) + parameter_values = [round(getattr(dist, name), 2) for name in + parameter_names] + name_values = [f"{name}={value}" for name, value in + zip(parameter_names, parameter_values)] + instantiation = f"{name}({', '.join(name_values)})" + attributes = ", ".join([f"X.{param}" for param in dist._parameters]) + X = dist_family(**dict(zip(parameter_names, parameter_values))) + else: + instantiation = f"{name}()" + X = dist + + p = 0.32 + x = round(X.icdf(p), 2) + y = round(X.icdf(2 * p), 2) + + example = f""" + To use the distribution class, it must be instantiated using keyword + parameters corresponding with one of the accepted parameterizations. + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from scipy import stats + >>> from scipy.stats import {name} + >>> X = {instantiation} + + For convenience, the ``plot`` method can be used to visualize the density + and other functions of the distribution. + + >>> X.plot() + >>> plt.show() + + The support of the underlying distribution is available using the ``support`` + method. + + >>> X.support() + {X.support()} + """ + + if n_parameters: + example += f""" + The numerical values of parameters associated with all parameterizations + are available as attributes. + + >>> {attributes} + {tuple(X._parameters.values())} + """ + + example += f""" + To evaluate the probability density function of the underlying distribution + at argument ``x={x}``: + + >>> x = {x} + >>> X.pdf(x) + {X.pdf(x)} + + The cumulative distribution function, its complement, and the logarithm + of these functions are evaluated similarly. + + >>> np.allclose(np.exp(X.logccdf(x)), 1 - X.cdf(x)) + True + + The inverse of these functions with respect to the argument ``x`` is also + available. + + >>> logp = np.log(1 - X.ccdf(x)) + >>> np.allclose(X.ilogcdf(logp), x) + True + + Note that distribution functions and their logarithms also have two-argument + versions for working with the probability mass between two arguments. The + result tends to be more accurate than the naive implementation because it avoids + subtractive cancellation. + + >>> y = {y} + >>> np.allclose(X.ccdf(x, y), 1 - (X.cdf(y) - X.cdf(x))) + True + + There are methods for computing measures of central tendency, + dispersion, higher moments, and entropy. + + >>> X.mean(), X.median(), X.mode() + {X.mean(), X.median(), X.mode()} + >>> X.variance(), X.standard_deviation() + {X.variance(), X.standard_deviation()} + >>> X.skewness(), X.kurtosis() + {X.skewness(), X.kurtosis()} + >>> np.allclose(X.moment(order=6, kind='standardized'), + ... X.moment(order=6, kind='central') / X.variance()**3) + True + >>> np.allclose(np.exp(X.logentropy()), X.entropy()) + True + + Pseudo-random and quasi-Monte Carlo samples can be drawn from + the underlying distribution using ``sample``. + + >>> rng = np.random.default_rng(2354873452) + >>> X.sample(shape=(4,), rng=rng) + {repr(X.sample(shape=(4,), rng=rng))} + >>> n = 200 + >>> s = X.sample(shape=(n,), rng=rng, qmc_engine=stats.qmc.Halton) + >>> assert np.count_nonzero(s < X.median()) == n/2 + """ + # remove the indentation due to use of block quote within function; + # eliminate blank first line + example = "\n".join([line.lstrip() for line in example.split("\n")][1:]) + return example + + +class ContinuousDistribution: + r""" Class that represents a continuous statistical distribution. + + Instances of the class represent a random variable. + + Parameters + ---------- + tol : positive float, optional + The desired relative tolerance of calculations. Left unspecified, + calculations may be faster; when provided, calculations may be + more likely to meet the desired accuracy. + iv_policy : {None, "skip_all"} + Specifies the level of input validation to perform. Left unspecified, + input validation is performed to ensure appropriate behavior in edge + case (e.g. parameters out of domain, argument outside of distribution + support, etc.) and improve consistency of output dtype, shape, etc. + Pass ``'skip_all'`` to avoid the computational overhead of these + checks when rough edges are acceptable. + cache_policy : {None, "no_cache"} + Specifies the extent to which intermediate results are cached. Left + unspecified, intermediate results of some calculations (e.g. distribution + support, moments, etc.) are cached to improve performance of future + calculations. Pass ``'no_cache'`` to reduce memory reserved by the class + instance. + rng : numpy.random.Generator + Random number generator to be used by any methods that require + pseudo-random numbers (e.g. `sample`). + + Attributes + ---------- + All parameters are available as attributes. + + Methods + ------- + support + + plot + + sample + + fit + + moment + + mean + median + mode + + variance + standard_deviation + + skewness + kurtosis + + pdf + logpdf + + cdf + icdf + ccdf + iccdf + + logcdf + ilogcdf + logccdf + ilogccdf + + entropy + logentropy + + Notes + ----- + The following abbreviations are used throughout the documentation. + + - PDF: probability density function + - CDF: cumulative distribution function + - CCDF: complementary CDF + - entropy: differential entropy + - log-*F*: logarithm of *F* (e.g. log-CDF) + - inverse *F*: inverse function of *F* (e.g. inverse CDF) + + The API documentation is written to describe the API, not to serve as + a statistical reference. Effort is made to be correct at the level + required to use the functionality, not to be mathematically rigorous. + For example, continuity and differentiability may be implicitly assumed. + For precise mathematical definitions, consult your preferred mathematical + text. + + """ + _parameterizations = [] + + ### Initialization + + def __init__(self, *, tol=_null, iv_policy=None, cache_policy=None, + rng=None, **parameters): + self.tol = tol + self.iv_policy = iv_policy + self.cache_policy = cache_policy + self.rng = rng + self._not_implemented = ( + f"`{self.__class__.__name__}` does not provide an accurate " + "implementation of the required method. Consider leaving " + "`method` and `tol` unspecified to use another implementation." + ) + self._original_parameters = {} + # We may want to override the `__init__` method with parameters so + # IDEs can suggest parameter names. If there are multiple parameterizations, + # we'll need the default values of parameters to be None; this will + # filter out the parameters that were not actually specified by the user. + parameters = {key: val for key, val in parameters.items() if val is not None} + self._update_parameters(**parameters) + + def _update_parameters(self, *, iv_policy=None, **params): + r""" Update the numerical values of distribution parameters. + + Parameters + ---------- + **params : array + Desired numerical values of the distribution parameters. Any or all + of the parameters initially used to instantiate the distribution + may be modified. Parameters used in alternative parameterizations + are not accepted. + + iv_policy : str + To be documented. See Question 3 at the top. + """ + + parameters = original_parameters = self._original_parameters.copy() + parameters.update(**params) + parameterization = None + self._invalid = np.asarray(False) + self._any_invalid = False + self._shape = tuple() + self._ndim = 0 + self._size = 1 + self._dtype = np.float64 + + if (iv_policy or self.iv_policy) == _SKIP_ALL: + parameters = self._process_parameters(**parameters) + elif not len(self._parameterizations): + if parameters: + message = (f"The `{self.__class__.__name__}` distribution " + "family does not accept parameters, but parameters " + f"`{set(parameters)}` were provided.") + raise ValueError(message) + else: + # This is default behavior, which re-runs all parameter validations + # even when only a single parameter is modified. For many + # distributions, the domain of a parameter doesn't depend on other + # parameters, so parameters could safely be modified without + # re-validating all other parameters. To handle these cases more + # efficiently, we could allow the developer to override this + # behavior. + + # Currently the user can only update the original parameterization. + # Even though that parameterization is already known, + # `_identify_parameterization` is called to produce a nice error + # message if the user passes other values. To be a little more + # efficient, we could detect whether the values passed are + # consistent with the original parameterization rather than finding + # it from scratch. However, we might want other parameterizations + # to be accepted, which would require other changes, so I didn't + # optimize this. + + parameterization = self._identify_parameterization(parameters) + parameters, shape, size, ndim = self._broadcast(parameters) + parameters, invalid, any_invalid, dtype = ( + self._validate(parameterization, parameters)) + parameters = self._process_parameters(**parameters) + + self._invalid = invalid + self._any_invalid = any_invalid + self._shape = shape + self._size = size + self._ndim = ndim + self._dtype = dtype + + self.reset_cache() + self._parameters = parameters + self._parameterization = parameterization + self._original_parameters = original_parameters + + def reset_cache(self): + r""" Clear all cached values. + + To improve the speed of some calculations, the distribution's support + and moments are cached. + + This function is called automatically whenever the distribution + parameters are updated. + + """ + # We could offer finer control over what is cleared. + # For simplicity, these will still exist even if cache_policy is + # NO_CACHE; they just won't be populated. This allows caching to be + # turned on and off easily. + self._moment_raw_cache = {} + self._moment_central_cache = {} + self._moment_standardized_cache = {} + self._support_cache = None + self._method_cache = {} + self._constant_cache = None + + def _identify_parameterization(self, parameters): + # Determine whether a `parameters` dictionary matches is consistent + # with one of the parameterizations of the distribution. If so, + # return that parameterization object; if not, raise an error. + # + # I've come back to this a few times wanting to avoid this explicit + # loop. I've considered several possibilities, but they've all been a + # little unusual. For example, we could override `_eq_` so we can + # use _parameterizations.index() to retrieve the parameterization, + # or the user could put the parameterizations in a dictionary so we + # could look them up with a key (e.g. frozenset of parameter names). + # I haven't been sure enough of these approaches to implement them. + parameter_names_set = set(parameters) + + for parameterization in self._parameterizations: + if parameterization.matches(parameter_names_set): + break + else: + if not parameter_names_set: + message = (f"The `{self.__class__.__name__}` distribution " + "family requires parameters, but none were " + "provided.") + else: + parameter_names = self._get_parameter_str(parameters) + message = (f"The provided parameters `{parameter_names}` " + "do not match a supported parameterization of the " + f"`{self.__class__.__name__}` distribution family.") + raise ValueError(message) + + return parameterization + + def _broadcast(self, parameters): + # Broadcast the distribution parameters to the same shape. If the + # arrays are not broadcastable, raise a meaningful error. + # + # We always make sure that the parameters *are* the same shape + # and not just broadcastable. Users can access parameters as + # attributes, and I think they should see the arrays as the same shape. + # More importantly, arrays should be the same shape before logical + # indexing operations, which are needed in infrastructure code when + # there are invalid parameters, and may be needed in + # distribution-specific code. We don't want developers to need to + # broadcast in implementation functions. + + # It's much faster to check whether broadcasting is necessary than to + # broadcast when it's not necessary. + parameter_vals = [np.asarray(parameter) + for parameter in parameters.values()] + parameter_shapes = set(parameter.shape for parameter in parameter_vals) + if len(parameter_shapes) == 1: + return (parameters, parameter_vals[0].shape, + parameter_vals[0].size, parameter_vals[0].ndim) + + try: + parameter_vals = np.broadcast_arrays(*parameter_vals) + except ValueError as e: + parameter_names = self._get_parameter_str(parameters) + message = (f"The parameters `{parameter_names}` provided to the " + f"`{self.__class__.__name__}` distribution family " + "cannot be broadcast to the same shape.") + raise ValueError(message) from e + return (dict(zip(parameters.keys(), parameter_vals)), + parameter_vals[0].shape, + parameter_vals[0].size, + parameter_vals[0].ndim) + + def _validate(self, parameterization, parameters): + # Broadcasts distribution parameter arrays and converts them to a + # consistent dtype. Replaces invalid parameters with `np.nan`. + # Returns the validated parameters, a boolean mask indicated *which* + # elements are invalid, a boolean scalar indicating whether *any* + # are invalid (to skip special treatments if none are invalid), and + # the common dtype. + valid, dtype = parameterization.validation(parameters) + invalid = ~valid + any_invalid = invalid if invalid.shape == () else np.any(invalid) + # If necessary, make the arrays contiguous and replace invalid with NaN + if any_invalid: + for parameter_name in parameters: + parameters[parameter_name] = np.copy( + parameters[parameter_name]) + parameters[parameter_name][invalid] = np.nan + + return parameters, invalid, any_invalid, dtype + + def _process_parameters(self, **params): + r""" Process and cache distribution parameters for reuse. + + This is intended to be overridden by subclasses. It allows distribution + authors to pre-process parameters for re-use. For instance, when a user + parameterizes a LogUniform distribution with `a` and `b`, it makes + sense to calculate `log(a)` and `log(b)` because these values will be + used in almost all distribution methods. The dictionary returned by + this method is passed to all private methods that calculate functions + of the distribution. + """ + return params + + def _get_parameter_str(self, parameters): + # Get a string representation of the parameters like "{a, b, c}". + parameter_names_list = list(parameters.keys()) + parameter_names_list.sort() + return f"{{{', '.join(parameter_names_list)}}}" + + def _copy_parameterization(self): + self._parameterizations = self._parameterizations.copy() + for i in range(len(self._parameterizations)): + self._parameterizations[i] = self._parameterizations[i].copy() + + ### Attributes + + # `tol` attribute is just notional right now. See Question 4 above. + @property + def tol(self): + r"""positive float: + The desired relative tolerance of calculations. Left unspecified, + calculations may be faster; when provided, calculations may be + more likely to meet the desired accuracy. + """ + return self._tol + + @tol.setter + def tol(self, tol): + if _isnull(tol): + self._tol = tol + return + + tol = np.asarray(tol) + if (tol.shape != () or not tol > 0 or # catches NaNs + not np.issubdtype(tol.dtype, np.floating)): + message = (f"Attribute `tol` of `{self.__class__.__name__}` must " + "be a positive float, if specified.") + raise ValueError(message) + self._tol = tol[()] + + @property + def cache_policy(self): + r"""{None, "no_cache"}: + Specifies the extent to which intermediate results are cached. Left + unspecified, intermediate results of some calculations (e.g. distribution + support, moments, etc.) are cached to improve performance of future + calculations. Pass ``'no_cache'`` to reduce memory reserved by the class + instance. + """ + return self._cache_policy + + @cache_policy.setter + def cache_policy(self, cache_policy): + cache_policy = str(cache_policy).lower() if cache_policy is not None else None + cache_policies = {None, 'no_cache'} + if cache_policy not in cache_policies: + message = (f"Attribute `cache_policy` of `{self.__class__.__name__}` " + f"must be one of {cache_policies}, if specified.") + raise ValueError(message) + self._cache_policy = cache_policy + + @property + def iv_policy(self): + r"""{None, "skip_all"}: + Specifies the level of input validation to perform. Left unspecified, + input validation is performed to ensure appropriate behavior in edge + case (e.g. parameters out of domain, argument outside of distribution + support, etc.) and improve consistency of output dtype, shape, etc. + Use ``'skip_all'`` to avoid the computational overhead of these + checks when rough edges are acceptable. + """ + return self._iv_policy + + @iv_policy.setter + def iv_policy(self, iv_policy): + iv_policy = str(iv_policy).lower() if iv_policy is not None else None + iv_policies = {None, 'skip_all'} + if iv_policy not in iv_policies: + message = (f"Attribute `iv_policy` of `{self.__class__.__name__}` " + f"must be one of {iv_policies}, if specified.") + raise ValueError(message) + self._iv_policy = iv_policy + + @property + def rng(self): + r"""numpy.random.Generator + Random number generator to be used by any methods that require + pseudo-random numbers (e.g. `sample`). + """ + return self._rng + + @rng.setter + def rng(self, rng): + rng = self._validate_rng(rng) + self._rng = rng + + + def __getattr__(self, item): + # This override allows distribution parameters to be accessed as + # attributes. See Question 1 at the top. + + # This might be needed in __init__ to ensure that `_parameters` exists + # super().__setattr__('_parameters', dict()) + + # This is needed for deepcopy/pickling + if '_parameters' not in vars(self): + return super().__getattribute__(item) + + if item in self._parameters: + return self._parameters[item][()] + + return super().__getattribute__(item) + + ### Other magic methods + + def __repr__(self): + r""" Returns a string representation of the distribution. + + Includes the name of the distribution family, the names of the + parameters, and the broadcasted shape and result dtype of the + parameters. + + """ + class_name = self.__class__.__name__ + parameters = list(self._original_parameters.items()) + info = [] + if parameters: + parameters.sort() + if self._size <= 3: + str_parameters = [f"{symbol}={value}" for symbol, value in parameters] + str_parameters = f"{', '.join(str_parameters)}" + else: + str_parameters = f"{', '.join([symbol for symbol, _ in parameters])}" + info.append(str_parameters) + if self._shape: + info.append(f"shape={self._shape}") + if self._dtype != np.float64: + info.append(f"dtype={self._dtype}") + return f"{class_name}({', '.join(info)})" + + def __add__(self, loc): + return ShiftedScaledDistribution(self, loc=loc) + + def __sub__(self, loc): + return ShiftedScaledDistribution(self, loc=-self.loc) + + def __mul__(self, scale): + return ShiftedScaledDistribution(self, scale=scale) + + def __truediv__(self, scale): + return ShiftedScaledDistribution(self, scale=1/scale) + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__add__(other) + + def __rmul__(self, other): + return self.__add__(other) + + def __rtruediv__(self, other): + return self.__add__(other) + + def __neg__(self): + return self * -1 + + ### Utilities + + ## Input validation + + def _validate_rng(self, rng): + # Yet another RNG validating function. Unlike others in SciPy, if `rng + # is None`, this returns `None`. This reduces overhead (~30 µs on my + # machine) of distribution initialization by delaying a call to + # `default_rng()` until the RNG will actually be used. It also + # raises a distribution-specific error message to facilitate + # identification of the source of the error. + if self.iv_policy == _SKIP_ALL: + return rng + + if rng is not None and not isinstance(rng, np.random.Generator): + message = ( + f"Argument `rng` passed to the `{self.__class__.__name__}` " + f"distribution family is of type `{type(rng)}`, but it must " + "be a NumPy `Generator`.") + raise ValueError(message) + return rng + + def _validate_order_kind(self, order, kind, kinds): + # Yet another integer validating function. Unlike others in SciPy, it + # Is quite flexible about what is allowed as an integer, and it + # raises a distribution-specific error message to facilitate + # identification of the source of the error. + if self.iv_policy == _SKIP_ALL: + return order + + order = np.asarray(order, dtype=self._dtype)[()] + message = (f"Argument `order` of `{self.__class__.__name__}.moment` " + "must be a finite, positive integer.") + try: + order_int = round(order.item()) + # If this fails for any reason (e.g. it's an array, it's infinite) + # it's not a valid `order`. + except Exception as e: + raise ValueError(message) from e + + if order_int <0 or order_int != order: + raise ValueError(message) + + message = (f"Argument `kind` of `{self.__class__.__name__}.moment` " + f"must be one of {set(kinds)}.") + if kind.lower() not in kinds: + raise ValueError(message) + + return order + + def _preserve_type(self, x): + x = np.asarray(x) + if x.dtype != self._dtype: + x = x.astype(self._dtype) + return x[()] + + ## Testing + + @classmethod + def _draw(cls, sizes=None, rng=None, i_parameterization=None, + proportions=None): + r""" Draw a specific (fully-defined) distribution from the family. + + See _Parameterization.draw for documentation details. + """ + rng = rng or np.random.default_rng() + if len(cls._parameterizations) == 0: + return cls() + if i_parameterization is None: + n = cls._num_parameterizations() + i_parameterization = rng.integers(0, max(0, n - 1), endpoint=True) + + parameterization = cls._parameterizations[i_parameterization] + parameters = parameterization.draw(sizes, rng, proportions=proportions) + return cls(**parameters) + + @classmethod + def _num_parameterizations(cls): + # Returns the number of parameterizations accepted by the family. + return len(cls._parameterizations) + + @classmethod + def _num_parameters(cls, i_parameterization=0): + # Returns the number of parameters used in the specified + # parameterization. + return (0 if not cls._num_parameterizations() + else len(cls._parameterizations[i_parameterization])) + + ## Algorithms + + def _quadrature(self, integrand, limits=None, args=None, + params=None, log=False): + # Performs numerical integration of an integrand between limits. + # Much of this should be added to `_tanhsinh`. + a, b = self._support(**params) if limits is None else limits + a, b = np.broadcast_arrays(a, b) + if not a.size: + # maybe need to figure out result type from a, b + return np.empty(a.shape, dtype=self._dtype) + args = [] if args is None else args + params = {} if params is None else params + f, args = _kwargs2args(integrand, args=args, kwargs=params) + args = np.broadcast_arrays(*args) + # If we know the median or mean, consider breaking up the interval + rtol = None if _isnull(self.tol) else self.tol + res = _tanhsinh(f, a, b, args=args, log=log, rtol=rtol) + # For now, we ignore the status, but I want to return the error + # estimate - see question 5 at the top. + return res.integral + + def _solve_bounded(self, f, p, *, bounds=None, params=None): + # Finds the argument of a function that produces the desired output. + # Much of this should be added to _bracket_root / _chandrupatla. + xmin, xmax = self._support(**params) if bounds is None else bounds + params = {} if params is None else params + + p, xmin, xmax = np.broadcast_arrays(p, xmin, xmax) + if not p.size: + # might need to figure out result type based on p + return np.empty(p.shape, dtype=self._dtype) + + def f2(x, p, **kwargs): + return f(x, **kwargs) - p + + f3, args = _kwargs2args(f2, args=[p], kwargs=params) + # If we know the median or mean, should use it + + # Any operations between 0d array and a scalar produces a scalar, so... + shape = xmin.shape + xmin, xmax = np.atleast_1d(xmin, xmax) + + a = -np.ones_like(xmin) + b = np.ones_like(xmax) + d = xmax - xmin + + i = np.isfinite(xmin) & np.isfinite(xmax) + a[i] = xmin[i] + b[i] = xmax[i] + + i = np.isfinite(xmin) & ~np.isfinite(xmax) + a[i] = xmin[i] + b[i] = xmin[i] + 1 + + i = np.isfinite(xmax) & ~np.isfinite(xmin) + a[i] = xmax[i] - 1 + b[i] = xmax[i] + + xmin = xmin.reshape(shape) + xmax = xmax.reshape(shape) + a = a.reshape(shape) + b = b.reshape(shape) + + res = _bracket_root(f3, xl0=a, xr0=b, xmin=xmin, xmax=xmax, args=args) + # For now, we ignore the status, but I want to use the bracket width + # as an error estimate - see question 5 at the top. + xrtol = None if _isnull(self.tol) else self.tol + return _chandrupatla(f3, a=res.xl, b=res.xr, args=args, xrtol=xrtol).x + + ## Other + + def _overrides(self, method_name): + # Determines whether a class overrides a specified method. + # Returns True if the method implementation exists and is the same as + # that of the `ContinuousDistribution` class; otherwise returns False. + method = getattr(self.__class__, method_name, None) + super_method = getattr(ContinuousDistribution, method_name, None) + return method is not super_method + + ### Distribution properties + # The following "distribution properties" are exposed via a public method + # that accepts only options (not distribution parameters or quantile/ + # percentile argument). + # support + # logentropy, entropy, + # median, mode, mean, + # variance, standard_deviation + # skewness, kurtosis + # Common options are: + # method - a string that indicates which method should be used to compute + # the quantity (e.g. a formula or numerical integration). + # Input/output validation is provided by the `_set_invalid_nan_property` + # decorator. These are the methods meant to be called by users. + # + # Each public method calls a private "dispatch" method that + # determines which "method" (strategy for calculating the desired quantity) + # to use by default and, via the `@_dispatch` decorator, calls the + # method and computes the result. + # Dispatch methods always accept: + # method - as passed from the public method + # params - a dictionary of distribution shape parameters passed by + # the public method. + # Dispatch methods accept `params` rather than relying on the state of the + # object because iterative algorithms like `_tanhsinh` and `_chandrupatla` + # need their callable to follow a strict elementwise protocol: each element + # of the output is determined solely by the values of the inputs at the + # corresponding location. The public methods do not satisfy this protocol + # because they do not accept the parameters as arguments, producing an + # output that generally has a different shape than that of the input. Also, + # by calling "dispatch" methods rather than the public methods, the + # iterative algorithms avoid the overhead of input validation. + # + # Each dispatch method can designate the responsibility of computing + # the required value to any of several "implementation" methods. These + # methods accept only `**params`, the parameter dictionary passed from + # the public method via the dispatch method. We separate the implementation + # methods from the dispatch methods for the sake of simplicity (via + # compartmentalization) and to allow subclasses to override certain + # implementation methods (typically only the "formula" methods). The names + # of implementation methods are combinations of the public method name and + # the name of the "method" (strategy for calculating the desired quantity) + # string. (In fact, the name of the implementation method is calculated + # from these two strings in the `_dispatch` decorator.) Common method + # strings are: + # formula - distribution-specific analytical expressions to be implemented + # by subclasses. + # log/exp - Compute the log of a number and then exponentiate it or vice + # versa. + # quadrature - Compute the value via numerical integration. + # + # The default method (strategy) is determined based on what implementation + # methods are available and the error tolerance of the user. Typically, + # a formula is always used if available. We fall back to "log/exp" if a + # formula for the logarithm or exponential of the quantity is available, + # and we use quadrature otherwise. + + def support(self): + r"""Support of the random variable + + The support of a random variable is set of all possible outcomes; + i.e., the subset of the domain of argument :math:`x` for which + the probability density function :math:`f(x)` is nonzero. + + This function returns lower and upper bounds of the support. + + Returns + ------- + out : tuple of Array + The lower and upper bounds of the support. + + See Also + -------- + pdf + + References + ---------- + .. [1] Support (mathematics), *Wikipedia*, + https://en.wikipedia.org/wiki/Support_(mathematics) + + Notes + ----- + Suppose a continuous probability distribution has support ``(l, r)``. + The following table summarizes the value returned by methods + of ``ContinuousDistribution`` for arguments outside the support. + + +----------------+---------------------+---------------------+ + | Method | Value for ``x < l`` | Value for ``x > r`` | + +================+=====================+=====================+ + | ``pdf(x)`` | 0 | 0 | + +----------------+---------------------+---------------------+ + | ``logpdf(x)`` | -inf | -inf | + +----------------+---------------------+---------------------+ + | ``cdf(x)`` | 0 | 1 | + +----------------+---------------------+---------------------+ + | ``logcdf(x)`` | -inf | 0 | + +----------------+---------------------+---------------------+ + | ``ccdf(x)`` | 1 | 0 | + +----------------+---------------------+---------------------+ + | ``logccdf(x)`` | 0 | -inf | + +----------------+---------------------+---------------------+ + + For the ``cdf`` and related methods, the inequality need not be + strict; i.e. the tabulated value is returned when the method is + evaluated *at* the corresponding boundary. + + The following table summarizes the value returned by the inverse + methods of ``ContinuousDistribution`` for arguments at the boundaries + of the domain ``0`` to ``1``. + + +-------------+-----------+-----------+ + | Method | ``x = 0`` | ``x = 1`` | + +=============+===========+===========+ + | ``icdf(x)`` | ``l`` | ``r`` | + +-------------+-----------+-----------+ + | ``icdf(x)`` | ``r`` | ``l`` | + +-------------+-----------+-----------+ + + For the inverse log-functions, the same values are returned for + for ``x = log(0)`` and ``x = log(1)``. All inverse functions return + ``nan`` when evaluated at an argument outside the domain ``0`` to ``1``. + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Retrieve the support of the distribution: + + >>> X.support() + (-0.5, 0.5) + + For a distribution with infinite support, + + >>> X = stats.Normal() + >>> X.support() + (-inf, inf) + + Due to underflow, the numerical value returned by the PDF may be zero + even for arguments within the support, even if the true value is + nonzero. In such cases, the log-PDF may be useful. + + >>> X.pdf([-100., 100.]) + array([0., 0.]) + >>> X.logpdf([-100., 100.]) + array([-5000.91893853, -5000.91893853]) + + Use cases for the log-CDF and related methods are analogous. + + """ + # If this were a `cached_property`, we couldn't update the value + # when the distribution parameters change. + # Caching is important, though, because calls to _support take 1~2 µs + # even when `a` and `b` are already the same shape. + if self._support_cache is not None: + return self._support_cache + + a, b = self._support(**self._parameters) + if a.shape != self._shape: + a = np.broadcast_to(a, self._shape) + if b.shape != self._shape: + b = np.broadcast_to(b, self._shape) + + if self._any_invalid: + a, b = np.asarray(a).copy(), np.asarray(b).copy() + a[self._invalid], b[self._invalid] = np.nan, np.nan + a, b = a[()], b[()] + + support = (a, b) + + if self.cache_policy != _NO_CACHE: + self._support_cache = support + + return support + + def _support(self, **params): + # Computes the support given distribution parameters + a, b = self._variable.domain.get_numerical_endpoints(params) + if len(params): + # the parameters should all be of the same dtype and shape at this point + vals = list(params.values()) + shape = vals[0].shape + a = np.broadcast_to(a, shape) if a.shape != shape else a + b = np.broadcast_to(b, shape) if b.shape != shape else b + return self._preserve_type(a), self._preserve_type(b) + + @_set_invalid_nan_property + def logentropy(self, *, method=None): + r"""Logarithm of the differential entropy + + In terms of probability density function :math:`f(x)` and support + :math:`\chi`, the differential entropy (or simply "entropy") of a random + variable :math:`X` is: + + .. math:: + + h(X) = - \int_{\chi} f(x) \log f(x) dx + + `logentropy` computes the logarithm of the differential entropy + ("log-entropy"), :math:`log(h(X))`, but it may be numerically favorable + compared to the naive implementation (computing :math:`h(X)` then + taking the logarithm). + + Parameters + ---------- + method : {None, 'formula', 'logexp', 'quadrature} + The strategy used to evaluate the log-entropy. By default + (``None``), the infrastructure chooses between the following options, + listed in order of precedence. + + - ``'formula'``: use a formula for the log-entropy itself + - ``'logexp'``: evaluate the entropy and take the logarithm + - ``'quadrature'``: numerically log-integrate the logarithm of the + entropy integrand + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The log-entropy. + + See Also + -------- + entropy + logpdf + + Notes + ----- + If the entropy of a distribution is negative, then the log-entropy + is complex with imaginary part divisible by :math:`\pi`. For + consistency, the result of this function always has complex dtype, + regardless of the value of the imaginary part. + + References + ---------- + .. [1] Differential entropy, *Wikipedia*, + https://en.wikipedia.org/wiki/Differential_entropy + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-1., b=1.) + + Evaluate the log-entropy: + + >>> X.logentropy() + (-0.3665129205816642+0j) + >>> np.allclose(np.exp(X.logentropy()), X.entropy()) + True + + For a random variable with negative entropy, the log-entropy has an + imaginary part equal to `np.pi`. + + >>> X = stats.Uniform(a=-.1, b=.1) + >>> X.entropy(), X.logentropy() + (-1.6094379124341007, (0.4758849953271105+3.141592653589793j)) + + """ + return self._logentropy_dispatch(method=method, **self._parameters) + 0j + + @_dispatch + def _logentropy_dispatch(self, method=None, **params): + if self._overrides('_logentropy_formula'): + method = self._logentropy_formula + elif _isnull(self.tol) and self._overrides('_entropy_formula'): + method = self._logentropy_logexp + else: + method = self._logentropy_quadrature + return method + + def _logentropy_formula(self, **params): + raise NotImplementedError(self._not_implemented) + + def _logentropy_logexp(self, **params): + res = np.log(self._entropy_dispatch(**params)+0j) + return _log_real_standardize(res) + + def _logentropy_quadrature(self, **params): + def logintegrand(x, **params): + logpdf = self._logpdf_dispatch(x, **params) + return logpdf + np.log(0j+logpdf) + res = self._quadrature(logintegrand, params=params, log=True) + return _log_real_standardize(res + np.pi*1j) + + @_set_invalid_nan_property + def entropy(self, *, method=None): + r"""Differential entropy + + In terms of probability density function :math:`f(x)` and support + :math:`\chi`, the differential entropy (or simply "entropy") of a + continuous random variable :math:`X` is: + + .. math:: + + h(X) = - \int_{\chi} f(x) \log f(x) dx + + Parameters + ---------- + method : {None, 'formula', 'logexp', 'quadrature'} + The strategy used to evaluate the entropy. By default (``None``), + the infrastructure chooses between the following options, listed + in order of precedence. + + - ``'formula'``: use a formula for the entropy itself + - ``'logexp'``: evaluate the log-entropy and exponentiate + - ``'quadrature'``: use numerical integration + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The entropy of the random variable. + + See Also + -------- + logentropy + pdf + + Notes + ----- + This function calculates the entropy using the natural logarithm; i.e. + the logarithm with base :math:`e`. Consequently, the value is expressed + in (dimensionless) "units" of nats. To convert the entropy to different + units (i.e. corresponding with a different base), divide the result by + the natural logarithm of the desired base. + + References + ---------- + .. [1] Differential entropy, *Wikipedia*, + https://en.wikipedia.org/wiki/Differential_entropy + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Uniform(a=-1., b=1.) + + Evaluate the entropy: + + >>> X.entropy() + 0.6931471805599454 + + """ + return self._entropy_dispatch(method=method, **self._parameters) + + @_dispatch + def _entropy_dispatch(self, method=None, **params): + if self._overrides('_entropy_formula'): + method = self._entropy_formula + elif self._overrides('_logentropy_formula'): + method = self._entropy_logexp + else: + method = self._entropy_quadrature + return method + + def _entropy_formula(self, **params): + raise NotImplementedError(self._not_implemented) + + def _entropy_logexp(self, **params): + return np.real(np.exp(self._logentropy_dispatch(**params))) + + def _entropy_quadrature(self, **params): + def integrand(x, **params): + pdf = self._pdf_dispatch(x, **params) + logpdf = self._logpdf_dispatch(x, **params) + return logpdf * pdf + return -self._quadrature(integrand, params=params) + + @_set_invalid_nan_property + def median(self, *, method=None): + r"""Median (50th percentil) + + If a continuous random variable :math:`X` has probability :math:`0.5` of + taking on a value less than :math:`m`, then :math:`m` is the median. + That is, the median is the value :math:`m` for which: + + .. math:: + + P(X ≤ m) = 0.5 = P(X ≥ m) + + Parameters + ---------- + method : {None, 'formula', 'icdf'} + The strategy used to evaluate the median. + By default (``None``), the infrastructure chooses between the + following options, listed in order of precedence. + + - ``'formula'``: use a formula for the median + - ``'icdf'``: evaluate the inverse CDF of 0.5 + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The median + + See Also + -------- + mean + mode + icdf + + References + ---------- + .. [1] Median, *Wikipedia*, + https://en.wikipedia.org/wiki/Median#Probability_distributions + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Uniform(a=0., b=10.) + + Compute the median: + + >>> X.median() + 5 + >>> X.median() == X.icdf(0.5) == X.iccdf(0.5) + True + + """ + return self._median_dispatch(method=method, **self._parameters) + + @_dispatch + def _median_dispatch(self, method=None, **params): + if self._overrides('_median_formula'): + method = self._median_formula + else: + method = self._median_icdf + return method + + def _median_formula(self, **params): + raise NotImplementedError(self._not_implemented) + + def _median_icdf(self, **params): + return self._icdf_dispatch(0.5, **params) + + @_set_invalid_nan_property + def mode(self, *, method=None): + r"""Mode (most likely value) + + Informally, the mode is a value that a random variable has the highest + probability (density) of assuming. That is, the mode is the element of + the support :math:`\chi` that maximizes the probability density + function :math:`f(x)`: + + .. math:: + + \text{mode} = \arg\max_{x \in \chi} f(x) + + Parameters + ---------- + method : {None, 'formula', 'optimization'} + The strategy used to evaluate the mode. + By default (``None``), the infrastructure chooses between the + following options, listed in order of precedence. + + - ``'formula'``: use a formula for the median + - ``'optimization'``: numerically maximize the PDF + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The mode + + See Also + -------- + mean + median + pdf + + Notes + ----- + For some distributions + + #. the mode is not unique (e.g. the uniform distribution); + #. the PDF has one or more singularities, and it is debateable whether + a singularity is considered to be in the domain and called the mode + (e.g. the gamma distribution with shape parameter less than 1); and/or + #. the probability density function may have one or more local maxima + that are not a global maximum (e.g. mixture distributions). + + In such cases, `mode` will + + #. return a single value, + #. consider the mode to occur at a singularity, and/or + #. return a local maximum which may or may not be a global maximum. + + If a formula for the mode is not specifically implemented for the + chosen distribution, SciPy will attempt to compute the mode + numerically, which may not meet the user's preferred definition of a + mode. In such cases, the user is encouraged to subclass the + distribution and override ``mode``. + + References + ---------- + .. [1] Mode (statistics), *Wikipedia*, + https://en.wikipedia.org/wiki/Mode_(statistics) + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Evaluate the mode: + + >>> X.mode() + 1.0 + + If the mode is not uniquely defined, ``mode`` nonetheless returns a + single value. + + >>> X = stats.Uniform(a=0., b=1.) + >>> X.mode() + 0.5 + + If this choice does not satisfy your requirements, subclass the + distribution and override ``mode``: + + >>> class BetterUniform(stats.Uniform): + ... def mode(self): + ... return self.b + >>> X = BetterUniform(a=0., b=1.) + >>> X.mode() + 1.0 + + """ + return self._mode_dispatch(method=method, **self._parameters) + + @_dispatch + def _mode_dispatch(self, method=None, **params): + # We could add a method that looks for a critical point with + # differentiation and the root finder + if self._overrides('_mode_formula'): + method = self._mode_formula + else: + method = self._mode_optimization + return method + + def _mode_formula(self, **params): + raise NotImplementedError(self._not_implemented) + + def _mode_optimization(self, **params): + if not self._size: + return np.empty(self._shape, dtype=self._dtype) + + a, b = self._support(**params) + m = self._median_dispatch(**params) + + f, args = _kwargs2args(lambda x, **params: -self._pdf_dispatch(x, **params), + args=(), kwargs=params) + res_b = _bracket_minimum(f, m, xmin=a, xmax=b, args=args) + res = _chandrupatla_minimize(f, res_b.xl, res_b.xm, res_b.xr, args=args) + mode = np.asarray(res.x) + mode_at_boundary = res_b.status == -1 + mode_at_left = mode_at_boundary & (res_b.fl <= res_b.fm) + mode_at_right = mode_at_boundary & (res_b.fr < res_b.fm) + mode[mode_at_left] = a[mode_at_left] + mode[mode_at_right] = b[mode_at_right] + return mode[()] + + def mean(self, *, method=None): + r"""Mean (raw first moment about the origin) + + Parameters + ---------- + method : {None, 'formula', 'transform', 'quadrature', 'cache'} + Method used to calculate the raw first moment. Not + all methods are available for all distributions. See + `moment` for details. + + See Also + -------- + moment + median + mode + + References + ---------- + .. [1] Mean, *Wikipedia*, + https://en.wikipedia.org/wiki/Mean#Mean_of_a_probability_distribution + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Evaluate the variance: + + >>> X.mean() + 1.0 + >>> X.mean() == X.moment(order=1, kind='raw') == X.mu + True + + """ + return self.moment(1, kind='raw', method=method) + + def variance(self, *, method=None): + r"""Variance (central second moment) + + Parameters + ---------- + method : {None, 'formula', 'transform', 'normalize', 'quadrature', 'cache'} + Method used to calculate the central second moment. Not + all methods are available for all distributions. See + `moment` for details. + + See Also + -------- + moment + standard_deviation + mean + + References + ---------- + .. [1] Variance, *Wikipedia*, + https://en.wikipedia.org/wiki/Variance#Absolutely_continuous_random_variable + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Evaluate the variance: + + >>> X.variance() + 4.0 + >>> X.variance() == X.moment(order=2, kind='central') == X.sigma**2 + True + + """ + return self.moment(2, kind='central', method=method) + + def standard_deviation(self, *, method=None): + r"""Standard deviation (square root of the second central moment) + + Parameters + ---------- + method : {None, 'formula', 'transform', 'normalize', 'quadrature', 'cache'} + Method used to calculate the central second moment. Not + all methods are available for all distributions. See + `moment` for details. + + See Also + -------- + variance + mean + moment + + References + ---------- + .. [1] Standard deviation, *Wikipedia*, + https://en.wikipedia.org/wiki/Standard_deviation#Definition_of_population_values + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Evaluate the standard deviation: + + >>> X.standard_deviation() + 2.0 + >>> X.standard_deviation() == X.moment(order=2, kind='central')**0.5 == X.sigma + True + + """ + return np.sqrt(self.variance(method=method)) + + def skewness(self, *, method=None): + r"""Skewness (standardized third moment) + + Parameters + ---------- + method : {None, 'formula', 'general', 'transform', 'normalize', 'cache'} + Method used to calculate the standardized third moment. Not + all methods are available for all distributions. See + `moment` for details. + + See Also + -------- + moment + mean + variance + + References + ---------- + .. [1] Skewness, *Wikipedia*, + https://en.wikipedia.org/wiki/Skewness + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Evaluate the skewness: + + >>> X.skewness() + 0.0 + >>> X.skewness() == X.moment(order=3, kind='standardized') + True + + """ + return self.moment(3, kind='standardized', method=method) + + def kurtosis(self, *, method=None, convention='non-excess'): + r"""Kurtosis (standardized fourth moment) + + By default, this is the standardized fourth moment, also known as the + "non-excess" or "Pearson" kurtosis (e.g. the kurtosis of the normal + distribution is 3). The "excess" or "Fisher" kurtosis (the standardized + fourth moment minus 3) is available via the `convention` parameter. + + Parameters + ---------- + method : {None, 'formula', 'general', 'transform', 'normalize', 'cache'} + Method used to calculate the standardized fourth moment. Not + all methods are available for all distributions. See + `moment` for details. + convention : {'non-excess', 'excess'} + Two distinct conventions are available: + + - ``'non-excess'``: the standardized fourth moment (Pearson's kurtosis) + - ``'excess'``: the standardized fourth moment minus 3 (Fisher's kurtosis) + + The default is ``'non-excess'``. + + See Also + -------- + moment + mean + variance + + References + ---------- + .. [1] Kurtosis, *Wikipedia*, + https://en.wikipedia.org/wiki/Kurtosis + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Evaluate the kurtosis: + + >>> X.kurtosis() + 3.0 + >>> (X.kurtosis() + ... == X.kurtosis(convention='excess') + 3. + ... == X.moment(order=4, kind='standardized')) + True + + """ + conventions = {'non-excess', 'excess'} + message = (f'Parameter `convention` of `{self.__class__.__name__}.kurtosis` ' + f"must be one of {conventions}.") + convention = convention.lower() + if convention not in conventions: + raise ValueError(message) + k = self.moment(4, kind='standardized', method=method) + return k - 3 if convention == 'excess' else k + + ### Distribution functions + # The following functions related to the distribution PDF and CDF are + # exposed via a public method that accepts one positional argument - the + # quantile - and keyword options (but not distribution parameters). + # logpdf, pdf + # logcdf, cdf + # logccdf, ccdf + # The `logcdf` and `cdf` functions can also be called with two positional + # arguments - lower and upper quantiles - and they return the probability + # mass (integral of the PDF) between them. The 2-arg versions of `logccdf` + # and `ccdf` return the complement of this quantity. + # All the (1-arg) cumulative distribution functions have inverse + # functions, which accept one positional argument - the percentile. + # ilogcdf, icdf + # ilogccdf, iccdf + # Common keyword options include: + # method - a string that indicates which method should be used to compute + # the quantity (e.g. a formula or numerical integration). + # Tolerance options should be added. + # Input/output validation is provided by the `_set_invalid_nan` + # decorator. These are the methods meant to be called by users. + # + # Each public method calls a private "dispatch" method that + # determines which "method" (strategy for calculating the desired quantity) + # to use by default and, via the `@_dispatch` decorator, calls the + # method and computes the result. + # Each dispatch method can designate the responsibility of computing + # the required value to any of several "implementation" methods. These + # methods accept only `**params`, the parameter dictionary passed from + # the public method via the dispatch method. + # See the note corresponding with the "Distribution Parameters" for more + # information. + + ## Probability Density Functions + + @_set_invalid_nan + def logpdf(self, x, *, method=None): + r"""Log of the probability density function + + The probability density function ("PDF"), denoted :math:`f(x)`, is the + probability *per unit length* that the random variable will assume the + value :math:`x`. Mathematically, it can be defined as the derivative + of the cumulative distribution function :math:`F(x)`: + + .. math:: + + f(x) = \frac{d}{dx} F(x) + + `logpdf` computes the logarithm of the probability density function + ("log-PDF"), :math:`\log(f(x))`, but it may be numerically favorable + compared to the naive implementation (computing :math:`f(x)` and + taking the logarithm). + + `logpdf` accepts `x` for :math:`x`. + + Parameters + ---------- + x : array + The argument of the log-PDF. + method : {None, 'formula', 'logexp'} + The strategy used to evaluate the log-PDF. By default (``None``), the + infrastructure chooses between the following options, listed in order + of precedence. + + - ``'formula'``: use a formula for the log-PDF itself + - ``'logexp'``: evaluate the PDF and takes its logarithm + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The log-PDF evaluated at the argument `x`. + + See Also + -------- + pdf + logcdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. + By definition of the support, the log-PDF evaluates to its minimum value + of :math:`-\infty` (i.e. :math:`\log(0)`) outside the support; i.e. for + :math:`x < l` or :math:`x > r`. The maximum of the log-PDF may be less + than or greater than :math:`\log(1) = 0` because the maximum of the PDF + can be any positive real. + + For distributions with infinite support, it is common for `pdf` to return + a value of ``0`` when the argument is theoretically within the support; + this can occur because the true value of the PDF is too small to be + represented by the chosen dtype. The log-PDF, however, will often be finite + (not ``-inf``) over a much larger domain. Consequently, it may be preferred + to work with the logarithms of probabilities and probability densities to + avoid underflow. + + References + ---------- + .. [1] Probability density function, *Wikipedia*, + https://en.wikipedia.org/wiki/Probability_density_function + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-1.0, b=1.0) + + Evaluate the log-PDF at the desired argument: + + >>> X.logpdf(0.5) + -0.6931471805599453 + >>> np.allclose(X.logpdf(0.5), np.log(X.pdf(0.5))) + True + + """ + return self._logpdf_dispatch(x, method=method, **self._parameters) + + @_dispatch + def _logpdf_dispatch(self, x, *, method=None, **params): + if self._overrides('_logpdf_formula'): + method = self._logpdf_formula + elif _isnull(self.tol): # ensure that developers override _logpdf + method = self._logpdf_logexp + return method + + def _logpdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _logpdf_logexp(self, x, **params): + return np.log(self._pdf_dispatch(x, **params)) + + @_set_invalid_nan + def pdf(self, x, *, method=None): + r"""Probability density function + + The probability density function ("PDF"), denoted :math:`f(x)`, is the + probability *per unit length* that the random variable will assume the + value :math:`x`. Mathematically, it can be defined as the derivative + of the cumulative distribution function :math:`F(x)`: + + .. math:: + + f(x) = \frac{d}{dx} F(x) + + `pdf` accepts `x` for :math:`x`. + + Parameters + ---------- + x : array + The argument of the PDF. + method : {None, 'formula', 'logexp'} + The strategy used to evaluate the PDF. By default (``None``), the + infrastructure chooses between the following options, listed in + order of precedence. + + - ``'formula'``: use a formula for the PDF itself + - ``'logexp'``: evaluate the log-PDF and exponentiate + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The PDF evaluated at the argument `x`. + + See Also + -------- + cdf + logpdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. + By definition of the support, the PDF evaluates to its minimum value + of :math:`0` outside the support; i.e. for :math:`x < l` or + :math:`x > r`. The maximum of the PDF may be less than or greater than + :math:`1`; since the valus is a probability *density*, only its integral + over the support must equal :math:`1`. + + References + ---------- + .. [1] Probability density function, *Wikipedia*, + https://en.wikipedia.org/wiki/Probability_density_function + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Uniform(a=-1., b=1.) + + Evaluate the PDF at the desired argument: + + >>> X.pdf(0.25) + 0.5 + + """ + return self._pdf_dispatch(x, method=method, **self._parameters) + + @_dispatch + def _pdf_dispatch(self, x, *, method=None, **params): + if self._overrides('_pdf_formula'): + method = self._pdf_formula + else: + method = self._pdf_logexp + return method + + def _pdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _pdf_logexp(self, x, **params): + return np.exp(self._logpdf_dispatch(x, **params)) + + ## Cumulative Distribution Functions + + def logcdf(self, x, y=None, *, method=None): + r"""Log of the cumulative distribution function + + The cumulative distribution function ("CDF"), denoted :math:`F(x)`, is + the probability the random variable :math:`X` will assume a value + less than or equal to :math:`x`: + + .. math:: + + F(x) = P(X ≤ x) + + A two-argument variant of this function is also defined as the + probability the random variable :math:`X` will assume a value between + :math:`x` and :math:`y`. + + .. math:: + + F(x, y) = P(x ≤ X ≤ y) + + `logcdf` computes the logarithm of the cumulative distribution function + ("log-CDF"), :math:`\log(F(x))`/:math:`\log(F(x, y))`, but it may be + numerically favorable compared to the naive implementation (computing + the CDF and taking the logarithm). + + `logcdf` accepts `x` for :math:`x` and `y` for :math:`y`. + + Parameters + ---------- + x, y : array + The arguments of the log-CDF. `x` is required; `y` is optional. + method : {None, 'formula', 'logexp', 'complement', 'quadrature', 'subtraction'} + The strategy used to evaluate the log-CDF. + By default (``None``), the one-argument form of the function + chooses between the following options, listed in order of precedence. + + - ``'formula'``: use a formula for the log-CDF itself + - ``'logexp'``: evaluate the CDF and take the logarithm + - ``'complement'``: evaluate the log-CCDF and take the + logarithmic complement (see Notes) + - ``'quadrature'``: numerically log-integrate the log-PDF + + In place of ``'complement'``, the two-argument form accepts: + + - ``'subtraction'``: compute the log-CDF at each argument and take + the logarithmic difference (see Notes) + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The log-CDF evaluated at the provided argument(s). + + See Also + -------- + cdf + logccdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. + The log-CDF evaluates to its minimum value of :math:`\log(0) = -\infty` + for :math:`x ≤ l` and its maximum value of :math:`\log(1) = 0` for + :math:`x ≥ r`. + + For distributions with infinite support, it is common for + `cdf` to return a value of ``0`` when the argument + is theoretically within the support; this can occur because the true value + of the CDF is too small to be represented by the chosen dtype. `logcdf`, + however, will often return a finite (not ``-inf``) result over a much larger + domain. Similarly, `logcdf` may provided a strictly negative result with + arguments for which `cdf` would return ``1.0``. Consequently, it may be + preferred to work with the logarithms of probabilities to avoid underflow + and related limitations of floating point numbers. + + The "logarithmic complement" of a number :math:`z` is mathematically + equivalent to :math:`\log(1-\exp(z))`, but it is computed to avoid loss + of precision when :math:`\exp(z)` is nearly :math:`0` or :math:`1`. + Similarly, the term "logarithmic difference" of :math:`w` and :math:`z` + is used here to mean :math:`\log(\exp(w)-\exp(z))`. + + References + ---------- + .. [1] Cumulative distribution function, *Wikipedia*, + https://en.wikipedia.org/wiki/Cumulative_distribution_function + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the log-CDF at the desired argument: + + >>> X.logcdf(0.25) + -0.287682072451781 + >>> np.allclose(X.logcdf(0.), np.log(X.cdf(0.))) + True + + """ # noqa: E501 + if y is None: + return self._logcdf1(x, method=method) + else: + return self._logcdf2(x, y, method=method) + + @_cdf2_input_validation + def _logcdf2(self, x, y, *, method): + return self._logcdf2_dispatch(x, y, method=method, **self._parameters) + + @_dispatch + def _logcdf2_dispatch(self, x, y, *, method=None, **params): + # dtype is complex if any x > y, else real + # Should revisit this logic. + if self._overrides('_logcdf2_formula'): + method = self._logcdf2_formula + elif (self._overrides('_logcdf_formula') + or self._overrides('_logccdf_formula')): + method = self._logcdf2_subtraction + elif _isnull(self.tol) and (self._overrides('_cdf_formula') + or self._overrides('_ccdf_formula')): + method = self._logcdf2_logexp + else: + method = self._logcdf2_quadrature + return method + + def _logcdf2_formula(self, x, y, **params): + raise NotImplementedError(self._not_implemented) + + def _logcdf2_subtraction(self, x, y, **params): + flip_sign = x > y + x, y = np.minimum(x, y), np.maximum(x, y) + logcdf_x = self._logcdf_dispatch(x, **params) + logcdf_y = self._logcdf_dispatch(y, **params) + logccdf_x = self._logccdf_dispatch(x, **params) + logccdf_y = self._logccdf_dispatch(y, **params) + case_left = (logcdf_x < -1) & (logcdf_y < -1) + case_right = (logccdf_x < -1) & (logccdf_y < -1) + case_central = ~(case_left | case_right) + log_mass = _logexpxmexpy(logcdf_y, logcdf_x) + log_mass[case_right] = _logexpxmexpy(logccdf_x, logccdf_y)[case_right] + log_tail = np.logaddexp(logcdf_x, logccdf_y)[case_central] + log_mass[case_central] = _log1mexp(log_tail) + log_mass[flip_sign] += np.pi * 1j + return np.real_if_close(log_mass[()]) + + def _logcdf2_logexp(self, x, y, **params): + expres = self._cdf2_dispatch(x, y, **params) + expres = expres + 0j if np.any(x > y) else expres + return np.log(expres) + + def _logcdf2_quadrature(self, x, y, **params): + logres = self._quadrature(self._logpdf_dispatch, limits=(x, y), + log=True, params=params) + return logres + + @_set_invalid_nan + def _logcdf1(self, x, *, method=None): + return self._logcdf_dispatch(x, method=method, **self._parameters) + + @_dispatch + def _logcdf_dispatch(self, x, *, method=None, **params): + if self._overrides('_logcdf_formula'): + method = self._logcdf_formula + elif _isnull(self.tol) and self._overrides('_cdf_formula'): + method = self._logcdf_logexp + elif self._overrides('_logccdf_formula'): + method = self._logcdf_complement + else: + method = self._logcdf_quadrature + return method + + def _logcdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _logcdf_logexp(self, x, **params): + return np.log(self._cdf_dispatch(x, **params)) + + def _logcdf_complement(self, x, **params): + return _log1mexp(self._logccdf_dispatch(x, **params)) + + def _logcdf_quadrature(self, x, **params): + a, _ = self._support(**params) + return self._quadrature(self._logpdf_dispatch, limits=(a, x), + params=params, log=True) + + def cdf(self, x, y=None, *, method=None): + r"""Cumulative distribution function + + The cumulative distribution function ("CDF"), denoted :math:`F(x)`, is + the probability the random variable :math:`X` will assume a value + less than or equal to :math:`x`: + + .. math:: + + F(x) = P(X ≤ x) + + A two-argument variant of this function is also defined as the + probability the random variable :math:`X` will assume a value between + :math:`x` and :math:`y`. + + .. math:: + + F(x, y) = P(x ≤ X ≤ y) + + `cdf` accepts `x` for :math:`x` and `y` for :math:`y`. + + Parameters + ---------- + x, y : array + The arguments of the CDF. `x` is required; `y` is optional. + method : {None, 'formula', 'logexp', 'complement', 'quadrature', 'subtraction'} + The strategy used to evaluate the CDF. + By default (``None``), the one-argument form of the function + chooses between the following options, listed in order of precedence. + + - ``'formula'``: use a formula for the CDF itself + - ``'logexp'``: evaluate the log-CDF and exponentiate + - ``'complement'``: evaluate the CCDF and take the complement + - ``'quadrature'``: numerically integrate the PDF + + In place of ``'complement'``, the two-argument form accepts: + + - ``'subtraction'``: compute the CDF at each argument and take + the difference. + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The CDF evaluated at the provided argument(s). + + See Also + -------- + logcdf + ccdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. + The CDF :math:`F(x)` is related to the probability density function + :math:`f(x)` by: + + .. math:: + + F(x) = \int_l^x f(u) du + + The two argument version is: + + .. math:: + + F(x, y) = \int_x^y f(u) du = F(y) - F(x) + + The CDF evaluates to its minimum value of :math:`0` for :math:`x ≤ l` + and its maximum value of :math:`1` for :math:`x ≥ r`. + + The CDF is also known simply as the "distribution function". + + References + ---------- + .. [1] Cumulative distribution function, *Wikipedia*, + https://en.wikipedia.org/wiki/Cumulative_distribution_function + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the CDF at the desired argument: + + >>> X.cdf(0.25) + 0.75 + + Evaluate the cumulative probability between two arguments: + + >>> X.cdf(-0.25, 0.25) == X.cdf(0.25) - X.cdf(-0.25) + True + + """ # noqa: E501 + if y is None: + return self._cdf1(x, method=method) + else: + return self._cdf2(x, y, method=method) + + @_cdf2_input_validation + def _cdf2(self, x, y, *, method): + return self._cdf2_dispatch(x, y, method=method, **self._parameters) + + @_dispatch + def _cdf2_dispatch(self, x, y, *, method=None, **params): + # Should revisit this logic. + if self._overrides('_cdf2_formula'): + method = self._cdf2_formula + elif (self._overrides('_logcdf_formula') + or self._overrides('_logccdf_formula')): + method = self._cdf2_logexp + elif _isnull(self.tol) and (self._overrides('_cdf_formula') + or self._overrides('_ccdf_formula')): + method = self._cdf2_subtraction + else: + method = self._cdf2_quadrature + return method + + def _cdf2_formula(self, x, y, **params): + raise NotImplementedError(self._not_implemented) + + def _cdf2_logexp(self, x, y, **params): + return np.real(np.exp(self._logcdf2_dispatch(x, y, **params))) + + def _cdf2_subtraction(self, x, y, **params): + # Improvements: + # Lazy evaluation of cdf/ccdf only where needed + # Stack x and y to reduce function calls? + cdf_x = self._cdf_dispatch(x, **params) + cdf_y = self._cdf_dispatch(y, **params) + ccdf_x = self._ccdf_dispatch(x, **params) + ccdf_y = self._ccdf_dispatch(y, **params) + i = (cdf_x < 0.5) & (cdf_y < 0.5) + return np.where(i, cdf_y-cdf_x, ccdf_x-ccdf_y) + + def _cdf2_quadrature(self, x, y, **params): + return self._quadrature(self._pdf_dispatch, limits=(x, y), params=params) + + @_set_invalid_nan + def _cdf1(self, x, *, method): + return self._cdf_dispatch(x, method=method, **self._parameters) + + @_dispatch + def _cdf_dispatch(self, x, *, method=None, **params): + if self._overrides('_cdf_formula'): + method = self._cdf_formula + elif self._overrides('_logcdf_formula'): + method = self._cdf_logexp + elif _isnull(self.tol) and self._overrides('_ccdf_formula'): + method = self._cdf_complement + else: + method = self._cdf_quadrature + return method + + def _cdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _cdf_logexp(self, x, **params): + return np.exp(self._logcdf_dispatch(x, **params)) + + def _cdf_complement(self, x, **params): + return 1 - self._ccdf_dispatch(x, **params) + + def _cdf_quadrature(self, x, **params): + a, _ = self._support(**params) + return self._quadrature(self._pdf_dispatch, limits=(a, x), + params=params) + + def logccdf(self, x, y=None, *, method=None): + r"""Log of the complementary cumulative distribution function + + The complementary cumulative distribution function ("CCDF"), denoted + :math:`G(x)` is the complement of the cumulative distribution function + :math:`F(x)`; i.e., probability the random variable :math:`X` will + assume a value greater than :math:`x`: + + .. math:: + + G(x) = 1 - F(x) = P(X > x) + + A two-argument variant of this function is: + + .. math:: + + G(x, y) = 1 - F(x, y) = P(X < x \quad \text{or} \quad X > y) + + `logccdf` computes the logarithm of the complementary cumulative + distribution function ("log-CCDF"), :math:`\log(G(x))`/:math:`\log(G(x, y))`, + but it may be numerically favorable compared to the naive implementation + (computing the CDF and taking the logarithm). + + `logccdf` accepts `x` for :math:`x` and `y` for :math:`y`. + + Parameters + ---------- + x, y : array + The arguments of the log-CCDF. `x` is required; `y` is optional. + method : {None, 'formula', 'logexp', 'complement', 'quadrature', 'addition'} + The strategy used to evaluate the log-CCDF. + By default (``None``), the one-argument form of the function + chooses between the following options, listed in order of precedence. + + - ``'formula'``: use a formula for the log CCDF itself + - ``'logexp'``: evaluate the CCDF and take the logarithm + - ``'complement'``: evaluate the log-CDF and take the + logarithmic complement (see Notes) + - ``'quadrature'``: numerically log-integrate the log-PDF + + The two-argument form chooses between: + + - ``'formula'``: use a formula for the log CCDF itself + - ``'addition'``: compute the log-CDF at `x` and the log-CCDF at `y`, + then take the logarithmic sum (see Notes) + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The log-CCDF evaluated at the provided argument(s). + + See Also + -------- + ccdf + logcdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. + The log-CCDF returns its minimum value of :math:`\log(0)=-\infty` for + :math:`x ≥ r` and its maximum value of :math:`\log(1) = 0` for + :math:`x ≤ l`. + + For distributions with infinite support, it is common for + `ccdf` to return a value of ``0`` when the argument + is theoretically within the support; this can occur because the true value + of the CCDF is too small to be represented by the chosen dtype. The log + of the CCDF, however, will often be finite (not ``-inf``) over a much larger + domain. Similarly, `logccdf` may provided a strictly negative result with + arguments for which `ccdf` would return ``1.0``. Consequently, it may be + preferred to work with the logarithms of probabilities to avoid underflow + and related limitations of floating point numbers. + + The "logarithmic complement" of a number :math:`z` is mathematically + equivalent to :math:`\log(1-\exp(z))`, but it is computed to avoid loss + of precision when :math:`\exp(z)` is nearly :math:`0` or :math:`1`. + Similarly, the term "logarithmic sum" of :math:`w` and :math:`z` + is used here to mean the :math:`\log(\exp(w)+\exp(z))`, AKA + :math:`\text{LogSumExp}(w, z)`. + + + References + ---------- + .. [1] Cumulative distribution function, *Wikipedia*, + https://en.wikipedia.org/wiki/Cumulative_distribution_function#Derived_functions + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the log-CCDF at the desired argument: + + >>> X.logccdf(0.25) + -1.3862943611198906 + >>> np.allclose(X.logccdf(0.), np.log(X.ccdf(0.))) + True + + """ # noqa: E501 + if y is None: + return self._logccdf1(x, method=method) + else: + return self._logccdf2(x, y, method=method) + + @_cdf2_input_validation + def _logccdf2(self, x, y, *, method): + return self._logccdf2_dispatch(x, y, method=method, **self._parameters) + + @_dispatch + def _logccdf2_dispatch(self, x, y, *, method=None, **params): + # if _logccdf2_formula exists, we could use the complement + # if _ccdf2_formula exists, we could use log/exp + if self._overrides('_logccdf2_formula'): + method = self._logccdf2_formula + else: + method = self._logccdf2_addition + return method + + def _logccdf2_formula(self, x, y, **params): + raise NotImplementedError(self._not_implemented) + + def _logccdf2_addition(self, x, y, **params): + logcdf_x = self._logcdf_dispatch(x, **params) + logccdf_y = self._logccdf_dispatch(y, **params) + return special.logsumexp([logcdf_x, logccdf_y], axis=0) + + @_set_invalid_nan + def _logccdf1(self, x, *, method=None): + return self._logccdf_dispatch(x, method=method, **self._parameters) + + @_dispatch + def _logccdf_dispatch(self, x, method=None, **params): + if self._overrides('_logccdf_formula'): + method = self._logccdf_formula + elif _isnull(self.tol) and self._overrides('_ccdf_formula'): + method = self._logccdf_logexp + elif self._overrides('_logcdf_formula'): + method = self._logccdf_complement + else: + method = self._logccdf_quadrature + return method + + def _logccdf_formula(self): + raise NotImplementedError(self._not_implemented) + + def _logccdf_logexp(self, x, **params): + return np.log(self._ccdf_dispatch(x, **params)) + + def _logccdf_complement(self, x, **params): + return _log1mexp(self._logcdf_dispatch(x, **params)) + + def _logccdf_quadrature(self, x, **params): + _, b = self._support(**params) + return self._quadrature(self._logpdf_dispatch, limits=(x, b), + params=params, log=True) + + def ccdf(self, x, y=None, *, method=None): + r"""Complementary cumulative distribution function + + The complementary cumulative distribution function ("CCDF"), denoted + :math:`G(x)`, is the complement of the cumulative distribution function + :math:`F(x)`; i.e., probability the random variable :math:`X` will + assume a value greater than :math:`x`: + + .. math:: + + G(x) = 1 - F(x) = P(X > x) + + A two-argument variant of this function is: + + .. math:: + + G(x, y) = 1 - F(x, y) = P(X < x \text{ or } X > y) + + `ccdf` accepts `x` for :math:`x` and `y` for :math:`y`. + + Parameters + ---------- + x, y : array + The arguments of the CCDF. `x` is required; `y` is optional. + method : {None, 'formula', 'logexp', 'complement', 'quadrature', 'addition'} + The strategy used to evaluate the CCDF. + By default (``None``), the infrastructure chooses between the + following options, listed in order of precedence. + + - ``'formula'``: use a formula for the CCDF itself + - ``'logexp'``: evaluate the log-CCDF and exponentiate + - ``'complement'``: evaluate the CDF and take the complement + - ``'quadrature'``: numerically integrate the PDF + + The two-argument form chooses between: + + - ``'formula'``: use a formula for the CCDF itself + - ``'addition'``: compute the CDF at `x` and the CCDF at `y`, then add + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The CCDF evaluated at the provided argument(s). + + See Also + -------- + cdf + logccdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. + The CCDF :math:`G(x)` is related to the probability density function + :math:`f(x)` by: + + .. math:: + + G(x) = \int_x^r f(u) du + + The two argument version is: + + .. math:: + + G(x, y) = \int_l^x f(u) du + \int_y^r f(u) du + + The CCDF returns its minimum value of :math:`0` for :math:`x ≥ r` + and its maximum value of :math:`1` for :math:`x ≤ l`. + + The CCDF is also known as the "survival function". + + References + ---------- + .. [1] Cumulative distribution function, *Wikipedia*, + https://en.wikipedia.org/wiki/Cumulative_distribution_function#Derived_functions + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the CCDF at the desired argument: + + >>> X.ccdf(0.25) + 0.25 + >>> np.allclose(X.ccdf(0.25), 1-X.cdf(0.25)) + True + + Evaluate the complement of the cumulative probability between two arguments: + + >>> X.ccdf(-0.25, 0.25) == X.cdf(-0.25) + X.ccdf(0.25) + True + + """ # noqa: E501 + if y is None: + return self._ccdf1(x, method=method) + else: + return self._ccdf2(x, y, method=method) + + @_cdf2_input_validation + def _ccdf2(self, x, y, *, method): + return self._ccdf2_dispatch(x, y, method=method, **self._parameters) + + @_dispatch + def _ccdf2_dispatch(self, x, y, *, method=None, **params): + if self._overrides('_ccdf2_formula'): + method = self._ccdf2_formula + else: + method = self._ccdf2_addition + return method + + def _ccdf2_formula(self, x, y, **params): + raise NotImplementedError(self._not_implemented) + + def _ccdf2_addition(self, x, y, **params): + cdf_x = self._cdf_dispatch(x, **params) + ccdf_y = self._ccdf_dispatch(y, **params) + # even if x > y, cdf(x, y) + ccdf(x,y) sums to 1 + return cdf_x + ccdf_y + + @_set_invalid_nan + def _ccdf1(self, x, *, method): + return self._ccdf_dispatch(x, method=method, **self._parameters) + + @_dispatch + def _ccdf_dispatch(self, x, method=None, **params): + if self._overrides('_ccdf_formula'): + method = self._ccdf_formula + elif self._overrides('_logccdf_formula'): + method = self._ccdf_logexp + elif _isnull(self.tol) and self._overrides('_cdf_formula'): + method = self._ccdf_complement + else: + method = self._ccdf_quadrature + return method + + def _ccdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _ccdf_logexp(self, x, **params): + return np.exp(self._logccdf_dispatch(x, **params)) + + def _ccdf_complement(self, x, **params): + return 1 - self._cdf_dispatch(x, **params) + + def _ccdf_quadrature(self, x, **params): + _, b = self._support(**params) + return self._quadrature(self._pdf_dispatch, limits=(x, b), + params=params) + + ## Inverse cumulative distribution functions + + @_set_invalid_nan + def ilogcdf(self, logp, *, method=None): + r"""Inverse of the logarithm of the cumulative distribution function. + + The inverse of the logarithm of the cumulative distribution function + ("inverse log-CDF") is the argument :math:`x` for which the logarithm + of the cumulative distribution function :math:`\log(F(x))` evaluates + to :math:`\log(p)`. + + Mathematically, it is equivalent to :math:`F^{-1}(\exp(y))`, where + :math:`y = \log(p)`, but it may be numerically favorable compared to + the naive implementation (computing :math:`p = \exp(y)`, then + :math:`F^{-1}(p)`). + + `ilogcdf` accepts `logp` for :math:`\log(p) ≤ 0`. + + Parameters + ---------- + logp : array + The argument of the inverse log-CDF. + method : {None, 'formula', 'complement', 'inversion'} + The strategy used to evaluate the inverse log-CDF. + By default (``None``), the infrastructure chooses between the + following options, listed in order of precedence. + + - ``'formula'``: use a formula for the inverse log-CDF itself + - ``'complement'``: evaluate the inverse log-CCDF at the + logarithmic complement of `logp` (see Notes) + - ``'inversion'``: solve numerically for the argument at which the + log-CDF is equal to `logp` + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The inverse log-CDF evaluated at the provided argument. + + See Also + -------- + icdf + logcdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. + The inverse log-CDF returns its minimum value of :math:`l` at + :math:`\log(p) = \log(0) = -\infty` and its maximum value of :math:`r` at + :math:`\log(p) = \log(1) = 0`. Because the log-CDF has range + :math:`[-\infty, 0]`, the inverse log-CDF is only defined on the + negative reals; for :math:`\log(p) > 0`, `ilogcdf` returns ``nan``. + + Occasionally, it is needed to find the argument of the CDF for which + the resulting probability is very close to ``0`` or ``1`` - too close to + represent accurately with floating point arithmetic. In many cases, + however, the *logarithm* of this resulting probability may be + represented in floating point arithmetic, in which case this function + may be used to find the argument of the CDF for which the *logarithm* + of the resulting probability is :math:`y = \log(p)`. + + The "logarithmic complement" of a number :math:`z` is mathematically + equivalent to :math:`\log(1-\exp(z))`, but it is computed to avoid loss + of precision when :math:`\exp(z)` is nearly :math:`0` or :math:`1`. + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the inverse log-CDF at the desired argument: + + >>> X.ilogcdf(-0.25) + 0.2788007830714034 + >>> np.allclose(X.ilogcdf(-0.25), X.icdf(np.exp(-0.25))) + True + + """ + return self._ilogcdf_dispatch(logp, method=method, **self._parameters) + + @_dispatch + def _ilogcdf_dispatch(self, x, method=None, **params): + if self._overrides('_ilogcdf_formula'): + method = self._ilogcdf_formula + elif self._overrides('_ilogccdf_formula'): + method = self._ilogcdf_complement + else: + method = self._ilogcdf_inversion + return method + + def _ilogcdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _ilogcdf_complement(self, x, **params): + return self._ilogccdf_dispatch(_log1mexp(x), **params) + + def _ilogcdf_inversion(self, x, **params): + return self._solve_bounded(self._logcdf_dispatch, x, params=params) + + @_set_invalid_nan + def icdf(self, p, *, method=None): + r"""Inverse of the cumulative distribution function. + + The inverse of the cumulative distribution function ("inverse CDF"), + denoted :math:`F^{-1}(p)`, is the argument :math:`x` for which the + cumulative distribution function :math:`F(x)` evaluates to :math:`p`. + + .. math:: + + F^{-1}(p) = x \quad \text{s.t.} \quad F(x) = p + + `icdf` accepts `p` for :math:`p \in [0, 1]`. + + Parameters + ---------- + p : array + The argument of the inverse CDF. + method : {None, 'formula', 'complement', 'inversion'} + The strategy used to evaluate the inverse CDF. + By default (``None``), the infrastructure chooses between the + following options, listed in order of precedence. + + - ``'formula'``: use a formula for the inverse CDF itself + - ``'complement'``: evaluate the inverse CCDF at the + complement of `p` + - ``'inversion'``: solve numerically for the argument at which the + CDF is equal to `p` + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The inverse CDF evaluated at the provided argument. + + See Also + -------- + cdf + ilogcdf + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. The + inverse CDF returns its minimum value of :math:`l` at :math:`p = 0` + and its maximum value of :math:`r` at :math:`p = 1`. Because the CDF + has range :math:`[0, 1]`, the inverse CDF is only defined on the + domain :math:`[0, 1]`; for :math:`p < 0` and :math:`p > 1`, `icdf` + returns ``nan``. + + The inverse CDF is also known as the quantile function, percentile function, + and percent-point function. + + References + ---------- + .. [1] Quantile function, *Wikipedia*, + https://en.wikipedia.org/wiki/Quantile_function + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the inverse CDF at the desired argument: + + >>> X.icdf(0.25) + -0.25 + >>> np.allclose(X.cdf(X.icdf(0.25)), 0.25) + True + + This function returns NaN when the argument is outside the domain. + + >>> X.icdf([-0.1, 0, 1, 1.1]) + array([ nan, -0.5, 0.5, nan]) + + """ + return self._icdf_dispatch(p, method=method, **self._parameters) + + @_dispatch + def _icdf_dispatch(self, x, method=None, **params): + if self._overrides('_icdf_formula'): + method = self._icdf_formula + elif _isnull(self.tol) and self._overrides('_iccdf_formula'): + method = self._icdf_complement + else: + method = self._icdf_inversion + return method + + def _icdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _icdf_complement(self, x, **params): + return self._iccdf_dispatch(1 - x, **params) + + def _icdf_inversion(self, x, **params): + return self._solve_bounded(self._cdf_dispatch, x, params=params) + + @_set_invalid_nan + def ilogccdf(self, logp, *, method=None): + r"""Inverse of the log of the complementary cumulative distribution function. + + The inverse of the logarithm of the complementary cumulative distribution + function ("inverse log-CCDF") is the argument :math:`x` for which the logarithm + of the complementary cumulative distribution function :math:`\log(G(x))` + evaluates to :math:`\log(p)`. + + Mathematically, it is equivalent to :math:`G^{-1}(\exp(y))`, where + :math:`y = \log(p)`, but it may be numerically favorable compared to the naive + implementation (computing :math:`p = \exp(y)`, then :math:`G^{-1}(p)`). + + `ilogccdf` accepts `logp` for :math:`\log(p) ≤ 0`. + + Parameters + ---------- + x : array + The argument of the inverse log-CCDF. + method : {None, 'formula', 'complement', 'inversion'} + The strategy used to evaluate the inverse log-CCDF. + By default (``None``), the infrastructure chooses between the + following options, listed in order of precedence. + + - ``'formula'``: use a formula for the inverse log-CCDF itself + - ``'complement'``: evaluate the inverse log-CDF at the + logarithmic complement of `x` (see Notes) + - ``'inversion'``: solve numerically for the argument at which the + log-CCDF is equal to `x` + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The inverse log-CCDF evaluated at the provided argument. + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. The + inverse log-CCDF returns its minimum value of :math:`l` at + :math:`\log(p) = \log(1) = 0` and its maximum value of :math:`r` at + :math:`\log(p) = \log(0) = -\infty`. Because the log-CCDF has range + :math:`[-\infty, 0]`, the inverse log-CDF is only defined on the + negative reals; for :math:`\log(p) > 0`, `ilogccdf` returns ``nan``. + + Occasionally, it is needed to find the argument of the CCDF for which + the resulting probability is very close to ``0`` or ``1`` - too close to + represent accurately with floating point arithmetic. In many cases, + however, the *logarithm* of this resulting probability may be + represented in floating point arithmetic, in which case this function + may be used to find the argument of the CCDF for which the *logarithm* + of the resulting probability is `y = \log(p)`. + + The "logarithmic complement" of a number :math:`z` is mathematically + equivalent to :math:`\log(1-\exp(z))`, but it is computed to avoid loss + of precision when :math:`\exp(z)` is nearly :math:`0` or :math:`1`. + + See Also + -------- + iccdf + ilogccdf + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the inverse log-CCDF at the desired argument: + + >>> X.ilogccdf(-0.25) + -0.2788007830714034 + >>> np.allclose(X.ilogccdf(-0.25), X.iccdf(np.exp(-0.25))) + True + + """ + return self._ilogccdf_dispatch(logp, method=method, **self._parameters) + + @_dispatch + def _ilogccdf_dispatch(self, x, method=None, **params): + if self._overrides('_ilogccdf_formula'): + method = self._ilogccdf_formula + elif self._overrides('_ilogcdf_formula'): + method = self._ilogccdf_complement + else: + method = self._ilogccdf_inversion + return method + + def _ilogccdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _ilogccdf_complement(self, x, **params): + return self._ilogcdf_dispatch(_log1mexp(x), **params) + + def _ilogccdf_inversion(self, x, **params): + return self._solve_bounded(self._logccdf_dispatch, x, params=params) + + @_set_invalid_nan + def iccdf(self, p, *, method=None): + r"""Inverse complementary cumulative distribution function. + + The inverse complementary cumulative distribution function ("inverse CCDF"), + denoted :math:`G^{-1}(p)`, is the argument :math:`x` for which the + complementary cumulative distribution function :math:`G(x)` evaluates to + :math:`p`. + + .. math:: + + G^{-1}(p) = x \quad \text{s.t.} \quad G(x) = p + + `iccdf` accepts `p` for :math:`p \in [0, 1]`. + + Parameters + ---------- + p : array + The argument of the inverse CCDF. + method : {None, 'formula', 'complement', 'inversion'} + The strategy used to evaluate the inverse CCDF. + By default (``None``), the infrastructure chooses between the + following options, listed in order of precedence. + + - ``'formula'``: use a formula for the inverse CCDF itself + - ``'complement'``: evaluate the inverse CDF at the + complement of `p` + - ``'inversion'``: solve numerically for the argument at which the + CCDF is equal to `p` + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a ``NotImplementedError`` + will be raised. + + Returns + ------- + out : array + The inverse CCDF evaluated at the provided argument. + + Notes + ----- + Suppose a continuous probability distribution has support :math:`[l, r]`. The + inverse CCDF returns its minimum value of :math:`l` at :math:`p = 1` + and its maximum value of :math:`r` at :math:`p = 0`. Because the CCDF + has range :math:`[0, 1]`, the inverse CCDF is only defined on the + domain :math:`[0, 1]`; for :math:`p < 0` and :math:`p > 1`, ``iccdf`` + returns ``nan``. + + See Also + -------- + icdf + ilogccdf + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=-0.5, b=0.5) + + Evaluate the inverse CCDF at the desired argument: + + >>> X.iccdf(0.25) + 0.25 + >>> np.allclose(X.iccdf(0.25), X.icdf(1-0.25)) + True + + This function returns NaN when the argument is outside the domain. + + >>> X.iccdf([-0.1, 0, 1, 1.1]) + array([ nan, 0.5, -0.5, nan]) + + """ + return self._iccdf_dispatch(p, method=method, **self._parameters) + + @_dispatch + def _iccdf_dispatch(self, x, method=None, **params): + if self._overrides('_iccdf_formula'): + method = self._iccdf_formula + elif _isnull(self.tol) and self._overrides('_icdf_formula'): + method = self._iccdf_complement + else: + method = self._iccdf_inversion + return method + + def _iccdf_formula(self, x, **params): + raise NotImplementedError(self._not_implemented) + + def _iccdf_complement(self, x, **params): + return self._icdf_dispatch(1 - x, **params) + + def _iccdf_inversion(self, x, **params): + return self._solve_bounded(self._ccdf_dispatch, x, params=params) + + ### Sampling Functions + # The following functions for drawing samples from the distribution are + # exposed via a public method that accepts one positional argument - the + # shape of the sample - and keyword options (but not distribution + # parameters). + # sample + # ~~qmc_sample~~ built into sample now + # + # Common keyword options include: + # method - a string that indicates which method should be used to compute + # the quantity (e.g. a formula or numerical integration). + # rng - the NumPy Generator object to used for drawing random numbers. + # + # Input/output validation is included in each function, since there is + # little code to be shared. + # These are the methods meant to be called by users. + # + # Each public method calls a private "dispatch" method that + # determines which "method" (strategy for calculating the desired quantity) + # to use by default and, via the `@_dispatch` decorator, calls the + # method and computes the result. + # Each dispatch method can designate the responsibility of sampling to any + # of several "implementation" methods. These methods accept only + # `**params`, the parameter dictionary passed from the public method via + # the "dispatch" method. + # See the note corresponding with the "Distribution Parameters" for more + # information. + + def sample(self, shape=(), *, method=None, rng=None, qmc_engine=None): + """Random or quasi-Monte Carlo sample from the distribution. + + Parameters + ---------- + shape : tuple of ints, default: () + The shape of the sample to draw. If the parameters of the distribution + underlying the random variable are arrays of shape ``param_shape``, + the output array will be of shape ``shape + param_shape``. + method : {None, 'formula', 'inverse_transform'} + The strategy used to produce the sample. By default (``None``), + the infrastructure chooses between the following options, + listed in order of precedence. + + - ``'formula'``: an implementation specific to the distribution + - ``'inverse_transform'``: generate a uniformly distributed sample and + return the inverse CDF at these arguments. + + Not all `method` options are available for all distributions. + If the selected `method` is not available, a `NotImplementedError`` + will be raised. + rng : `numpy.random.Generator`, optional + The pseudorandom number generator instance with which to generate + the sample. If `None` (default), a new generator is instantiated + with fresh, unpredictable entropy from the operating system. + qmc_engine : `scipy.stats.qmc.QMCEngine` subclass, optional + A QMC engine class with which to generate a quasi-Monte Carlo sample. + An instance of the `qmc_engine` class will be created and provided + with `rng` (e.g. for use with shuffling). Typically, the use of + `qmc_engine` with ``method='formula'`` will be incompatible. + + Notes + ----- + The values of a quasi-Monte Carlo sequence are not statistically + independent; the sequence is designed to have low-discrepancy. + The output of `sample` is always formed with this low-discrepancy + sequence aligned along axis ``0``. Separate slices along axis ``0`` + (e.g. separate columns of a 2D output) are separate low-discrepancy + sequences; the low-discrepancy properties hold *only* along axis + ``0``, not along other axes. By default, many `QMCEngine` classes + use a pseudorandom number generator (provided by `rng`, in this case) + to *scramble* these separate sequences so they are statistically + independent. Consequently, the following simple rule holds for + quasi-Monte Carlo samples drawn using `QMCEngine` classes that + employ scrambling: each slice along axis ``0`` of the result is a + statistically independent low-discrepancy sequence. + + References + ---------- + .. [1] Sampling (statistics), *Wikipedia*, + https://en.wikipedia.org/wiki/Sampling_(statistics) + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> from scipy import stats + >>> X = stats.Uniform(a=0., b=1.) + + Generate a pseudorandom sample: + + >>> x = X.sample((1000, 1)) + >>> octiles = (np.arange(8) + 1) / 8 + >>> np.count_nonzero(x <= octiles, axis=0) + array([ 148, 263, 387, 516, 636, 751, 865, 1000]) # may vary + + Generate a Quasi-Monte Carlo sample: + + >>> x = X.sample((1000, 1), qmc_engine=stats.qmc.Halton) + >>> np.count_nonzero(x <= octiles, axis=0) + array([ 125, 250, 375, 500, 625, 750, 875, 1000]) + + The QMC sample has low discrepancy along axis 0: + + >>> x = X.sample((1000, 3, 1), qmc_engine=stats.qmc.Halton) + >>> np.count_nonzero(x <= octiles, axis=0) + array([[ 125, 250, 375, 500, 625, 750, 875, 1000], + [ 124, 249, 374, 498, 624, 750, 875, 1000], + [ 124, 249, 374, 498, 624, 750, 874, 1000]]) + + The shape of the result is the sum of the `shape` parameter + and the shape of the broadcasted distribution parameter arrays. + + >>> X = stats.Uniform(a=np.zeros((3, 1)), b=np.ones(2)) + >>> X.a.shape, + (3, 2) + >>> x = X.sample(shape=(5, 4)) + >>> x.shape + (5, 4, 3, 2) + + """ + # needs output validation to ensure that developer returns correct + # dtype and shape + sample_shape = (shape,) if not np.iterable(shape) else tuple(shape) + full_shape = sample_shape + self._shape + rng = self._validate_rng(rng) or self.rng or np.random.default_rng() + + if qmc_engine is None: + res = self._sample_dispatch(sample_shape, full_shape, method=method, + rng=rng, **self._parameters) + else: + # needs input validation for qrng + d = int(np.prod(full_shape[1:])) + length = full_shape[0] if full_shape else 1 + qrng = qmc_engine(d=d, seed=rng) + res = self._qmc_sample_dispatch(length, full_shape, method=method, + qrng=qrng, **self._parameters) + return res.astype(self._dtype, copy=False) + + @_dispatch + def _sample_dispatch(self, sample_shape, full_shape, *, method, rng, **params): + # make sure that tests catch if sample is 0d array + if self._overrides('_sample_formula'): + method = self._sample_formula + else: + method = self._sample_inverse_transform + return method + + def _sample_formula(self, sample_shape, full_shape, *, rng, **params): + raise NotImplementedError(self._not_implemented) + + def _sample_inverse_transform(self, sample_shape, full_shape, *, rng, **params): + uniform = rng.random(size=full_shape, dtype=self._dtype) + return self._icdf_dispatch(uniform, **params) + + @_dispatch + def _qmc_sample_dispatch(self, length, full_shape, *, method, qrng, **params): + # make sure that tests catch if sample is 0d array + if self._overrides('_qmc_sample_formula'): + method = self._qmc_sample_formula + else: + method = self._qmc_sample_inverse_transform + return method + + def _qmc_sample_formula(self, length, full_shape, *, qrng, **params): + raise NotImplementedError(self._not_implemented) + + def _qmc_sample_inverse_transform(self, length, full_shape, *, qrng, **params): + uniform = qrng.random(length) + uniform = np.reshape(uniform, full_shape).astype(self._dtype) + return self._icdf_dispatch(uniform, **params) + + ### Moments + # The `moment` method accepts two positional arguments - the order and kind + # (raw, central, or standard) of the moment - and a keyword option: + # method - a string that indicates which method should be used to compute + # the quantity (e.g. a formula or numerical integration). + # Like the distribution properties, input/output validation is provided by + # the `_set_invalid_nan_property` decorator. + # + # Unlike most public methods above, `moment` dispatches to one of three + # private methods - one for each 'kind'. Like most *public* methods above, + # each of these private methods calls a private "dispatch" method that + # determines which "method" (strategy for calculating the desired quantity) + # to use. Also, each dispatch method can designate the responsibility + # computing the moment to one of several "implementation" methods. + # Unlike the dispatch methods above, however, the `@_dispatch` decorator + # is not used, and both logic and method calls are included in the function + # itself. + # Instead of determining which method will be used based solely on the + # implementation methods available and calling only the corresponding + # implementation method, *all* the implementation methods are called + # in sequence until one returns the desired information. When an + # implementation methods cannot provide the requested information, it + # returns the object None (which is distinct from arrays with NaNs or infs, + # which are valid values of moments). + # The reason for this approach is that although formulae for the first + # few moments of a distribution may be found, general formulae that work + # for all orders are not always easy to find. This approach allows the + # developer to write "formula" implementation functions that return the + # desired moment when it is available and None otherwise. + # + # Note that the first implementation method called is a cache. This is + # important because lower-order moments are often needed to compute + # higher moments from formulae, so we eliminate redundant calculations + # when moments of several orders are needed. + + @cached_property + def _moment_methods(self): + return {'cache', 'formula', 'transform', + 'normalize', 'general', 'quadrature'} + + @property + def _zero(self): + return self._constants()[0] + + @property + def _one(self): + return self._constants()[1] + + def _constants(self): + if self._constant_cache is not None: + return self._constant_cache + + constants = self._preserve_type([0, 1]) + + if self.cache_policy != _NO_CACHE: + self._constant_cache = constants + + return constants + + @_set_invalid_nan_property + def moment(self, order=1, kind='raw', *, method=None): + r"""Raw, central, or standard moment of positive integer order. + + In terms of probability density function :math:`f(x)` and support + :math:`\chi`, the "raw" moment (about the origin) of order :math:`n` of + a random variable :math:`X` is: + + .. math:: + + \mu'_n(X) = \int_{\chi} x^n f(x) dx + + The "central" moment is the raw moment taken about the mean, + :math:`\mu = \mu'_1`: + + .. math:: + + \mu_n(X) = \int_{\chi} (x - \mu) ^n f(x) dx + + The "standardized" moment is the central moment normalized by the + :math:`n^\text{th}` power of the standard deviation + :math:`\sigma = \sqrt{\mu_2}` to produce a scale invariant quantity: + + .. math:: + + \tilde{\mu}_n(X) = \frac{\mu_n(X)} + {\sigma^n} + + Parameters + ---------- + order : int + The integer order of the moment; i.e. :math:`n` in the formulae above. + kind : {'raw', 'central', 'standardized'} + Whether to return the raw (default), central, or standardized moment + defined above. + method : {None, 'formula', 'general', 'transform', 'normalize', 'quadrature', 'cache'} + The strategy used to evaluate the moment. By default (``None``), + the infrastructure chooses between the following options, + listed in order of precedence. + + - ``'cache'``: use the value of the moment most recently calculated + via another method + - ``'formula'``: use a formula for the moment itself + - ``'general'``: use a general result that is true for all distributions + with finite moments; for instance, the zeroth raw moment is + identically 1 + - ``'transform'``: transform a raw moment to a central moment or + vice versa (see Notes) + - ``'normalize'``: normalize a central moment to get a standardized + or vice versa + - ``'quadrature'``: numerically integrate according to the definition + + Not all `method` options are available for all orders, kinds, and + distributions. If the selected `method` is not available, a + ``NotImplementedError`` will be raised. + + Returns + ------- + out : array + The moment of the random variable of the specified order and kind. + + See Also + -------- + pdf + mean + variance + standard_deviation + skewness + kurtosis + + Notes + ----- + Not all distributions have finite moments of all orders; moments of some + orders may be undefined or infinite. If a formula for the moment is not + specifically implemented for the chosen distribution, SciPy will attempt + to compute the moment via a generic method, which may yield a finite + result where none exists. This is not a critical bug, but an opportunity + for an enhancement. + + The definition of a raw moment in the summary is specific to the raw moment + about the origin. The raw moment about any point :math:`a` is: + + .. math:: + + E[(X-a)^n] = \int_{\chi} (x-a)^n f(x) dx + + In this notation, a raw moment about the origin is :math:`\mu'_n = E[x^n]`, + and a central moment is :math:`\mu_n = E[(x-\mu)^n]`, where :math:`\mu` + is the first raw moment; i.e. the mean. + + The ``'transform'`` method takes advantage of the following relationships + between moments taken about different points :math:`a` and :math:`b`. + + .. math:: + + E[(X-b)^n] = \sum_{i=0}^n E[(X-a)^i] {n \choose i} (a - b)^{n-i} + + For instance, to transform the raw moment to the central moment, we let + :math:`b = \mu` and :math:`a = 0`. + + The distribution infrastructure provides flexibility for distribution + authors to implement separate formulas for raw moments, central moments, + and standardized moments of any order. By default, the moment of the + desired order and kind is evaluated from the formula if such a formula + is available; if not, the infrastructure uses any formulas that are + available rather than resorting directly to numerical integration. + For instance, if formulas for the first three raw moments are + available and the third standardized moments is desired, the + infrastructure will evaluate the raw moments and perform the transforms + and standardization required. The decision tree is somewhat complex, + but the strategy for obtaining a moment of a given order and kind + (possibly as an intermediate step due to the recursive nature of the + transform formula above) roughly follows this order of priority: + + #. Use cache (if order of same moment and kind has been calculated) + #. Use formula (if available) + #. Transform between raw and central moment and/or normalize to convert + between central and standardized moments (if efficient) + #. Use a generic result true for most distributions (if available) + #. Use quadrature + + References + ---------- + .. [1] Moment, *Wikipedia*, + https://en.wikipedia.org/wiki/Moment_(mathematics) + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Evaluate the first raw moment: + + >>> X.moment(order=1, kind='raw') + 1.0 + >>> X.moment(order=1, kind='raw') == X.mean() == X.mu + True + + Evaluate the second central moment: + + >>> X.moment(order=2, kind='central') + 4.0 + >>> X.moment(order=2, kind='central') == X.variance() == X.sigma**2 + True + + Evaluate the fourth standardized moment: + + >>> X.moment(order=4, kind='standardized') + 3.0 + >>> X.moment(order=4, kind='standardized') == X.kurtosis(convention='non-excess') + True + + """ # noqa:E501 + kinds = {'raw': self._moment_raw, + 'central': self._moment_central, + 'standardized': self._moment_standardized} + order = self._validate_order_kind(order, kind, kinds) + moment_kind = kinds[kind] + return moment_kind(order, method=method) + + def _moment_raw(self, order=1, *, method=None): + """Raw distribution moment about the origin.""" + # Consider exposing the point about which moments are taken as an + # option. This is easy to support, since `_moment_transform_center` + # does all the work. + methods = self._moment_methods if method is None else {method} + return self._moment_raw_dispatch(order, methods=methods, **self._parameters) + + def _moment_raw_dispatch(self, order, *, methods, **params): + moment = None + + if 'cache' in methods: + moment = self._moment_raw_cache.get(order, None) + + if moment is None and 'formula' in methods: + moment = self._moment_raw_formula(order, **params) + + if moment is None and 'transform' in methods and order > 1: + moment = self._moment_raw_transform(order, **params) + + if moment is None and 'general' in methods: + moment = self._moment_raw_general(order, **params) + + if moment is None and 'quadrature' in methods: + moment = self._moment_integrate_pdf(order, center=self._zero, **params) + + if moment is not None and self.cache_policy != _NO_CACHE: + self._moment_raw_cache[order] = moment + + return moment + + def _moment_raw_formula(self, order, **params): + return None + + def _moment_raw_transform(self, order, **params): + central_moments = [] + for i in range(int(order) + 1): + methods = {'cache', 'formula', 'normalize', 'general'} + moment_i = self._moment_central_dispatch(order=i, methods=methods, **params) + if moment_i is None: + return None + central_moments.append(moment_i) + + # Doesn't make sense to get the mean by "transform", since that's + # how we got here. Questionable whether 'quadrature' should be here. + mean_methods = {'cache', 'formula', 'quadrature'} + mean = self._moment_raw_dispatch(self._one, methods=mean_methods, **params) + if mean is None: + return None + + moment = self._moment_transform_center(order, central_moments, mean, self._zero) + return moment + + def _moment_raw_general(self, order, **params): + # This is the only general formula for a raw moment of a probability + # distribution + return self._one if order == 0 else None + + def _moment_central(self, order=1, *, method=None): + """Distribution moment about the mean.""" + methods = self._moment_methods if method is None else {method} + return self._moment_central_dispatch(order, methods=methods, **self._parameters) + + def _moment_central_dispatch(self, order, *, methods, **params): + moment = None + + if 'cache' in methods: + moment = self._moment_central_cache.get(order, None) + + if moment is None and 'formula' in methods: + moment = self._moment_central_formula(order, **params) + + if moment is None and 'transform' in methods: + moment = self._moment_central_transform(order, **params) + + if moment is None and 'normalize' in methods and order > 2: + moment = self._moment_central_normalize(order, **params) + + if moment is None and 'general' in methods: + moment = self._moment_central_general(order, **params) + + if moment is None and 'quadrature' in methods: + mean = self._moment_raw_dispatch(self._one, **params, + methods=self._moment_methods) + moment = self._moment_integrate_pdf(order, center=mean, **params) + + if moment is not None and self.cache_policy != _NO_CACHE: + self._moment_central_cache[order] = moment + + return moment + + def _moment_central_formula(self, order, **params): + return None + + def _moment_central_transform(self, order, **params): + + raw_moments = [] + for i in range(int(order) + 1): + methods = {'cache', 'formula', 'general'} + moment_i = self._moment_raw_dispatch(order=i, methods=methods, **params) + if moment_i is None: + return None + raw_moments.append(moment_i) + + mean_methods = self._moment_methods + mean = self._moment_raw_dispatch(self._one, methods=mean_methods, **params) + + moment = self._moment_transform_center(order, raw_moments, self._zero, mean) + return moment + + def _moment_central_normalize(self, order, **params): + methods = {'cache', 'formula', 'general'} + standard_moment = self._moment_standardized_dispatch(order, **params, + methods=methods) + if standard_moment is None: + return None + var = self._moment_central_dispatch(2, methods=self._moment_methods, **params) + return standard_moment*var**(order/2) + + def _moment_central_general(self, order, **params): + general_central_moments = {0: self._one, 1: self._zero} + return general_central_moments.get(order, None) + + def _moment_standardized(self, order=1, *, method=None): + """Standardized distribution moment.""" + methods = self._moment_methods if method is None else {method} + return self._moment_standardized_dispatch(order, methods=methods, + **self._parameters) + + def _moment_standardized_dispatch(self, order, *, methods, **params): + moment = None + + if 'cache' in methods: + moment = self._moment_standardized_cache.get(order, None) + + if moment is None and 'formula' in methods: + moment = self._moment_standardized_formula(order, **params) + + if moment is None and 'normalize' in methods: + moment = self._moment_standardized_normalize(order, False, **params) + + if moment is None and 'general' in methods: + moment = self._moment_standardized_general(order, **params) + + if moment is None and 'normalize' in methods: + moment = self._moment_standardized_normalize(order, True, **params) + + if moment is not None and self.cache_policy != _NO_CACHE: + self._moment_standardized_cache[order] = moment + + return moment + + def _moment_standardized_formula(self, order, **params): + return None + + def _moment_standardized_normalize(self, order, use_quadrature, **params): + methods = ({'quadrature'} if use_quadrature + else {'cache', 'formula', 'transform'}) + central_moment = self._moment_central_dispatch(order, **params, + methods=methods) + if central_moment is None: + return None + var = self._moment_central_dispatch(2, methods=self._moment_methods, + **params) + return central_moment/var**(order/2) + + def _moment_standardized_general(self, order, **params): + general_standard_moments = {0: self._one, 1: self._zero, 2: self._one} + return general_standard_moments.get(order, None) + + def _moment_integrate_pdf(self, order, center, **params): + def integrand(x, order, center, **params): + pdf = self._pdf_dispatch(x, **params) + return pdf*(x-center)**order + return self._quadrature(integrand, args=(order, center), params=params) + + def _moment_transform_center(self, order, moment_as, a, b): + a, b, *moment_as = np.broadcast_arrays(a, b, *moment_as) + n = order + i = np.arange(n+1).reshape([-1]+[1]*a.ndim) # orthogonal to other axes + i = self._preserve_type(i) + n_choose_i = special.binom(n, i) + moment_b = np.sum(n_choose_i*moment_as*(a-b)**(n-i), axis=0) + return moment_b + + def _logmoment(self, order=1, *, logcenter=None, standardized=False): + # make this private until it is worked into moment + if logcenter is None or standardized is True: + logmean = self._logmoment_quad(self._one, -np.inf, **self._parameters) + else: + logmean = None + + logcenter = logmean if logcenter is None else logcenter + res = self._logmoment_quad(order, logcenter, **self._parameters) + if standardized: + logvar = self._logmoment_quad(2, logmean, **self._parameters) + res = res - logvar * (order/2) + return res + + def _logmoment_quad(self, order, logcenter, **params): + def logintegrand(x, order, logcenter, **params): + logpdf = self._logpdf_dispatch(x, **params) + return logpdf + order*_logexpxmexpy(np.log(x+0j), logcenter) + return self._quadrature(logintegrand, args=(order, logcenter), + params=params, log=True) + + ### Convenience + + def plot(self, x='x', y='pdf', *, t=('cdf', 0.0005, 0.9995), ax=None): + r"""Plot a function of the distribution. + + Convenience function for quick visualization of the distribution + underlying the random variable. + + Parameters + ---------- + x, y : str, optional + String indicating the quantities to be used as the abscissa and + ordinate (horizontal and vertical coordinates), respectively. + Defaults are ``'x'`` (the domain of the random variable) and + ``'pdf'`` (the probability density function). Valid values are: + 'x', 'pdf', 'cdf', 'ccdf', 'icdf', 'iccdf', 'logpdf', 'logcdf', + 'logccdf', 'ilogcdf', 'ilogccdf'. + t : 3-tuple of (str, float, float), optional + Tuple indicating the limits within which the quantities are plotted. + Default is ``('cdf', 0.001, 0.999)`` indicating that the central + 99.9% of the distribution is to be shown. Valid values are: + 'x', 'cdf', 'ccdf', 'icdf', 'iccdf', 'logcdf', 'logccdf', + 'ilogcdf', 'ilogccdf'. + ax : `matplotlib.axes`, optional + Axes on which to generate the plot. If not provided, use the + current axes. + + Returns + ------- + ax : `matplotlib.axes` + Axes on which the plot was generated. + The plot can be customized by manipulating this object. + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from scipy import stats + >>> X = stats.Normal(mu=1., sigma=2.) + + Plot the PDF over the central 99.9% of the distribution. + Compare against a histogram of a random sample. + + >>> ax = X.plot() + >>> sample = X.sample(10000, qmc_engine=stats.qmc.Halton) + >>> ax.hist(sample, density=True, bins=50, alpha=0.5) + >>> plt.show() + + Plot ``logpdf(x)`` as a function of ``x`` in the left tail, + where the log of the CDF is between -10 and ``np.log(0.5)``. + + >>> X.plot('x', 'logpdf', t=('logcdf', -10, np.log(0.5))) + >>> plt.show() + + Plot the PDF of the normal distribution as a function of the + CDF for various values of the scale parameter. + + >>> X = stats.Normal(mu=0., sigma=[0.5, 1., 2]) + >>> X.plot('cdf', 'pdf') + >>> plt.show() + + """ + + # Strategy: given t limits, get quantile limits. Form grid of + # quantiles, compute requested x and y at quantiles, and plot. + # Currently, the grid of quantiles is always linearly spaced. + # Instead of always computing linearly-spaced quantiles, it + # would be better to choose: + # a) quantiles or probabilities + # b) linearly or logarithmically spaced + # based on the specified `t`. + # TODO: + # - smart spacing of points + # - when the parameters of the distribution are an array, + # use the full range of abscissae for all curves + + t_is_quantile = {'x', 'icdf', 'iccdf', 'ilogcdf', 'ilogccdf'} + t_is_probability = {'cdf', 'ccdf', 'logcdf', 'logccdf'} + valid_t = t_is_quantile.union(t_is_probability) + valid_xy = valid_t.union({'pdf', 'logpdf'}) + + ndim = self._ndim + x_name, y_name = x, y + t_name, tlim = t[0], np.asarray(t[1:]) + tlim = tlim[:, np.newaxis] if ndim else tlim + + # pdf/logpdf are not valid for `t` because we can't easily invert them + message = (f'Argument `t` of `{self.__class__.__name__}.plot` "' + f'must be one of {valid_t}') + if t_name not in valid_t: + raise ValueError(message) + + message = (f'Argument `x` of `{self.__class__.__name__}.plot` "' + f'must be one of {valid_xy}') + if x_name not in valid_xy: + raise ValueError(message) + + message = (f'Argument `y` of `{self.__class__.__name__}.plot` "' + f'must be one of {valid_xy}') + if t_name not in valid_xy: + raise ValueError(message) + + # This could just be a warning + message = (f'`{self.__class__.__name__}.plot` was called on a random ' + 'variable with at least one invalid shape parameters. When ' + 'a parameter is invalid, no plot can be shown.') + if self._any_invalid: + raise ValueError(message) + + # We could automatically ravel, but do we want to? For now, raise. + message = ("To use `plot`, distribution parameters must be " + "scalars or arrays with one or fewer dimensions.") + if ndim > 1: + raise ValueError(message) + + try: + import matplotlib.pyplot as plt # noqa: F401, E402 + except ModuleNotFoundError as exc: + message = ("`matplotlib` must be installed to use " + f"`{self.__class__.__name__}.plot`.") + raise ModuleNotFoundError(message) from exc + ax = plt.gca() if ax is None else ax + + # get quantile limits given t limits + qlim = tlim if t_name in t_is_quantile else getattr(self, 'i'+t_name)(tlim) + + message = (f"`{self.__class__.__name__}.plot` received invalid input for `t`: " + f"calling {'i'+t_name}({tlim}) produced {qlim}.") + if not np.all(np.isfinite(qlim)): + raise ValueError(message) + + # form quantile grid + grid = np.linspace(0, 1, 300) + grid = grid[:, np.newaxis] if ndim else grid + q = qlim[0] + (qlim[1] - qlim[0]) * grid + + # compute requested x and y at quantile grid + x = q if x_name in t_is_quantile else getattr(self, x_name)(q) + y = q if y_name in t_is_quantile else getattr(self, y_name)(q) + + # make plot + ax.plot(x, y) + ax.set_xlabel(x_name) + ax.set_ylabel(y_name) + ax.set_title(str(self)) + + # only need a legend if distribution has parameters + if len(self._parameters): + label = [] + parameters = self._parameterization.parameters + param_names = list(parameters) + param_arrays = [np.atleast_1d(self._parameters[pname]) + for pname in param_names] + for param_vals in zip(*param_arrays): + assignments = [f"{parameters[name].symbol} = {val:.4g}" + for name, val in zip(param_names, param_vals)] + label.append(", ".join(assignments)) + ax.legend(label) + + return ax + + + ### Fitting + # All methods above treat the distribution parameters as fixed, and the + # variable argument may be a quantile or probability. The fitting functions + # are fundamentally different because the quantiles (often observations) + # are considered to be fixed, and the distribution parameters are the + # variables. In a sense, they are like an inverse of the sampling + # functions. + # + # At first glance, it would seem ideal for `fit` to be a classmethod, + # called like `LogUniform.fit(sample=sample)`. + # I tried this. I insisted on it for a while. But if `fit` is a + # classmethod, it cannot call instance methods. If we want to support MLE, + # MPS, MoM, MoLM, then we end up with most of the distribution functions + # above needing to be classmethods, too. All state information, such as + # tolerances and the underlying distribution of `ShiftedScaledDistribution` + # and `OrderStatisticDistribution`, would need to be passed into all + # methods. And I'm not really sure how we would call `fit` as a + # classmethod of a transformed distribution - maybe + # ShiftedScaledDistribution.fit would accept the class of the + # shifted/scaled distribution as an argument? + # + # In any case, it was a conscious decision for the infrastructure to + # treat the parameters as "fixed" and the quantile/percentile arguments + # as "variable". There are a lot of advantages to this structure, and I + # don't think the fact that a few methods reverse the fixed and variable + # quantities should make us question that choice. It can still accomodate + # these methods reasonably efficiently. + + def llf(self, sample, *, axis=-1): + r"""Log-likelihood function + + Given a sample :math:`x`, the log-likelihood function (LLF) is the logarithm + of the joint probability density of the observed data. It is typically + viewed as a function of the parameters :math:`\theta` of a statistical + distribution: + + .. math:: + + \mathcal{L}(\theta | x) = \log \left( \prod_i f_\theta(x_i) \right) = \sum_{i} \log(f_\theta(x_i)) + + where :math:`f_\theta` is the probability density function with + parameters :math:`\theta`. + + As a method of `ContinuousDistribution`, the parameter values are specified + during instantiation; `llf` accepts only the sample :math:`x` as `sample`. + + Parameters + ---------- + sample : array + The given sample for to calculate the LLF. + axis : int or tuple of ints + The axis over which the reducing operation (sum of logarithms) is performed. + + Notes + ----- + The LLF is often viewed as a function of the parameters with the sample fixed; + see the Notes for an example of a function with this signature. + + References + ---------- + .. [1] Likelihood function, *Wikipedia*, + https://en.wikipedia.org/wiki/Likelihood_function + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from scipy import stats + >>> X = stats.Normal(mu=0., sigma=1.) + + Evaluate the LLF with the given sample: + + >>> sample = [1., 2., 3.] + >>> X.llf(sample) + -9.756815599614018 + >>> np.allclose(X.llf(sample), np.sum(X.logpdf(sample))) + True + + To generate a function that accepts only the parameters and + holds the data fixed: + + >>> def llf(mu, sigma): + ... return stats.Normal(mu=mu, sigma=sigma).llf(sample) + >>> llf(0., 1.) + -9.756815599614018 + + """ # noqa: E501 + return np.sum(self.logpdf(sample), axis=axis) + + # def dllf(self, parameters=None, *, sample, var): + # """Partial derivative of the log likelihood function.""" + # parameters = parameters or {} + # self._update_parameters(**parameters) + # + # def f(x): + # update = {} + # update[var] = x + # self._update_parameters(**update) + # res = self.llf(sample=sample[:, np.newaxis], axis=0) + # return np.reshape(res, x.shape) + # + # return _differentiate(f, self._parameters[var]).df + + def fit(self, parameters, objective): + """Fit the distribution parameters to meet an objective. + + Parameters + ---------- + parameters : iterable of str or dict + An iterable containing the names of distribution parameters to be + adjusted to meet the `objective`. If a dictionary, the value + corresponding with each parameter name is a 2-tuple containing + lower and upper bounds of the parameter. + objective : callable or dict + If a callable, this is a scalar-valued function to be maximized by + adjusting the specified `parameters` of the random variable. + + Otherwise, this is a dictionary with the following keys: + + * ``'f'``: a callable as above, but may be vector-valued. + * ``'input'`` (optional): a tuple of arguments to be passed to the callable. + * ``'output'`` (optional): the desired output of the callable. If an array, + the objective is to minimize the Euclidean norm of the residual. Strings + ``'maximize'`` (default) and ``'minimize'`` are also recognized. + + If the callable is recognized as a method of the distribution, + additional constraints may be imposed on the distribution parameters. For + instance, if the callable is `llf`, then the first + element of `input` represents observations of the random variable, so a + constraint ensures that the observations remain within the support. + + Notes + ----- + To use this method, the shape parameters of the distribution must be scalars. + If the distribution parameters are arrays, a ``NotImplementedError`` is raised. + + Currently, this method does not return a result; rather, it modifies the + parameters of the provided distribution instance. In the future, a result + object with information about the optimization status may be returned. + + Examples + -------- + Instantiate a distribution with the desired parameters: + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from scipy import stats + >>> X = stats.Normal(mu=0., sigma=1.) + >>> rng = np.random.default_rng() + + Adjust the shape parameters to fit the distribution to data using maximum + likelihood estimation. + + >>> data = X.sample(1000, rng=rng) + >>> X.fit(['mu', 'sigma'], dict(f=X.llf, input=(data,))) + >>> X.plot() + >>> plt.hist(data, density=True, alpha=0.5) + >>> plt.show() + + Adjust the shape parameters to achieve a desired mean and standard deviation. + + >>> X.fit(['mu', 'sigma'], + ... dict(f=lambda: [X.mean(), X.standard_deviation()], + ... output=[1, 2])) + >>> X.mean(), X.standard_deviation() + (1.0000000860089517, 1.9999999399112434) + + """ + # add `_fit` implementation methods + + # The value added, compared to requiring the user to optimize/solve + # on their own: + # - (potentially) more efficient calls to private rather than public functions + # - (potentially) more efficient changes in parameter values + # - (potentially) automatically include constraints + # - convenience (least important) + + # this should probably be in a context manager to make sure it gets set back + # rather than turning input validation off, call private function? + iv_policy = self.iv_policy + self.iv_policy = 'skip_all' + x0 = [getattr(self, parameter) for parameter in parameters] + + if callable(objective): + f = objective + args = () + output = 'maximize' + else: + f = objective['f'] + args = objective.get('input', ()) # should do input validation on these + output = objective.get('output', 'maximize') + + if output == 'maximize': + def objective(x): + self._update_parameters(**dict(zip(parameters, x))) + return -f(*args) + elif output == 'minimize': + def objective(x): + self._update_parameters(**dict(zip(parameters, x))) + return f(*args) + else: + output = np.asarray(output) + def objective(x): + self._update_parameters(**dict(zip(parameters, x))) + return np.linalg.norm(f(*args) - output) + + param_info = self._parameterization.parameters + bounds = np.asarray([param_info[param_name].domain.endpoints + for param_name in parameters], dtype=object) + # should use bounds when possible + numerical_bounds = [] + constraints = [] + + for i, bound in enumerate(bounds): + a, b = bound + str_a, str_b = isinstance(a, str), isinstance(b, str) + numerical_bound = [a if not str_a else -np.inf, b if not str_b else np.inf] + numerical_bounds.append(numerical_bound) + + if str_a or str_b: + def g(x): + p = dict(zip(parameters, x)) + name = parameters[i] + a, b = param_info[name].domain.get_numerical_endpoints(p) + var = x[i] + res = [] + if str_a: + res = var - a + if str_b: + res = b - var + return res + constraints.append(optimize.NonlinearConstraint(g, 0, np.inf)) + + if f in {self.llf, self.pdf, self.logpdf, self.cdf, + self.logcdf, self.ccdf, self.logccdf}: + + data = np.asarray(args[0]) + data_min, data_max = np.min(data), np.max(data) + + # for now, assume that support bounds cannot become + # infinite by changing parameters + a, b = self.support() + inf_a, inf_b = np.isinf(a), np.isinf(b) + + if not inf_a or not inf_b: + def g(x): + self._update_parameters(**dict(zip(parameters, x))) + a, b = self.support() + res = [] + if not inf_a: + res.append(data_min - a) + if not inf_b: + res.append(b - data_max) + return res + constraints.append(optimize.NonlinearConstraint(g, 0, np.inf)) + + res = optimize.minimize(objective, x0, constraints=constraints, + bounds=numerical_bounds) + self.iv_policy = iv_policy + self._update_parameters(**dict(zip(parameters, res.x))) + return + + +# Rough sketch of how we might shift/scale distributions. The purpose of +# making it a separate class is for +# a) simplicity of the ContinuousDistribution class and +# b) avoiding the requirement that every distribution accept loc/scale. +# The simplicity of ContinuousDistribution is important, because there are +# several other distribution transformations to be supported; e.g., truncation, +# wrapping, folding, and doubling. We wouldn't want to cram all of this +# into the `ContinuousDistribution` class. Also, the order of the composition +# matters (e.g. truncate then shift/scale or vice versa). It's easier to +# accommodate different orders if the transformation is built up from +# components rather than all built into `ContinuousDistribution`. + +def _shift_scale_distribution_function_2arg(func): + def wrapped(self, x, y, *args, loc, scale, sign, **kwargs): + item = func.__name__ + + f = getattr(self._dist, item) + + # Obviously it's possible to get away with half of the work here. + # Let's focus on correct results first and optimize later. + xt = self._transform(x, loc, scale) + yt = self._transform(y, loc, scale) + fxy = f(xt, yt, *args, **kwargs) + fyx = f(yt, xt, *args, **kwargs) + return np.where(sign, fxy, fyx)[()] + + return wrapped + +def _shift_scale_distribution_function(func): + # c is for complementary + citem = {'_logcdf_dispatch': '_logccdf_dispatch', + '_cdf_dispatch': '_ccdf_dispatch', + '_logccdf_dispatch': '_logcdf_dispatch', + '_ccdf_dispatch': '_cdf_dispatch'} + def wrapped(self, x, *args, loc, scale, sign, **kwargs): + item = func.__name__ + + f = getattr(self._dist, item) + cf = getattr(self._dist, citem[item]) + + # Obviously it's possible to get away with half of the work here. + # Let's focus on correct results first and optimize later. + xt = self._transform(x, loc, scale) + fx = f(xt, *args, **kwargs) + cfx = cf(xt, *args, **kwargs) + return np.where(sign, fx, cfx)[()] + + return wrapped + +def _shift_scale_inverse_function(func): + citem = {'_ilogcdf_dispatch': '_ilogccdf_dispatch', + '_icdf_dispatch': '_iccdf_dispatch', + '_ilogccdf_dispatch': '_ilogcdf_dispatch', + '_iccdf_dispatch': '_icdf_dispatch'} + def wrapped(self, p, *args, loc, scale, sign, **kwargs): + item = func.__name__ + + f = getattr(self._dist, item) + cf = getattr(self._dist, citem[item]) + + # Obviously it's possible to get away with half of the work here. + # Let's focus on correct results first and optimize later. + fx = self._itransform(f(p, *args, **kwargs), loc, scale) + cfx = self._itransform(cf(p, *args, **kwargs), loc, scale) + return np.where(sign, fx, cfx)[()] + + return wrapped + + +class TransformedDistribution(ContinuousDistribution): + # TODO: This may need some sort of default `_parameterizations` with a + # single `_Parameterization` that has no parameters. The reason is + # that `dist`'s parameters need to get added to it. If they're not + # added, then those parameter kwargs are not recognized in + # `_update_parameters`. + def __init__(self, dist, *args, **kwargs): + self._copy_parameterization() + self._variable = dist._variable + self._dist = dist + if dist._parameterization: + # Add standard distribution parameters to our parameterization + dist_parameters = dist._parameterization.parameters + set_params = set(dist_parameters) + for parameterization in self._parameterizations: + if set_params.intersection(parameterization.parameters): + message = (f"One or more of the parameters of {dist} has " + "the same name as a parameter of " + f"{self.__class__.__name__}. Name collisions " + "create ambiguities and are not supported.") + raise ValueError(message) + parameterization.parameters.update(dist_parameters) + super().__init__(*args, **kwargs) + + def _overrides(self, method_name): + return (self._dist._overrides(method_name) + or super()._overrides(method_name)) + + def reset_cache(self): + self._dist.reset_cache() + super().reset_cache() + + def _update_parameters(self, *, iv_policy=None, **params): + # maybe broadcast everything before processing? + parameters = {} + # There may be some issues with _original_parameters + # We only want to update with _dist._original_parameters during + # initialization. Afterward that, we want to start with + # self._original_parameters. + parameters.update(self._dist._original_parameters) + parameters.update(params) + super()._update_parameters(iv_policy=iv_policy, **parameters) + + def _process_parameters(self, **params): + return self._dist._process_parameters(**params) + + def __repr__(self): + s = super().__repr__() + return s.replace(self.__class__.__name__, + self._dist.__class__.__name__) + + +class ShiftedScaledDistribution(TransformedDistribution): + """Distribution with a standard shift/scale transformation.""" + # Unclear whether infinite loc/scale will work reasonably in all cases + _loc_domain = _RealDomain(endpoints=(-oo, oo), inclusive=(True, True)) + _loc_param = _RealParameter('loc', symbol='µ', + domain=_loc_domain, typical=(1, 2)) + + _scale_domain = _RealDomain(endpoints=(-oo, oo), inclusive=(True, True)) + _scale_param = _RealParameter('scale', symbol='σ', + domain=_scale_domain, typical=(0.1, 10)) + + _parameterizations = [_Parameterization(_loc_param, _scale_param), + _Parameterization(_loc_param), + _Parameterization(_scale_param)] + + def _process_parameters(self, loc=None, scale=None, **params): + loc = loc if loc is not None else np.zeros_like(scale)[()] + scale = scale if scale is not None else np.ones_like(loc)[()] + sign = scale > 0 + parameters = self._dist._process_parameters(**params) + parameters.update(dict(loc=loc, scale=scale, sign=sign)) + return parameters + + def _transform(self, x, loc, scale, **kwargs): + return (x - loc)/scale + + def _itransform(self, x, loc, scale, **kwargs): + return x * scale + loc + + def _support(self, loc, scale, sign, **params): + # Add shortcut for infinite support? + a, b = self._dist._support(**params) + a, b = self._itransform(a, loc, scale), self._itransform(b, loc, scale) + return np.where(sign, a, b)[()], np.where(sign, b, a)[()] + + # Here, we override all the `_dispatch` methods rather than the public + # methods or _function methods. Why not the public methods? + # If we were to override the public methods, then other + # TransformedDistribution classes (which could transform a + # ShiftedScaledDistribution) would need to call the public methods of + # ShiftedScaledDistribution, which would run the input validation again. + # Why not the _function methods? For distributions that rely on the + # default implementation of methods (e.g. `quadrature`, `inversion`), + # the implementation would "see" the location and scale like other + # distribution parameters, so they could affect the accuracy of the + # calculations. I think it is cleaner if `loc` and `scale` do not affect + # the underlying calculations at all. + + def _entropy_dispatch(self, *args, loc, scale, sign, **params): + return (self._dist._entropy_dispatch(*args, **params) + + np.log(abs(scale))) + + def _logentropy_dispatch(self, *args, loc, scale, sign, **params): + lH0 = self._dist._logentropy_dispatch(*args, **params) + lls = np.log(np.log(abs(scale))+0j) + return special.logsumexp(np.broadcast_arrays(lH0, lls), axis=0) + + def _median_dispatch(self, *, method, loc, scale, sign, **params): + raw = self._dist._median_dispatch(method=method, **params) + return self._itransform(raw, loc, scale) + + def _mode_dispatch(self, *, method, loc, scale, sign, **params): + raw = self._dist._mode_dispatch(method=method, **params) + return self._itransform(raw, loc, scale) + + def _logpdf_dispatch(self, x, *args, loc, scale, sign, **params): + x = self._transform(x, loc, scale) + logpdf = self._dist._logpdf_dispatch(x, *args, **params) + return logpdf - np.log(abs(scale)) + + def _pdf_dispatch(self, x, *args, loc, scale, sign, **params): + x = self._transform(x, loc, scale) + pdf = self._dist._pdf_dispatch(x, *args, **params) + return pdf / abs(scale) + + # Sorry about the magic. This is just a draft to show the behavior. + @_shift_scale_distribution_function + def _logcdf_dispatch(self, x, *, method=None, **params): + pass + + @_shift_scale_distribution_function + def _cdf_dispatch(self, x, *, method=None, **params): + pass + + @_shift_scale_distribution_function + def _logccdf_dispatch(self, x, *, method=None, **params): + pass + + @_shift_scale_distribution_function + def _ccdf_dispatch(self, x, *, method=None, **params): + pass + + @_shift_scale_distribution_function_2arg + def _logcdf2_dispatch(self, x, y, *, method=None, **params): + pass + + @_shift_scale_distribution_function_2arg + def _cdf2_dispatch(self, x, y, *, method=None, **params): + pass + + @_shift_scale_distribution_function_2arg + def _logccdf2_dispatch(self, x, y, *, method=None, **params): + pass + + @_shift_scale_distribution_function_2arg + def _ccdf2_dispatch(self, x, y, *, method=None, **params): + pass + + @_shift_scale_inverse_function + def _ilogcdf_dispatch(self, x, *, method=None, **params): + pass + + @_shift_scale_inverse_function + def _icdf_dispatch(self, x, *, method=None, **params): + pass + + @_shift_scale_inverse_function + def _ilogccdf_dispatch(self, x, *, method=None, **params): + pass + + @_shift_scale_inverse_function + def _iccdf_dispatch(self, x, *, method=None, **params): + pass + + def _moment_standardized_dispatch(self, order, *, loc, scale, sign, methods, + **params): + res = (self._dist._moment_standardized_dispatch( + order, methods=methods, **params)) + return None if res is None else res * np.sign(scale)**order + + def _moment_central_dispatch(self, order, *, loc, scale, sign, methods, + **params): + res = (self._dist._moment_central_dispatch( + order, methods=methods, **params)) + return None if res is None else res * scale**order + + def _moment_raw_dispatch(self, order, *, loc, scale, sign, methods, + **params): + raw_moments = [] + methods_highest_order = methods + for i in range(int(order) + 1): + methods = (self._moment_methods if i < order + else methods_highest_order) + raw = self._dist._moment_raw_dispatch(i, methods=methods, **params) + if raw is None: + return None + moment_i = raw * scale**i + raw_moments.append(moment_i) + + return self._moment_transform_center( + order, raw_moments, loc, self._zero) + + def _sample_dispatch(self, sample_shape, full_shape, *, + method, rng, **params): + rvs = self._dist._sample_dispatch( + sample_shape, full_shape, method=method, rng=rng, **params) + return self._itransform(rvs, **params) + + def _qmc_sample_dispatch(self, length, full_shape, *, + method, qrng, **params): + rvs = self._dist._qmc_sample_dispatch( + length, full_shape, method=method, qrng=qrng, **params) + return self._itransform(rvs, **params) + + def __add__(self, loc): + return ShiftedScaledDistribution(self._dist, loc=self.loc + loc, + scale=self.scale) + + def __sub__(self, loc): + return ShiftedScaledDistribution(self._dist, loc=self.loc - loc, + scale=self.scale) + + def __mul__(self, scale): + return ShiftedScaledDistribution(self._dist, + loc=self.loc * scale, + scale=self.scale * scale) + + def __truediv__(self, scale): + return ShiftedScaledDistribution(self._dist, + loc=self.loc / scale, + scale=self.scale / scale) + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__add__(other) + + def __rmul__(self, other): + return self.__add__(other) + + def __rtruediv__(self, other): + return self.__add__(other) + + def __neg__(self): + return self * -1 diff --git a/scipy/stats/_entropy.py b/scipy/stats/_entropy.py index 1e6c8d9e1963..4d033bae4752 100644 --- a/scipy/stats/_entropy.py +++ b/scipy/stats/_entropy.py @@ -124,7 +124,7 @@ def entropy(pk: np.typing.ArrayLike, >>> D = entropy(pk, qk, base=base) >>> D 0.7369655941662062 - >>> D == np.sum(pk * np.log(pk/qk)) / np.log(base) + >>> np.isclose(D, np.sum(pk * np.log(pk/qk)) / np.log(base), rtol=4e-16, atol=0) True The cross entropy can be calculated as the sum of the entropy and diff --git a/scipy/stats/_fit.py b/scipy/stats/_fit.py index b23e33d74a2c..eb194f7eda06 100644 --- a/scipy/stats/_fit.py +++ b/scipy/stats/_fit.py @@ -983,7 +983,10 @@ def goodness_of_fit(dist, data, *, known_params=None, fit_params=None, >>> loc, scale = np.mean(x), np.std(x, ddof=1) >>> cdf = stats.norm(loc, scale).cdf >>> stats.ks_1samp(x, cdf) - KstestResult(statistic=0.1119257570456813, pvalue=0.2827756409939257) + KstestResult(statistic=0.1119257570456813, + pvalue=0.2827756409939257, + statistic_location=0.7751845155861765, + statistic_sign=-1) An advantage of the KS-test is that the p-value - the probability of obtaining a value of the test statistic under the null hypothesis as @@ -1237,9 +1240,10 @@ def _compute_dminus(cdfvals): return (cdfvals - np.arange(0.0, n)/n).max(axis=-1) -def _kolmogorov_smirnov(dist, data, axis): - x = np.sort(data, axis=-1) +def _kolmogorov_smirnov(dist, data, axis=-1): + x = np.sort(data, axis=axis) cdfvals = dist.cdf(x) + cdfvals = np.moveaxis(cdfvals, axis, -1) Dplus = _compute_dplus(cdfvals) # always works along last axis Dminus = _compute_dminus(cdfvals) return np.maximum(Dplus, Dminus) diff --git a/scipy/stats/_hypotests.py b/scipy/stats/_hypotests.py index 2bd5539cdfbf..d445c5494fcc 100644 --- a/scipy/stats/_hypotests.py +++ b/scipy/stats/_hypotests.py @@ -37,7 +37,8 @@ def epps_singleton_2samp(x, y, t=(0.4, 0.8)): ---------- x, y : array-like The two samples of observations to be tested. Input must not have more - than one dimension. Samples can have different lengths. + than one dimension. Samples can have different lengths, but both + must have at least five observations. t : array-like, optional The points (t1, ..., tn) where the empirical characteristic function is to be evaluated. It should be positive distinct numbers. The default @@ -501,6 +502,7 @@ def cramervonmises(rvs, cdf, args=()): ---------- rvs : array_like A 1-D array of observed values of the random variables :math:`X_i`. + The sample must contain at least two observations. cdf : str or callable The cumulative distribution function :math:`F` to test the observations against. If a string, it should be the name of a @@ -1556,8 +1558,10 @@ def cramervonmises_2samp(x, y, method='auto'): ---------- x : array_like A 1-D array of observed values of the random variables :math:`X_i`. + Must contain at least two observations. y : array_like A 1-D array of observed values of the random variables :math:`Y_i`. + Must contain at least two observations. method : {'auto', 'asymptotic', 'exact'}, optional The method used to compute the p-value, see Notes for details. The default is 'auto'. diff --git a/scipy/stats/_mannwhitneyu.py b/scipy/stats/_mannwhitneyu.py index bfc291560fde..19b7ce3883bd 100644 --- a/scipy/stats/_mannwhitneyu.py +++ b/scipy/stats/_mannwhitneyu.py @@ -436,7 +436,9 @@ def mannwhitneyu(x, y, use_continuity=True, alternative="two-sided", >>> from scipy.stats import ttest_ind >>> res = ttest_ind(females, males, alternative="less") >>> print(res) - Ttest_indResult(statistic=-2.239334696520584, pvalue=0.030068441095757924) + TtestResult(statistic=-2.239334696520584, + pvalue=0.030068441095757924, + df=7.0) Under this assumption, the *p*-value would be low enough to reject the null hypothesis in favor of the alternative. diff --git a/scipy/stats/_morestats.py b/scipy/stats/_morestats.py index 712515db52af..19f24929bb47 100644 --- a/scipy/stats/_morestats.py +++ b/scipy/stats/_morestats.py @@ -306,10 +306,6 @@ def kstat(data, n=2, *, axis=None): N = data.shape[axis] - # raise ValueError on empty input - if N == 0: - raise ValueError("Data input must not be empty") - S = [None] + [xp.sum(data**k, axis=axis) for k in range(1, n + 1)] if n == 1: return S[1] * 1.0/N @@ -377,10 +373,10 @@ def kstatvar(data, n=2, *, axis=None): N = data.shape[axis] if n == 1: - return kstat(data, n=2, axis=axis) * 1.0/N + return kstat(data, n=2, axis=axis, _no_deco=True) * 1.0/N elif n == 2: - k2 = kstat(data, n=2, axis=axis) - k4 = kstat(data, n=4, axis=axis) + k2 = kstat(data, n=2, axis=axis, _no_deco=True) + k4 = kstat(data, n=4, axis=axis, _no_deco=True) return (2*N*k2**2 + (N-1)*k4) / (N*(N+1)) else: raise ValueError("Only n=1 or n=2 supported.") @@ -1883,7 +1879,7 @@ def shapiro(x): Parameters ---------- x : array_like - Array of sample data. + Array of sample data. Must contain at least three observations. Returns ------- @@ -3061,17 +3057,11 @@ def bartlett(*samples, axis=0): if k < 2: raise ValueError("Must enter at least two input sample vectors.") - # Handle 1d empty input; _axis_nan_policy takes care of N-D - for sample in samples: - if xp_size(xp.asarray(sample)) == 0: - NaN = _get_nan(*samples, xp=xp) # get NaN of result_dtype of all samples - return BartlettResult(NaN, NaN) - samples = _broadcast_arrays(samples, axis=axis, xp=xp) samples = [xp_moveaxis_to_end(sample, axis, xp=xp) for sample in samples] Ni = [xp.asarray(sample.shape[-1], dtype=sample.dtype) for sample in samples] - Ni = [xp.broadcast_to(N, sample.shape[:-1]) for N in Ni] + Ni = [xp.broadcast_to(N, samples[0].shape[:-1]) for N in Ni] ssq = [xp.var(sample, correction=1, axis=-1) for sample in samples] Ni = [arr[xp.newaxis, ...] for arr in Ni] ssq = [arr[xp.newaxis, ...] for arr in ssq] @@ -3758,7 +3748,8 @@ def mood(x, y, axis=0, alternative="two-sided"): Parameters ---------- x, y : array_like - Arrays of sample data. + Arrays of sample data. There must be at least three observations + total. axis : int, optional The axis along which the samples are tested. `x` and `y` can be of different length along `axis`. @@ -4362,10 +4353,6 @@ def median_test(*samples, ties='below', correction=True, lambda_=1, def _circfuncs_common(samples, high, low, xp=None): xp = array_namespace(samples) if xp is None else xp - # Ensure samples are array-like and size is not zero - if xp_size(samples) == 0: - NaN = _get_nan(samples, xp=xp) - return NaN, NaN, NaN if xp.isdtype(samples.dtype, 'integral'): dtype = xp.asarray(1.).dtype # get default float type @@ -4458,6 +4445,10 @@ def circmean(samples, high=2*pi, low=0, axis=None, nan_policy='propagate'): """ xp = array_namespace(samples) + # Needed for non-NumPy arrays to get appropriate NaN result + # Apparently atan2(0, 0) is 0, even though it is mathematically undefined + if xp_size(samples) == 0: + return xp.mean(samples, axis=axis) samples, sin_samp, cos_samp = _circfuncs_common(samples, high, low, xp=xp) sin_sum = xp.sum(sin_samp, axis=axis) cos_sum = xp.sum(cos_samp, axis=axis) diff --git a/scipy/stats/_mstats_basic.py b/scipy/stats/_mstats_basic.py index 0ce0d9abb955..f2ab57298830 100644 --- a/scipy/stats/_mstats_basic.py +++ b/scipy/stats/_mstats_basic.py @@ -45,11 +45,10 @@ from scipy._lib._bunch import _make_tuple_bunch import scipy.special as special import scipy.stats._stats_py +import scipy.stats._stats_py as _stats_py from ._stats_mstats_common import ( _find_repeats, - linregress as stats_linregress, - LinregressResult as stats_LinregressResult, theilslopes as stats_theilslopes, siegelslopes as stats_siegelslopes ) @@ -93,11 +92,11 @@ def _ttest_finish(df, t, alternative): # We use ``stdtr`` directly here to preserve masked arrays if alternative == 'less': - pval = special.stdtr(df, t) + pval = special._ufuncs.stdtr(df, t) elif alternative == 'greater': - pval = special.stdtr(df, -t) + pval = special._ufuncs.stdtr(df, -t) elif alternative == 'two-sided': - pval = special.stdtr(df, -np.abs(t))*2 + pval = special._ufuncs.stdtr(df, -np.abs(t))*2 else: raise ValueError("alternative must be " "'less', 'greater' or 'two-sided'") @@ -1174,15 +1173,15 @@ def linregress(x, y=None): x = ma.array(x, mask=m) y = ma.array(y, mask=m) if np.any(~m): - result = stats_linregress(x.data[~m], y.data[~m]) + result = _stats_py.linregress(x.data[~m], y.data[~m]) else: # All data is masked - result = stats_LinregressResult(slope=None, intercept=None, - rvalue=None, pvalue=None, - stderr=None, - intercept_stderr=None) + result = _stats_py.LinregressResult(slope=None, intercept=None, + rvalue=None, pvalue=None, + stderr=None, + intercept_stderr=None) else: - result = stats_linregress(x.data, y.data) + result = _stats_py.linregress(x.data, y.data) return result diff --git a/scipy/stats/_new_distribution_docs.json b/scipy/stats/_new_distribution_docs.json new file mode 100644 index 000000000000..06b255f880c4 --- /dev/null +++ b/scipy/stats/_new_distribution_docs.json @@ -0,0 +1,5 @@ +{ + "Normal": "\nNormal distribution with prescribed mean and standard deviation.\n\nThe probability density function of the normal distribution is:\n\n.. math::\n\n f(x) = \\frac{1}{\\sigma \\sqrt{2 \\pi}} \\exp {\n \\left( -\\frac{1}{2}\\left( \\frac{x - \\mu}{\\sigma} \\right)^2 \\right)}\n\nfor :math:`x` in (-\u221e, \u221e).\nThis class accepts one parameterization:\n`mu` for :math:`\u00b5 \u2208 (-\u221e, \u221e)`, `sigma` for :math:`\u03c3 \u2208 (0, \u221e)`.\n\n\nParameters\n----------\ntol : positive float, optional\n The desired relative tolerance of calculations. Left unspecified,\n calculations may be faster; when provided, calculations may be\n more likely to meet the desired accuracy.\niv_policy : {None, \"skip_all\"}\n Specifies the level of input validation to perform. Left unspecified,\n input validation is performed to ensure appropriate behavior in edge\n case (e.g. parameters out of domain, argument outside of distribution\n support, etc.) and improve consistency of output dtype, shape, etc.\n Pass ``'skip_all'`` to avoid the computational overhead of these\n checks when rough edges are acceptable.\ncache_policy : {None, \"no_cache\"}\n Specifies the extent to which intermediate results are cached. Left\n unspecified, intermediate results of some calculations (e.g. distribution\n support, moments, etc.) are cached to improve performance of future\n calculations. Pass ``'no_cache'`` to reduce memory reserved by the class\n instance.\nrng : numpy.random.Generator\n Random number generator to be used by any methods that require\n pseudo-random numbers (e.g. `sample`).\n\nNotes\n-----\nThe following abbreviations are used throughout the documentation.\n\n- PDF: probability density function\n- CDF: cumulative distribution function\n- CCDF: complementary CDF\n- entropy: differential entropy\n- log-*F*: logarithm of *F* (e.g. log-CDF)\n- inverse *F*: inverse function of *F* (e.g. inverse CDF)\n\nThe API documentation is written to describe the API, not to serve as\na statistical reference. Effort is made to be correct at the level\nrequired to use the functionality, not to be mathematically rigorous.\nFor example, continuity and differentiability may be implicitly assumed.\nFor precise mathematical definitions, consult your preferred mathematical\ntext.\n\nExamples\n--------\nTo use the distribution class, it must be instantiated using keyword\nparameters corresponding with one of the accepted parameterizations.\n\n>>> import numpy as np\n>>> import matplotlib.pyplot as plt\n>>> from scipy import stats\n>>> from scipy.stats import Normal\n>>> X = Normal(mu=-0.81, sigma=0.69)\n\nFor convenience, the ``plot`` method can be used to visualize the density\nand other functions of the distribution.\n\n>>> X.plot()\n>>> plt.show()\n\nThe support of the underlying distribution is available using the ``support``\nmethod.\n\n>>> X.support()\n(-inf, inf)\n\nThe numerical values of parameters associated with all parameterizations\nare available as attributes.\n\n>>> X.mu, X.sigma\n(-0.81, 0.69)\n\nTo evaluate the probability density function of the underlying distribution\nat argument ``x=-1.13``:\n\n>>> x = -1.13\n>>> X.pdf(x)\n0.5192263911374636\n\nThe cumulative distribution function, its complement, and the logarithm\nof these functions are evaluated similarly.\n\n>>> np.allclose(np.exp(X.logccdf(x)), 1 - X.cdf(x))\nTrue\n\nThe inverse of these functions with respect to the argument ``x`` is also\navailable.\n\n>>> logp = np.log(1 - X.ccdf(x))\n>>> np.allclose(X.ilogcdf(logp), x)\nTrue\n\nNote that distribution functions and their logarithms also have two-argument\nversions for working with the probability mass between two arguments. The\nresult tends to be more accurate than the naive implementation because it avoids\nsubtractive cancellation.\n\n>>> y = -0.56\n>>> np.allclose(X.ccdf(x, y), 1 - (X.cdf(y) - X.cdf(x)))\nTrue\n\nThere are methods for computing measures of central tendency,\ndispersion, higher moments, and entropy.\n\n>>> X.mean(), X.median(), X.mode()\n(-0.81, -0.81, -0.81)\n>>> X.variance(), X.standard_deviation()\n(0.4760999999999999, 0.69)\n>>> X.skewness(), X.kurtosis()\n(0.0, 3.0)\n>>> np.allclose(X.moment(order=6, kind='standardized'),\n... X.moment(order=6, kind='central') / X.variance()**3)\nTrue\n>>> np.allclose(np.exp(X.logentropy()), X.entropy())\nTrue\n\nPseudo-random and quasi-Monte Carlo samples can be drawn from\nthe underlying distribution using ``sample``.\n\n>>> rng = np.random.default_rng(2354873452)\n>>> X.sample(shape=(4,), rng=rng)\narray([-1.62449664, -1.00878072, -0.70469216, -1.48426691])\n>>> n = 200\n>>> s = X.sample(shape=(n,), rng=rng, qmc_engine=stats.qmc.Halton)\n>>> assert np.count_nonzero(s < X.median()) == n/2\n\n\nAttributes\n----------\nAll parameters are available as attributes.\n\nMethods\n-------\nsupport\nplot\nsample\nfit\nmoment\nmean\nmedian\nmode\nvariance\nstandard_deviation\nskewness\nkurtosis\npdf\nlogpdf\ncdf\nicdf\nccdf\niccdf\nlogcdf\nilogcdf\nlogccdf\nilogccdf\nentropy\nlogentropy\n", + "Uniform": "\nUniform distribution.\n\nThe probability density function of the uniform distribution is:\n\n.. math::\n\n f(x; a, b) = \\frac{1}\n {b - a}\n\nfor :math:`x` in (a, b).\nThis class accepts one parameterization:\n`a` for :math:`a \u2208 (-\u221e, \u221e)`, `b` for :math:`b \u2208 (a, \u221e)`.\n\n\nParameters\n----------\ntol : positive float, optional\n The desired relative tolerance of calculations. Left unspecified,\n calculations may be faster; when provided, calculations may be\n more likely to meet the desired accuracy.\niv_policy : {None, \"skip_all\"}\n Specifies the level of input validation to perform. Left unspecified,\n input validation is performed to ensure appropriate behavior in edge\n case (e.g. parameters out of domain, argument outside of distribution\n support, etc.) and improve consistency of output dtype, shape, etc.\n Pass ``'skip_all'`` to avoid the computational overhead of these\n checks when rough edges are acceptable.\ncache_policy : {None, \"no_cache\"}\n Specifies the extent to which intermediate results are cached. Left\n unspecified, intermediate results of some calculations (e.g. distribution\n support, moments, etc.) are cached to improve performance of future\n calculations. Pass ``'no_cache'`` to reduce memory reserved by the class\n instance.\nrng : numpy.random.Generator\n Random number generator to be used by any methods that require\n pseudo-random numbers (e.g. `sample`).\n\nNotes\n-----\nThe following abbreviations are used throughout the documentation.\n\n- PDF: probability density function\n- CDF: cumulative distribution function\n- CCDF: complementary CDF\n- entropy: differential entropy\n- log-*F*: logarithm of *F* (e.g. log-CDF)\n- inverse *F*: inverse function of *F* (e.g. inverse CDF)\n\nThe API documentation is written to describe the API, not to serve as\na statistical reference. Effort is made to be correct at the level\nrequired to use the functionality, not to be mathematically rigorous.\nFor example, continuity and differentiability may be implicitly assumed.\nFor precise mathematical definitions, consult your preferred mathematical\ntext.\n\nExamples\n--------\nTo use the distribution class, it must be instantiated using keyword\nparameters corresponding with one of the accepted parameterizations.\n\n>>> import numpy as np\n>>> import matplotlib.pyplot as plt\n>>> from scipy import stats\n>>> from scipy.stats import Uniform\n>>> X = Uniform(a=0.09, b=188.73)\n\nFor convenience, the ``plot`` method can be used to visualize the density\nand other functions of the distribution.\n\n>>> X.plot()\n>>> plt.show()\n\nThe support of the underlying distribution is available using the ``support``\nmethod.\n\n>>> X.support()\n(0.09, 188.73)\n\nThe numerical values of parameters associated with all parameterizations\nare available as attributes.\n\n>>> X.a, X.b, X.ab\n(0.09, 188.73, 188.64)\n\nTo evaluate the probability density function of the underlying distribution\nat argument ``x=60.45``:\n\n>>> x = 60.45\n>>> X.pdf(x)\n0.005301102629346905\n\nThe cumulative distribution function, its complement, and the logarithm\nof these functions are evaluated similarly.\n\n>>> np.allclose(np.exp(X.logccdf(x)), 1 - X.cdf(x))\nTrue\n\nThe inverse of these functions with respect to the argument ``x`` is also\navailable.\n\n>>> logp = np.log(1 - X.ccdf(x))\n>>> np.allclose(X.ilogcdf(logp), x)\nTrue\n\nNote that distribution functions and their logarithms also have two-argument\nversions for working with the probability mass between two arguments. The\nresult tends to be more accurate than the naive implementation because it avoids\nsubtractive cancellation.\n\n>>> y = 120.82\n>>> np.allclose(X.ccdf(x, y), 1 - (X.cdf(y) - X.cdf(x)))\nTrue\n\nThere are methods for computing measures of central tendency,\ndispersion, higher moments, and entropy.\n\n>>> X.mean(), X.median(), X.mode()\n(94.41000000000001, 94.41, 94.41)\n>>> X.variance(), X.standard_deviation()\n(2965.4208, 54.4556773899655)\n>>> X.skewness(), X.kurtosis()\n(-1.182253748070285e-15, 1.7999999999999996)\n>>> np.allclose(X.moment(order=6, kind='standardized'),\n... X.moment(order=6, kind='central') / X.variance()**3)\nTrue\n>>> np.allclose(np.exp(X.logentropy()), X.entropy())\nTrue\n\nPseudo-random and quasi-Monte Carlo samples can be drawn from\nthe underlying distribution using ``sample``.\n\n>>> rng = np.random.default_rng(2354873452)\n>>> X.sample(shape=(4,), rng=rng)\narray([161.627692 , 27.17787751, 49.56323663, 38.16222048])\n>>> n = 200\n>>> s = X.sample(shape=(n,), rng=rng, qmc_engine=stats.qmc.Halton)\n>>> assert np.count_nonzero(s < X.median()) == n/2\n\n\nAttributes\n----------\nAll parameters are available as attributes.\n\nMethods\n-------\nsupport\nplot\nsample\nfit\nmoment\nmean\nmedian\nmode\nvariance\nstandard_deviation\nskewness\nkurtosis\npdf\nlogpdf\ncdf\nicdf\nccdf\niccdf\nlogcdf\nilogcdf\nlogccdf\nilogccdf\nentropy\nlogentropy\n", + "LogUniform": "\nLog-uniform distribution.\n\nThe probability density function of the log-uniform distribution is:\n\n.. math::\n\n f(x; a, b) = \\frac{1}\n {x (\\log(b) - \\log(a))}\n\nIf :math:`\\log(X)` is a random variable that follows a uniform distribution\nbetween :math:`\\log(a)` and :math:`\\log(b)`, then :math:`X` is log-uniformly\ndistributed with shape parameters :math:`a` and :math:`b`.\n\nfor :math:`x` in [a, b].\nThis class accepts two parameterizations:\n\n- `log_a` for :math:`\\log(a) \u2208 (-\u221e, \u221e)`, `log_b` for :math:`\\log(b) \u2208 (\\log(a), \u221e)`\n- `a` for :math:`a \u2208 (0, \u221e)`, `b` for :math:`b \u2208 (a, \u221e)`\n\n\nParameters\n----------\ntol : positive float, optional\n The desired relative tolerance of calculations. Left unspecified,\n calculations may be faster; when provided, calculations may be\n more likely to meet the desired accuracy.\niv_policy : {None, \"skip_all\"}\n Specifies the level of input validation to perform. Left unspecified,\n input validation is performed to ensure appropriate behavior in edge\n case (e.g. parameters out of domain, argument outside of distribution\n support, etc.) and improve consistency of output dtype, shape, etc.\n Pass ``'skip_all'`` to avoid the computational overhead of these\n checks when rough edges are acceptable.\ncache_policy : {None, \"no_cache\"}\n Specifies the extent to which intermediate results are cached. Left\n unspecified, intermediate results of some calculations (e.g. distribution\n support, moments, etc.) are cached to improve performance of future\n calculations. Pass ``'no_cache'`` to reduce memory reserved by the class\n instance.\nrng : numpy.random.Generator\n Random number generator to be used by any methods that require\n pseudo-random numbers (e.g. `sample`).\n\nNotes\n-----\nThe following abbreviations are used throughout the documentation.\n\n- PDF: probability density function\n- CDF: cumulative distribution function\n- CCDF: complementary CDF\n- entropy: differential entropy\n- log-*F*: logarithm of *F* (e.g. log-CDF)\n- inverse *F*: inverse function of *F* (e.g. inverse CDF)\n\nThe API documentation is written to describe the API, not to serve as\na statistical reference. Effort is made to be correct at the level\nrequired to use the functionality, not to be mathematically rigorous.\nFor example, continuity and differentiability may be implicitly assumed.\nFor precise mathematical definitions, consult your preferred mathematical\ntext.\n\nExamples\n--------\nTo use the distribution class, it must be instantiated using keyword\nparameters corresponding with one of the accepted parameterizations.\n\n>>> import numpy as np\n>>> import matplotlib.pyplot as plt\n>>> from scipy import stats\n>>> from scipy.stats import LogUniform\n>>> X = LogUniform(log_a=-2.72, log_b=0.64)\n\nFor convenience, the ``plot`` method can be used to visualize the density\nand other functions of the distribution.\n\n>>> X.plot()\n>>> plt.show()\n\nThe support of the underlying distribution is available using the ``support``\nmethod.\n\n>>> X.support()\n(0.06587475442640295, 1.8964808793049515)\n\nThe numerical values of parameters associated with all parameterizations\nare available as attributes.\n\n>>> X.a, X.b, X.log_a, X.log_b\n(0.06587475442640295, 1.8964808793049515, -2.72, 0.64)\n\nTo evaluate the probability density function of the underlying distribution\nat argument ``x=0.19``:\n\n>>> x = 0.19\n>>> X.pdf(x)\n1.5664160401002505\n\nThe cumulative distribution function, its complement, and the logarithm\nof these functions are evaluated similarly.\n\n>>> np.allclose(np.exp(X.logccdf(x)), 1 - X.cdf(x))\nTrue\n\nThe inverse of these functions with respect to the argument ``x`` is also\navailable.\n\n>>> logp = np.log(1 - X.ccdf(x))\n>>> np.allclose(X.ilogcdf(logp), x)\nTrue\n\nNote that distribution functions and their logarithms also have two-argument\nversions for working with the probability mass between two arguments. The\nresult tends to be more accurate than the naive implementation because it avoids\nsubtractive cancellation.\n\n>>> y = 0.57\n>>> np.allclose(X.ccdf(x, y), 1 - (X.cdf(y) - X.cdf(x)))\nTrue\n\nThere are methods for computing measures of central tendency,\ndispersion, higher moments, and entropy.\n\n>>> X.mean(), X.median(), X.mode()\n(0.544823251451949, 0.35345468195878005, 0.06587475442640298)\n>>> X.variance(), X.standard_deviation()\n(0.23773611311460957, 0.4875819040065059)\n>>> X.skewness(), X.kurtosis()\n(1.0901044819654708, 3.106819336127329)\n>>> np.allclose(X.moment(order=6, kind='standardized'),\n... X.moment(order=6, kind='central') / X.variance()**3)\nTrue\n>>> np.allclose(np.exp(X.logentropy()), X.entropy())\nTrue\n\nPseudo-random and quasi-Monte Carlo samples can be drawn from\nthe underlying distribution using ``sample``.\n\n>>> rng = np.random.default_rng(2354873452)\n>>> X.sample(shape=(4,), rng=rng)\narray([1.17030183, 0.10672299, 0.15900855, 0.12978593])\n>>> n = 200\n>>> s = X.sample(shape=(n,), rng=rng, qmc_engine=stats.qmc.Halton)\n>>> assert np.count_nonzero(s < X.median()) == n/2\n\n\nAttributes\n----------\nAll parameters are available as attributes.\n\nMethods\n-------\nsupport\nplot\nsample\nfit\nmoment\nmean\nmedian\nmode\nvariance\nstandard_deviation\nskewness\nkurtosis\npdf\nlogpdf\ncdf\nicdf\nccdf\niccdf\nlogcdf\nilogcdf\nlogccdf\nilogccdf\nentropy\nlogentropy\n" +} \ No newline at end of file diff --git a/scipy/stats/_new_distributions.py b/scipy/stats/_new_distributions.py new file mode 100644 index 000000000000..47849f8b1ee7 --- /dev/null +++ b/scipy/stats/_new_distributions.py @@ -0,0 +1,452 @@ +import sys +import json +import os + +import numpy as np +from scipy import special +from scipy.stats._distribution_infrastructure import ( + ContinuousDistribution, _RealDomain, _RealParameter, _Parameterization, + oo, TransformedDistribution, _combine_docs) + +__all__ = ['Normal', 'Uniform', 'LogUniform'] + +def factorial(n): + return special.gamma(n + 1) + + +class OrderStatisticDistribution(TransformedDistribution): + + # These should really be _IntegerDomain/_IntegerParameter + _r_domain = _RealDomain(endpoints=(1, 'n'), inclusive=(True, True)) + _r_param = _RealParameter('r', domain=_r_domain, typical=(1, 2)) + + _n_domain = _RealDomain(endpoints=(1, np.inf), inclusive=(True, True)) + _n_param = _RealParameter('n', domain=_n_domain, typical=(1, 4)) + + _r_domain.define_parameters(_n_param) + + _parameterizations = [_Parameterization(_r_param, _n_param)] + + def _overrides(self, method_name): + return method_name == '_pdf_formula' + + def _pdf_formula(self, x, r, n, **kwargs): + factor = factorial(n) / (factorial(r-1) * factorial(n-r)) + fX = self._dist._pdf_dispatch(x, **kwargs) + FX = self._dist._cdf_dispatch(x, **kwargs) + cFX = self._dist._ccdf_dispatch(x, **kwargs) + return factor * fX * FX**(r-1) * cFX**(n-r) + + +class Normal(ContinuousDistribution): + r"""Normal distribution with prescribed mean and standard deviation. + + The probability density function of the normal distribution is: + + .. math:: + + f(x) = \frac{1}{\sigma \sqrt{2 \pi}} \exp { + \left( -\frac{1}{2}\left( \frac{x - \mu}{\sigma} \right)^2 \right)} + + """ + # `ShiftedScaledDistribution` allows this to be generated automatically from + # an instance of `StandardNormal`, but the normal distribution is so frequently + # used that it's worth a bit of code duplication to get better performance. + _mu_domain = _RealDomain(endpoints=(-oo, oo)) + _sigma_domain = _RealDomain(endpoints=(0, oo)) + _x_support = _RealDomain(endpoints=(-oo, oo)) + + _mu_param = _RealParameter('mu', symbol=r'µ', domain=_mu_domain, + typical=(-1, 1)) + _sigma_param = _RealParameter('sigma', symbol=r'σ', domain=_sigma_domain, + typical=(0.5, 1.5)) + _x_param = _RealParameter('x', domain=_x_support, typical=(-1, 1)) + + _parameterizations = [_Parameterization(_mu_param, _sigma_param)] + + _variable = _x_param + _normalization = 1/np.sqrt(2*np.pi) + _log_normalization = np.log(2*np.pi)/2 + + def __new__(cls, mu=None, sigma=None, **kwargs): + if mu is None and sigma is None: + return super().__new__(StandardNormal) + return super().__new__(cls) + + def __init__(self, *, mu=0., sigma=1., **kwargs): + super().__init__(mu=mu, sigma=sigma, **kwargs) + + def _logpdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._logpdf_formula(self, (x - mu)/sigma) - np.log(sigma) + + def _pdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._pdf_formula(self, (x - mu)/sigma) / sigma + + def _logcdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._logcdf_formula(self, (x - mu)/sigma) + + def _cdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._cdf_formula(self, (x - mu)/sigma) + + def _logccdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._logccdf_formula(self, (x - mu)/sigma) + + def _ccdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._ccdf_formula(self, (x - mu)/sigma) + + def _icdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._icdf_formula(self, x) * sigma + mu + + def _ilogcdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._ilogcdf_formula(self, x) * sigma + mu + + def _iccdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._iccdf_formula(self, x) * sigma + mu + + def _ilogccdf_formula(self, x, *, mu, sigma, **kwargs): + return StandardNormal._ilogccdf_formula(self, x) * sigma + mu + + def _entropy_formula(self, *, mu, sigma, **kwargs): + return StandardNormal._entropy_formula(self) + np.log(abs(sigma)) + + def _logentropy_formula(self, *, mu, sigma, **kwargs): + lH0 = StandardNormal._logentropy_formula(self) + lls = np.log(np.log(abs(sigma))+0j) + return special.logsumexp(np.broadcast_arrays(lH0, lls), axis=0) + + def _median_formula(self, *, mu, sigma, **kwargs): + return mu + + def _mode_formula(self, *, mu, sigma, **kwargs): + return mu + + def _moment_raw_formula(self, order, *, mu, sigma, **kwargs): + if order == 0: + return np.ones_like(mu) + elif order == 1: + return mu + else: + return None + _moment_raw_formula.orders = [0, 1] + + def _moment_central_formula(self, order, *, mu, sigma, **kwargs): + if order == 0: + return np.ones_like(mu) + elif order % 2: + return np.zeros_like(mu) + else: + # exact is faster (and obviously more accurate) for reasonable orders + return sigma**order * special.factorial2(int(order) - 1, exact=True) + + def _sample_formula(self, sample_shape, full_shape, rng, *, mu, sigma, **kwargs): + return rng.normal(loc=mu, scale=sigma, size=full_shape)[()] + + +def _log_diff(log_p, log_q): + return special.logsumexp([log_p, log_q+np.pi*1j], axis=0) + + +class StandardNormal(Normal): + r"""Standard normal distribution. + + The probability density function of the standard normal distribution is: + + .. math:: + + f(x) = \frac{1}{\sqrt{2 \pi}} \exp \left( -\frac{1}{2} x^2 \right) + + """ + _x_support = _RealDomain(endpoints=(-oo, oo)) + _x_param = _RealParameter('x', domain=_x_support, typical=(-5, 5)) + _variable = _x_param + _parameterizations = [] + _normalization = 1/np.sqrt(2*np.pi) + _log_normalization = np.log(2*np.pi)/2 + mu = np.float64(0.) + sigma = np.float64(1.) + + def __init__(self, **kwargs): + ContinuousDistribution.__init__(self, **kwargs) + + def _logpdf_formula(self, x, **kwargs): + return -(self._log_normalization + x**2/2) + + def _pdf_formula(self, x, **kwargs): + return self._normalization * np.exp(-x**2/2) + + def _logcdf_formula(self, x, **kwargs): + return special.log_ndtr(x) + + def _cdf_formula(self, x, **kwargs): + return special.ndtr(x) + + def _logccdf_formula(self, x, **kwargs): + return special.log_ndtr(-x) + + def _ccdf_formula(self, x, **kwargs): + return special.ndtr(-x) + + def _icdf_formula(self, x, **kwargs): + return special.ndtri(x) + + def _ilogcdf_formula(self, x, **kwargs): + return special.ndtri_exp(x) + + def _iccdf_formula(self, x, **kwargs): + return -special.ndtri(x) + + def _ilogccdf_formula(self, x, **kwargs): + return -special.ndtri_exp(x) + + def _entropy_formula(self, **kwargs): + return (1 + np.log(2*np.pi))/2 + + def _logentropy_formula(self, **kwargs): + return np.log1p(np.log(2*np.pi)) - np.log(2) + + def _median_formula(self, **kwargs): + return 0 + + def _mode_formula(self, **kwargs): + return 0 + + def _moment_raw_formula(self, order, **kwargs): + raw_moments = {0: 1, 1: 0, 2: 1, 3: 0, 4: 3, 5: 0} + return raw_moments.get(order, None) + + def _moment_central_formula(self, order, **kwargs): + return self._moment_raw_formula(order, **kwargs) + + def _moment_standardized_formula(self, order, **kwargs): + return self._moment_raw_formula(order, **kwargs) + + def _sample_formula(self, sample_shape, full_shape, rng, **kwargs): + return rng.normal(size=full_shape)[()] + + +class LogUniform(ContinuousDistribution): + r"""Log-uniform distribution. + + The probability density function of the log-uniform distribution is: + + .. math:: + + f(x; a, b) = \frac{1} + {x (\log(b) - \log(a))} + + If :math:`\log(X)` is a random variable that follows a uniform distribution + between :math:`\log(a)` and :math:`\log(b)`, then :math:`X` is log-uniformly + distributed with shape parameters :math:`a` and :math:`b`. + + """ + + _a_domain = _RealDomain(endpoints=(0, oo)) + _b_domain = _RealDomain(endpoints=('a', oo)) + _log_a_domain = _RealDomain(endpoints=(-oo, oo)) + _log_b_domain = _RealDomain(endpoints=('log_a', oo)) + _x_support = _RealDomain(endpoints=('a', 'b'), inclusive=(True, True)) + + _a_param = _RealParameter('a', domain=_a_domain, typical=(1e-3, 0.9)) + _b_param = _RealParameter('b', domain=_b_domain, typical=(1.1, 1e3)) + _log_a_param = _RealParameter('log_a', symbol=r'\log(a)', + domain=_log_a_domain, typical=(-3, -0.1)) + _log_b_param = _RealParameter('log_b', symbol=r'\log(b)', + domain=_log_b_domain, typical=(0.1, 3)) + _x_param = _RealParameter('x', domain=_x_support, typical=('a', 'b')) + + _b_domain.define_parameters(_a_param) + _log_b_domain.define_parameters(_log_a_param) + _x_support.define_parameters(_a_param, _b_param) + + _parameterizations = [_Parameterization(_log_a_param, _log_b_param), + _Parameterization(_a_param, _b_param)] + _variable = _x_param + + def __init__(self, *, a=None, b=None, log_a=None, log_b=None, **kwargs): + super().__init__(a=a, b=b, log_a=log_a, log_b=log_b, **kwargs) + + def _process_parameters(self, a=None, b=None, log_a=None, log_b=None, **kwargs): + a = np.exp(log_a) if a is None else a + b = np.exp(log_b) if b is None else b + log_a = np.log(a) if log_a is None else log_a + log_b = np.log(b) if log_b is None else log_b + kwargs.update(dict(a=a, b=b, log_a=log_a, log_b=log_b)) + return kwargs + + # def _logpdf_formula(self, x, *, log_a, log_b, **kwargs): + # return -np.log(x) - np.log(log_b - log_a) + + def _pdf_formula(self, x, *, log_a, log_b, **kwargs): + return ((log_b - log_a)*x)**-1 + + # def _cdf_formula(self, x, *, log_a, log_b, **kwargs): + # return (np.log(x) - log_a)/(log_b - log_a) + + def _moment_raw_formula(self, order, log_a, log_b, **kwargs): + if order == 0: + return self._one + t1 = self._one / (log_b - log_a) / order + t2 = np.real(np.exp(_log_diff(order * log_b, order * log_a))) + return t1 * t2 + + +class LogLaplace(ContinuousDistribution): + """Log-Laplace distribution.""" + + _mu_domain = _RealDomain(endpoints=(-oo, oo)) + _b_domain = _RealDomain(endpoints=(0, oo)) + _x_support = _RealDomain(endpoints=(0, np.inf), inclusive=(False, False)) + + _mu_param = _RealParameter('mu', domain=_mu_domain, symbol='µ', typical=(-1e2, 1e2)) + _b_param = _RealParameter('b', domain=_b_domain, typical=(1, 10)) + _x_param = _RealParameter('x', domain=_x_support, typical=(1, 10)) + + _parameterizations = [_Parameterization(_mu_param, _b_param)] + _variable = _x_param + + def _pdf_formula(self, x, *, mu, b, **kwargs): + return 1/(2*b*x) * np.exp(-np.abs(np.log(x) - mu)/b) + + def _moment_raw_formula(self, order, *, mu, b, **kwargs): + with np.errstate(divide='ignore'): + c2, n2 = b**-2, order**2 + return np.where(n2 < c2, c2 / (c2 - n2), np.inf) + + +class Uniform(ContinuousDistribution): + r"""Uniform distribution. + + The probability density function of the uniform distribution is: + + .. math:: + + f(x; a, b) = \frac{1} + {b - a} + + """ + + _a_domain = _RealDomain(endpoints=(-oo, oo)) + _b_domain = _RealDomain(endpoints=('a', oo)) + _x_support = _RealDomain(endpoints=('a', 'b'), inclusive=(False, False)) + + _a_param = _RealParameter('a', domain=_a_domain, typical=(1e-3, 0.9)) + _b_param = _RealParameter('b', domain=_b_domain, typical=(1.1, 1e3)) + _x_param = _RealParameter('x', domain=_x_support, typical=('a', 'b')) + + _b_domain.define_parameters(_a_param) + _x_support.define_parameters(_a_param, _b_param) + + _parameterizations = [_Parameterization(_a_param, _b_param)] + _variable = _x_param + + def __init__(self, *, a=None, b=None, **kwargs): + super().__init__(a=a, b=b, **kwargs) + + def _process_parameters(self, a=None, b=None, ab=None, **kwargs): + ab = b - a + kwargs.update(dict(a=a, b=b, ab=ab)) + return kwargs + + def _pdf_formula(self, x, *, ab, **kwargs): + return np.full(x.shape, 1/ab) + + def _icdf_formula(self, x, a, b, ab, **kwargs): + return a + ab*x + + def _mode_formula(self, *, a, b, ab, **kwargs): + return a + 0.5*ab + +# class CircularDistribution(ShiftedScaledDistribution): +# """Class that represents a circular statistical distribution.""" +# # Define 2-arg cdf functions +# # Define 2-arg inverse CDF - one argument is left quantile +# # Define mean, median, mode, variance, standard_deviation, entropy +# # Raise error on use of moment functions? +# # Should support be -inf, inf because that is the range of values that +# # produce nonzero pdf? Or should support be the left and right wrap +# # points? The trouble with left and right wrap points is that this +# # triggers `_set_invalid_nan` to zero the pdf. We'd need to adjust +# # `_set_invalid_nan` for circular distributions. (We probably need to +# # do that anyway.) The nice thing about using the left and right wrap +# # points is that some other methods would begin to do sensible things +# # by default. For example, I think `qmc_sample` would begin to work. +# _a_domain = _RealDomain(endpoints=(-oo, oo), inclusive=(True, True)) +# _a_param = _RealParameter('a', domain=_a_domain) +# +# _b_domain = _RealDomain(endpoints=('a', oo), inclusive=(True, True)) +# _b_param = _RealParameter('b', domain=_b_domain) +# +# _parameterizations = [_Parameterization(_a_param, _b_param)] +# +# def _process_parameters(self, a, b, **kwargs): +# scale = b - a +# parameters = self._dist._process_parameters(**kwargs) +# parameters.update(dict(a=a, b=b, scale=scale)) +# return parameters +# +# def _transform(self, x, a, b, scale, **kwargs): +# x01 = (x - a)/scale # shift/scale to 0-1 +# x01 %= 1 # wrap to 0-1 +# return 2*np.pi*x01 - np.pi # shift/scale to -π, π +# +# def _itransform(self, x, a, b, scale, **kwargs): +# x01 = (x + np.pi)/(2*np.pi) # shift/scale to 0-1 +# return scale*x01 + a # shift/scale to a, b +# +# def _support(self, a, b, scale, **kwargs): +# return np.full_like(a, -np.inf), np.full_like(b, np.inf) +# +# def _pdf_dispatch(self, x, *args, a, b, scale, **kwargs): +# x = self._transform(x, a, b, scale) +# pdf = self._dist._pdf_dispatch(x, *args, **kwargs) +# return pdf / abs(scale) * 2*np.pi +# +# def _sample_dispatch(self, sample_shape, full_shape, *, +# method, rng, **kwargs): +# rvs = self._dist._sample_dispatch( +# sample_shape, full_shape, method=method, rng=rng, **kwargs) +# return self._itransform(rvs, **kwargs) +# + +# class VonMises(CircularDistribution): +# def __init__(self, *args, mu, kappa, a=-np.pi, b=np.pi, **kwargs): +# super().__init__(_VonMises(mu=mu, kappa=kappa), *args, +# a=a, b=b, **kwargs) +# +# +# class _VonMises(ContinuousDistribution): +# +# _mu_domain = _RealDomain(endpoints=(-np.pi, np.pi), inclusive=(True, True)) +# _kappa_domain = _RealDomain(endpoints=(0, oo), inclusive=(False, False)) +# +# _mu_param = _RealParameter('mu', symbol='µ', domain=_mu_domain, +# typical=(-1, 1)) +# _kappa_param = _RealParameter('kappa', symbol='κ', domain=_kappa_domain, +# typical=(0.1, 10)) +# _x_param = _RealParameter('x', domain=_mu_domain, typical=(-1, 1)) +# +# _parameterizations = [_Parameterization(_mu_param, _kappa_param)] +# _variable = _x_param +# +# def _pdf_formula(self, x, mu, kappa, **kwargs): +# return np.exp(kappa * np.cos(x - mu))/(2*np.pi*special.i0(kappa)) +# +# def _sample_formula(self, sample_shape, full_shape, rng, mu, kappa, **kwargs): +# return rng.vonmises(mu=mu, kappa=kappa, size=full_shape)[()] + +_docfile = "_new_distribution_docs.json" +_docdir = os.path.dirname(__file__) +_docpath = os.path.abspath(os.path.join(_docdir, _docfile)) +_module = sys.modules[__name__].__dict__ + +if __name__ == "__main__": + docs = {} + for dist_name in __all__: + docs[dist_name] = _combine_docs(_module[dist_name]) + with open(_docpath, 'w') as f: + json.dump(docs, f, indent=" ") + +with open(_docpath, 'r') as f: + docs = json.load(f) + for dist_name in __all__: + _module[dist_name].__doc__ = docs[dist_name] diff --git a/scipy/stats/_qmc_cy.pyx b/scipy/stats/_qmc_cy.pyx index 72304005eadb..45749967e94d 100644 --- a/scipy/stats/_qmc_cy.pyx +++ b/scipy/stats/_qmc_cy.pyx @@ -38,27 +38,27 @@ from libcpp.vector cimport vector cdef mutex threaded_sum_mutex -def _cy_wrapper_centered_discrepancy(double[:, ::1] sample, bint iterative, +def _cy_wrapper_centered_discrepancy(const double[:, ::1] sample, bint iterative, workers): return centered_discrepancy(sample, iterative, workers) -def _cy_wrapper_wrap_around_discrepancy(double[:, ::1] sample, +def _cy_wrapper_wrap_around_discrepancy(const double[:, ::1] sample, bint iterative, workers): return wrap_around_discrepancy(sample, iterative, workers) -def _cy_wrapper_mixture_discrepancy(double[:, ::1] sample, +def _cy_wrapper_mixture_discrepancy(const double[:, ::1] sample, bint iterative, workers): return mixture_discrepancy(sample, iterative, workers) -def _cy_wrapper_l2_star_discrepancy(double[:, ::1] sample, +def _cy_wrapper_l2_star_discrepancy(const double[:, ::1] sample, bint iterative, workers): return l2_star_discrepancy(sample, iterative, workers) -cdef double centered_discrepancy(double[:, ::1] sample_view, +cdef double centered_discrepancy(const double[:, ::1] sample_view, bint iterative, unsigned int workers) noexcept nogil: cdef: Py_ssize_t n = sample_view.shape[0] @@ -85,7 +85,7 @@ cdef double centered_discrepancy(double[:, ::1] sample_view, + 1.0 / (n ** 2) * disc2) -cdef double centered_discrepancy_loop(double[:, ::1] sample_view, +cdef double centered_discrepancy_loop(const double[:, ::1] sample_view, Py_ssize_t istart, Py_ssize_t istop) noexcept nogil: cdef: @@ -106,7 +106,7 @@ cdef double centered_discrepancy_loop(double[:, ::1] sample_view, return disc2 -cdef double wrap_around_discrepancy(double[:, ::1] sample_view, +cdef double wrap_around_discrepancy(const double[:, ::1] sample_view, bint iterative, unsigned int workers) noexcept nogil: cdef: Py_ssize_t n = sample_view.shape[0] @@ -122,7 +122,7 @@ cdef double wrap_around_discrepancy(double[:, ::1] sample_view, return - (4.0 / 3.0) ** d + 1.0 / (n ** 2) * disc -cdef double wrap_around_loop(double[:, ::1] sample_view, +cdef double wrap_around_loop(const double[:, ::1] sample_view, Py_ssize_t istart, Py_ssize_t istop) noexcept nogil: cdef: @@ -140,7 +140,7 @@ cdef double wrap_around_loop(double[:, ::1] sample_view, return disc -cdef double mixture_discrepancy(double[:, ::1] sample_view, +cdef double mixture_discrepancy(const double[:, ::1] sample_view, bint iterative, unsigned int workers) noexcept nogil: cdef: Py_ssize_t n = sample_view.shape[0] @@ -169,7 +169,7 @@ cdef double mixture_discrepancy(double[:, ::1] sample_view, return disc - disc1 + disc2 -cdef double mixture_loop(double[:, ::1] sample_view, Py_ssize_t istart, +cdef double mixture_loop(const double[:, ::1] sample_view, Py_ssize_t istart, Py_ssize_t istop) noexcept nogil: cdef: @@ -192,7 +192,7 @@ cdef double mixture_loop(double[:, ::1] sample_view, Py_ssize_t istart, return disc2 -cdef double l2_star_discrepancy(double[:, ::1] sample_view, +cdef double l2_star_discrepancy(const double[:, ::1] sample_view, bint iterative, unsigned int workers) noexcept nogil: cdef: Py_ssize_t n = sample_view.shape[0] @@ -218,7 +218,7 @@ cdef double l2_star_discrepancy(double[:, ::1] sample_view, ) -cdef double l2_star_loop(double[:, ::1] sample_view, Py_ssize_t istart, +cdef double l2_star_loop(const double[:, ::1] sample_view, Py_ssize_t istart, Py_ssize_t istop) noexcept nogil: cdef: @@ -240,14 +240,14 @@ cdef double l2_star_loop(double[:, ::1] sample_view, Py_ssize_t istart, return disc2 -def _cy_wrapper_update_discrepancy(double[::1] x_new_view, - double[:, ::1] sample_view, +def _cy_wrapper_update_discrepancy(const double[::1] x_new_view, + const double[:, ::1] sample_view, double initial_disc): return c_update_discrepancy(x_new_view, sample_view, initial_disc) -cdef double c_update_discrepancy(double[::1] x_new_view, - double[:, ::1] sample_view, +cdef double c_update_discrepancy(const double[::1] x_new_view, + const double[:, ::1] sample_view, double initial_disc) noexcept: cdef: Py_ssize_t n = sample_view.shape[0] + 1 @@ -289,12 +289,12 @@ cdef double c_update_discrepancy(double[::1] x_new_view, return initial_disc + disc1 + disc2 + disc3 -ctypedef double (*func_type)(double[:, ::1], Py_ssize_t, +ctypedef double (*func_type)(const double[:, ::1], Py_ssize_t, Py_ssize_t) noexcept nogil cdef double threaded_loops(func_type loop_func, - double[:, ::1] sample_view, + const double[:, ::1] sample_view, unsigned int workers) noexcept nogil: cdef: Py_ssize_t n = sample_view.shape[0] @@ -325,7 +325,7 @@ cdef double threaded_loops(func_type loop_func, cdef void one_thread_loop(func_type loop_func, double& disc, - double[:, ::1] sample_view, + const double[:, ::1] sample_view, Py_ssize_t istart, Py_ssize_t istop, _) noexcept nogil: @@ -393,7 +393,7 @@ cdef _cy_van_der_corput_threaded_loop(Py_ssize_t istart, def _cy_van_der_corput_scrambled(Py_ssize_t n, long base, long start_index, - np.int64_t[:,::1] permutations, + const np.int64_t[:,::1] permutations, unsigned int workers): sequence = np.zeros(n) @@ -427,7 +427,7 @@ cdef _cy_van_der_corput_scrambled_loop(Py_ssize_t istart, Py_ssize_t istop, long base, long start_index, - np.int64_t[:,::1] permutations, + const np.int64_t[:,::1] permutations, double[::1] sequence_view): cdef: diff --git a/scipy/stats/_rcont/rcont.pyx b/scipy/stats/_rcont/rcont.pyx index c8cc9da7711e..c71f85818bda 100644 --- a/scipy/stats/_rcont/rcont.pyx +++ b/scipy/stats/_rcont/rcont.pyx @@ -41,7 +41,7 @@ cdef bitgen_t* get_bitgen(random_state): return PyCapsule_GetPointer(capsule, capsule_name) -def rvs_rcont1(tab_t[::1] row, tab_t[::1] col, tab_t ntot, +def rvs_rcont1(const tab_t[::1] row, const tab_t[::1] col, tab_t ntot, int size, random_state): cdef: @@ -69,7 +69,7 @@ def rvs_rcont1(tab_t[::1] row, tab_t[::1] col, tab_t ntot, return result -def rvs_rcont2(tab_t[::1] row, tab_t[::1] col, tab_t ntot, +def rvs_rcont2(const tab_t[::1] row, const tab_t[::1] col, tab_t ntot, int size, random_state): cdef: bitgen_t *rstate = get_bitgen(random_state) diff --git a/scipy/stats/_resampling.py b/scipy/stats/_resampling.py index d89a0b993038..d7d4d50a7b7f 100644 --- a/scipy/stats/_resampling.py +++ b/scipy/stats/_resampling.py @@ -185,9 +185,23 @@ def _bootstrap_iv(data, statistic, vectorized, paired, axis, confidence_level, if n_samples == 0: raise ValueError("`data` must contain at least one sample.") + message = ("Ignoring the dimension specified by `axis`, arrays in `data` do not " + "have the same shape. Beginning in SciPy 1.16.0, `bootstrap` will " + "explicitly broadcast elements of `data` to the same shape (ignoring " + "`axis`) before performing the calculation. To avoid this warning in " + "the meantime, ensure that all samples have the same shape (except " + "potentially along `axis`).") + data = [np.atleast_1d(sample) for sample in data] + reduced_shapes = set() + for sample in data: + reduced_shape = list(sample.shape) + reduced_shape.pop(axis) + reduced_shapes.add(tuple(reduced_shape)) + if len(reduced_shapes) != 1: + warnings.warn(message, FutureWarning, stacklevel=3) + data_iv = [] for sample in data: - sample = np.atleast_1d(sample) if sample.shape[axis_int] <= 1: raise ValueError("each sample in `data` must contain two or more " "observations along `axis`.") @@ -315,7 +329,18 @@ def bootstrap(data, statistic, *, n_resamples=9999, batch=None, Parameters ---------- data : sequence of array-like - Each element of data is a sample from an underlying distribution. + Each element of `data` is a sample containing scalar observations from an + underlying distribution. Elements of `data` must be broadcastable to the + same shape (with the possible exception of the dimension specified by `axis`). + + .. versionchanged:: 1.14.0 + `bootstrap` will now emit a ``FutureWarning`` if the shapes of the + elements of `data` are not the same (with the exception of the dimension + specified by `axis`). + Beginning in SciPy 1.16.0, `bootstrap` will explicitly broadcast the + elements to the same shape (except along `axis`) before performing + the calculation. + statistic : callable Statistic for which the confidence interval is to be calculated. `statistic` must be a callable that accepts ``len(data)`` samples diff --git a/scipy/stats/_rvs_sampling.py b/scipy/stats/_rvs_sampling.py deleted file mode 100644 index 86adb251c3e5..000000000000 --- a/scipy/stats/_rvs_sampling.py +++ /dev/null @@ -1,56 +0,0 @@ -import warnings -from scipy.stats.sampling import RatioUniforms - -def rvs_ratio_uniforms(pdf, umax, vmin, vmax, size=1, c=0, random_state=None): - """ - Generate random samples from a probability density function using the - ratio-of-uniforms method. - - .. deprecated:: 1.12.0 - `rvs_ratio_uniforms` is deprecated in favour of - `scipy.stats.sampling.RatioUniforms` from version 1.12.0 and will - be removed in SciPy 1.15.0 - - Parameters - ---------- - pdf : callable - A function with signature `pdf(x)` that is proportional to the - probability density function of the distribution. - umax : float - The upper bound of the bounding rectangle in the u-direction. - vmin : float - The lower bound of the bounding rectangle in the v-direction. - vmax : float - The upper bound of the bounding rectangle in the v-direction. - size : int or tuple of ints, optional - Defining number of random variates (default is 1). - c : float, optional. - Shift parameter of ratio-of-uniforms method, see Notes. Default is 0. - random_state : {None, int, `numpy.random.Generator`, - `numpy.random.RandomState`}, optional - - If `seed` is None (or `np.random`), the `numpy.random.RandomState` - singleton is used. - If `seed` is an int, a new ``RandomState`` instance is used, - seeded with `seed`. - If `seed` is already a ``Generator`` or ``RandomState`` instance then - that instance is used. - - Returns - ------- - rvs : ndarray - The random variates distributed according to the probability - distribution defined by the pdf. - - Notes - ----- - Please refer to `scipy.stats.sampling.RatioUniforms` for the documentation. - """ - warnings.warn("Please use `RatioUniforms` from the " - "`scipy.stats.sampling` namespace. The " - "`scipy.stats.rvs_ratio_uniforms` namespace is deprecated " - "and will be removed in SciPy 1.15.0", - category=DeprecationWarning, stacklevel=2) - gen = RatioUniforms(pdf, umax=umax, vmin=vmin, vmax=vmax, - c=c, random_state=random_state) - return gen.rvs(size) diff --git a/scipy/stats/_sobol.pyx b/scipy/stats/_sobol.pyx index 2b617270972d..a9e2599555b9 100644 --- a/scipy/stats/_sobol.pyx +++ b/scipy/stats/_sobol.pyx @@ -297,7 +297,7 @@ def _draw( num_gen, const int dim, const cnp.float64_t scale, - uint_32_64[:, ::1] sv, + const uint_32_64[:, ::1] sv, uint_32_64[::1] quasi, cnp.float64_t[:, ::1] sample ): @@ -314,7 +314,7 @@ cdef void draw( const uint_32_64 num_gen, const int dim, const cnp.float64_t scale, - uint_32_64[:, ::1] sv, + const uint_32_64[:, ::1] sv, uint_32_64[::1] quasi, cnp.float64_t[:, ::1] sample ) noexcept nogil: @@ -336,7 +336,7 @@ cdef void draw( cpdef void _fast_forward(const uint_32_64 n, const uint_32_64 num_gen, const int dim, - uint_32_64[:, ::1] sv, + const uint_32_64[:, ::1] sv, uint_32_64[::1] quasi) noexcept nogil: cdef int j, l cdef uint_32_64 num_gen_loc = num_gen @@ -350,7 +350,7 @@ cpdef void _fast_forward(const uint_32_64 n, @cython.boundscheck(False) @cython.wraparound(False) -cdef uint_32_64 cdot_pow2(uint_32_64[::1] a) noexcept nogil: +cdef uint_32_64 cdot_pow2(const uint_32_64[::1] a) noexcept nogil: cdef int i cdef int size = a.shape[0] cdef uint_32_64 z = 0 @@ -394,7 +394,7 @@ cpdef void _cscramble(const int dim, @cython.boundscheck(False) @cython.wraparound(False) -cpdef void _fill_p_cumulative(cnp.float_t[::1] p, +cpdef void _fill_p_cumulative(const cnp.float_t[::1] p, cnp.float_t[::1] p_cumulative) noexcept nogil: cdef int i cdef int len_p = p.shape[0] @@ -408,8 +408,8 @@ cpdef void _fill_p_cumulative(cnp.float_t[::1] p, @cython.boundscheck(False) @cython.wraparound(False) -cpdef void _categorize(cnp.float_t[::1] draws, - cnp.float_t[::1] p_cumulative, +cpdef void _categorize(const cnp.float_t[::1] draws, + const cnp.float_t[::1] p_cumulative, cnp.intp_t[::1] result) noexcept nogil: cdef int i cdef int n_p = p_cumulative.shape[0] @@ -420,7 +420,7 @@ cpdef void _categorize(cnp.float_t[::1] draws, @cython.boundscheck(False) @cython.wraparound(False) -cdef int _find_index(cnp.float_t[::1] p_cumulative, +cdef int _find_index(const cnp.float_t[::1] p_cumulative, const int size, const float value) noexcept nogil: cdef int l = 0 diff --git a/scipy/stats/_stats.pyx b/scipy/stats/_stats.pyx index 2274d114eb02..567a2bceadac 100644 --- a/scipy/stats/_stats.pyx +++ b/scipy/stats/_stats.pyx @@ -172,12 +172,12 @@ def _toint64(x): @cython.wraparound(False) @cython.boundscheck(False) -def _weightedrankedtau(ordered[:] x, ordered[:] y, intp_t[:] rank, weigher, bool additive): +def _weightedrankedtau(const ordered[:] x, const ordered[:] y, intp_t[:] rank, weigher, bool additive): # y_local and rank_local (declared below) are a work-around for a Cython # bug; see gh-16718. When we can require Cython 3.0, y_local and # rank_local can be removed, and the closure weigh() can refer directly # to y and rank. - cdef ordered[:] y_local = y + cdef const ordered[:] y_local = y cdef intp_t i, first cdef float64_t t, u, v, w, s, sq cdef int64_t n = np.int64(len(x)) @@ -377,8 +377,8 @@ def _transform_distance_matrix(distx, disty, global_corr='mgc', is_ranked=True): # MGC specific functions @cython.wraparound(False) @cython.boundscheck(False) -cdef _expected_covar(float64_t[:, :] distx, float64_t[:, :] disty, - int64_t[:, :] rank_distx, int64_t[:, :] rank_disty, +cdef _expected_covar(const float64_t[:, :] distx, const float64_t[:, :] disty, + const int64_t[:, :] rank_distx, const int64_t[:, :] rank_disty, float64_t[:, :] cov_xy, float64_t[:] expectx, float64_t[:] expecty): # summing up the element-wise product of A and B based on the ranks, @@ -712,8 +712,8 @@ ctypedef fused real: @cython.cdivision(True) @cython.boundscheck(False) cdef inline int gaussian_kernel_estimate_inner( - real[:, :] points_, real[:, :] values_, real[:, :] xi_, - real[:, :] estimate, real[:, :] cho_cov, + const real[:, :] points_, const real[:, :] values_, const real[:, :] xi_, + real[:, :] estimate, const real[:, :] cho_cov, int n, int m, int d, int p, ) noexcept nogil: cdef: diff --git a/scipy/stats/_stats_mstats_common.py b/scipy/stats/_stats_mstats_common.py index e4ffd9ca58b8..6900eba1fa61 100644 --- a/scipy/stats/_stats_mstats_common.py +++ b/scipy/stats/_stats_mstats_common.py @@ -3,15 +3,10 @@ from . import distributions from .._lib._bunch import _make_tuple_bunch from ._stats_pythran import siegelslopes as siegelslopes_pythran -from scipy.stats import _stats_py -__all__ = ['_find_repeats', 'linregress', 'theilslopes', 'siegelslopes'] +__all__ = ['_find_repeats', 'theilslopes', 'siegelslopes'] # This is not a namedtuple for backwards compatibility. See PR #12983 -LinregressResult = _make_tuple_bunch('LinregressResult', - ['slope', 'intercept', 'rvalue', - 'pvalue', 'stderr'], - extra_field_names=['intercept_stderr']) TheilslopesResult = _make_tuple_bunch('TheilslopesResult', ['slope', 'intercept', 'low_slope', 'high_slope']) @@ -19,194 +14,6 @@ ['slope', 'intercept']) -def linregress(x, y=None, alternative='two-sided'): - """ - Calculate a linear least-squares regression for two sets of measurements. - - Parameters - ---------- - x, y : array_like - Two sets of measurements. Both arrays should have the same length N. If - only `x` is given (and ``y=None``), then it must be a two-dimensional - array where one dimension has length 2. The two sets of measurements - are then found by splitting the array along the length-2 dimension. In - the case where ``y=None`` and `x` is a 2xN array, ``linregress(x)`` is - equivalent to ``linregress(x[0], x[1])``. - alternative : {'two-sided', 'less', 'greater'}, optional - Defines the alternative hypothesis. Default is 'two-sided'. - The following options are available: - - * 'two-sided': the slope of the regression line is nonzero - * 'less': the slope of the regression line is less than zero - * 'greater': the slope of the regression line is greater than zero - - .. versionadded:: 1.7.0 - - Returns - ------- - result : ``LinregressResult`` instance - The return value is an object with the following attributes: - - slope : float - Slope of the regression line. - intercept : float - Intercept of the regression line. - rvalue : float - The Pearson correlation coefficient. The square of ``rvalue`` - is equal to the coefficient of determination. - pvalue : float - The p-value for a hypothesis test whose null hypothesis is - that the slope is zero, using Wald Test with t-distribution of - the test statistic. See `alternative` above for alternative - hypotheses. - stderr : float - Standard error of the estimated slope (gradient), under the - assumption of residual normality. - intercept_stderr : float - Standard error of the estimated intercept, under the assumption - of residual normality. - - See Also - -------- - scipy.optimize.curve_fit : - Use non-linear least squares to fit a function to data. - scipy.optimize.leastsq : - Minimize the sum of squares of a set of equations. - - Notes - ----- - For compatibility with older versions of SciPy, the return value acts - like a ``namedtuple`` of length 5, with fields ``slope``, ``intercept``, - ``rvalue``, ``pvalue`` and ``stderr``, so one can continue to write:: - - slope, intercept, r, p, se = linregress(x, y) - - With that style, however, the standard error of the intercept is not - available. To have access to all the computed values, including the - standard error of the intercept, use the return value as an object - with attributes, e.g.:: - - result = linregress(x, y) - print(result.intercept, result.intercept_stderr) - - Examples - -------- - >>> import numpy as np - >>> import matplotlib.pyplot as plt - >>> from scipy import stats - >>> rng = np.random.default_rng() - - Generate some data: - - >>> x = rng.random(10) - >>> y = 1.6*x + rng.random(10) - - Perform the linear regression: - - >>> res = stats.linregress(x, y) - - Coefficient of determination (R-squared): - - >>> print(f"R-squared: {res.rvalue**2:.6f}") - R-squared: 0.717533 - - Plot the data along with the fitted line: - - >>> plt.plot(x, y, 'o', label='original data') - >>> plt.plot(x, res.intercept + res.slope*x, 'r', label='fitted line') - >>> plt.legend() - >>> plt.show() - - Calculate 95% confidence interval on slope and intercept: - - >>> # Two-sided inverse Students t-distribution - >>> # p - probability, df - degrees of freedom - >>> from scipy.stats import t - >>> tinv = lambda p, df: abs(t.ppf(p/2, df)) - - >>> ts = tinv(0.05, len(x)-2) - >>> print(f"slope (95%): {res.slope:.6f} +/- {ts*res.stderr:.6f}") - slope (95%): 1.453392 +/- 0.743465 - >>> print(f"intercept (95%): {res.intercept:.6f}" - ... f" +/- {ts*res.intercept_stderr:.6f}") - intercept (95%): 0.616950 +/- 0.544475 - - """ - TINY = 1.0e-20 - if y is None: # x is a (2, N) or (N, 2) shaped array_like - x = np.asarray(x) - if x.shape[0] == 2: - x, y = x - elif x.shape[1] == 2: - x, y = x.T - else: - raise ValueError("If only `x` is given as input, it has to " - "be of shape (2, N) or (N, 2); provided shape " - f"was {x.shape}.") - else: - x = np.asarray(x) - y = np.asarray(y) - - if x.size == 0 or y.size == 0: - raise ValueError("Inputs must not be empty.") - - if np.amax(x) == np.amin(x) and len(x) > 1: - raise ValueError("Cannot calculate a linear regression " - "if all x values are identical") - - n = len(x) - xmean = np.mean(x, None) - ymean = np.mean(y, None) - - # Average sums of square differences from the mean - # ssxm = mean( (x-mean(x))^2 ) - # ssxym = mean( (x-mean(x)) * (y-mean(y)) ) - ssxm, ssxym, _, ssym = np.cov(x, y, bias=1).flat - - # R-value - # r = ssxym / sqrt( ssxm * ssym ) - if ssxm == 0.0 or ssym == 0.0: - # If the denominator was going to be 0 - r = 0.0 - else: - r = ssxym / np.sqrt(ssxm * ssym) - # Test for numerical error propagation (make sure -1 < r < 1) - if r > 1.0: - r = 1.0 - elif r < -1.0: - r = -1.0 - - slope = ssxym / ssxm - intercept = ymean - slope*xmean - if n == 2: - # handle case when only two points are passed in - if y[0] == y[1]: - prob = 1.0 - else: - prob = 0.0 - slope_stderr = 0.0 - intercept_stderr = 0.0 - else: - df = n - 2 # Number of degrees of freedom - # n-2 degrees of freedom because 2 has been used up - # to estimate the mean and standard deviation - t = r * np.sqrt(df / ((1.0 - r + TINY)*(1.0 + r + TINY))) - prob = _stats_py._get_pvalue(t, distributions.t(df), alternative) - - slope_stderr = np.sqrt((1 - r**2) * ssym / ssxm / df) - - # Also calculate the standard error of the intercept - # The following relationship is used: - # ssxm = mean( (x-mean(x))^2 ) - # = ssx - sx*sx - # = mean( x^2 ) - mean(x)^2 - intercept_stderr = slope_stderr * np.sqrt(ssxm + xmean**2) - - return LinregressResult(slope=slope, intercept=intercept, rvalue=r, - pvalue=prob, stderr=slope_stderr, - intercept_stderr=intercept_stderr) - - def theilslopes(y, x=None, alpha=0.95, method='separate'): r""" Computes the Theil-Sen estimator for a set of points (x, y). diff --git a/scipy/stats/_stats_py.py b/scipy/stats/_stats_py.py index 34b31330d9c8..d0fd9250afe7 100644 --- a/scipy/stats/_stats_py.py +++ b/scipy/stats/_stats_py.py @@ -48,8 +48,8 @@ from scipy import linalg # noqa: F401 from . import distributions from . import _mstats_basic as mstats_basic -from ._stats_mstats_common import (_find_repeats, linregress, theilslopes, - siegelslopes) + +from ._stats_mstats_common import _find_repeats, theilslopes, siegelslopes from ._stats import _kendall_dis, _toint64, _weightedrankedtau from dataclasses import dataclass, field @@ -59,8 +59,8 @@ monte_carlo_test, permutation_test, bootstrap, _batch_generator) from ._axis_nan_policy import (_axis_nan_policy_factory, - _broadcast_concatenate, - _broadcast_shapes) + _broadcast_concatenate, _broadcast_shapes, + _broadcast_array_shapes_remove_axis, SmallSampleWarning) from ._binomtest import _binary_search_for_binom_tst as _binary_search from scipy._lib._bunch import _make_tuple_bunch from scipy import stats @@ -443,7 +443,7 @@ def _mode_result(mode, count): # but `count` should not be NaN; it should be zero. i = np.isnan(count) if i.shape == (): - count = count.dtype(0) if i else count + count = np.asarray(0, dtype=count.dtype)[()] if i else count else: count[i] = 0 return ModeResult(mode, count) @@ -1459,7 +1459,7 @@ def skewtest(a, axis=0, nan_policy='propagate', alternative='two-sided'): Parameters ---------- a : array - The data to be tested. + The data to be tested. Must contain at least eight observations. axis : int or None, optional Axis along which statistics are calculated. Default is 0. If None, compute over the whole array `a`. @@ -1611,12 +1611,13 @@ def skewtest(a, axis=0, nan_policy='propagate', alternative='two-sided'): xp = array_namespace(a) a, axis = _chk_asarray(a, axis, xp=xp) - b2 = skew(a, axis) + b2 = skew(a, axis, _no_deco=True) n = a.shape[axis] if n < 8: message = ("`skewtest` requires at least 8 observations; " - f"{n} observations were given.") + f"only {n=} observations were given.") raise ValueError(message) + y = b2 * math.sqrt(((n + 1) * (n + 3)) / (6.0 * (n - 2))) beta2 = (3.0 * (n**2 + 27*n - 70) * (n+1) * (n+3) / ((n-2.0) * (n+5) * (n+7) * (n+9))) @@ -1647,7 +1648,7 @@ def kurtosistest(a, axis=0, nan_policy='propagate', alternative='two-sided'): Parameters ---------- a : array - Array of the sample data. + Array of the sample data. Must contain at least five observations. axis : int or None, optional Axis along which to compute test. Default is 0. If None, compute over the whole array `a`. @@ -1814,7 +1815,7 @@ def kurtosistest(a, axis=0, nan_policy='propagate', alternative='two-sided'): message = ("`kurtosistest` p-value may be inaccurate with fewer than 20 " f"observations; only {n=} observations were given.") warnings.warn(message, stacklevel=2) - b2 = kurtosis(a, axis, fisher=False) + b2 = kurtosis(a, axis, fisher=False, _no_deco=True) E = 3.0*(n-1) / (n+1) varb2 = 24.0*n*(n-2)*(n-3) / ((n+1)*(n+1.)*(n+3)*(n+5)) # [1]_ Eq. 1 @@ -1858,7 +1859,8 @@ def normaltest(a, axis=0, nan_policy='propagate'): Parameters ---------- a : array_like - The array containing the sample to be tested. + The array containing the sample to be tested. Must contain + at least eight observations. axis : int or None, optional Axis along which to compute test. Default is 0. If None, compute over the whole array `a`. @@ -1997,8 +1999,8 @@ def normaltest(a, axis=0, nan_policy='propagate'): """ xp = array_namespace(a) - s, _ = skewtest(a, axis) - k, _ = kurtosistest(a, axis) + s, _ = skewtest(a, axis, _no_deco=True) + k, _ = kurtosistest(a, axis, _no_deco=True) statistic = s*s + k*k chi2 = _SimpleChi2(xp.asarray(2.)) @@ -2807,7 +2809,7 @@ def sem(a, axis=0, ddof=1, nan_policy='propagate'): ---------- a : array_like An array containing the values for which the standard error is - returned. + returned. Must contain at least two observations. axis : int or None, optional Axis along which to operate. Default is 0. If None, compute over the whole array `a`. @@ -3963,26 +3965,27 @@ def _first(arr, axis): def _f_oneway_is_too_small(samples, kwargs={}, axis=-1): + message = f"At least two samples are required; got {len(samples)}." + if len(samples) < 2: + raise TypeError(message) + # Check this after forming alldata, so shape errors are detected # and reported before checking for 0 length inputs. if any(sample.shape[axis] == 0 for sample in samples): - msg = 'at least one input has length 0' - warnings.warn(stats.DegenerateDataWarning(msg), stacklevel=2) return True # Must have at least one group with length greater than 1. if all(sample.shape[axis] == 1 for sample in samples): msg = ('all input arrays have length 1. f_oneway requires that at ' 'least one input has length greater than 1.') - warnings.warn(stats.DegenerateDataWarning(msg), stacklevel=2) + warnings.warn(SmallSampleWarning(msg), stacklevel=2) return True return False @_axis_nan_policy_factory( - F_onewayResult, n_samples=None, too_small=_f_oneway_is_too_small -) + F_onewayResult, n_samples=None, too_small=_f_oneway_is_too_small) def f_oneway(*samples, axis=0): """Perform one-way ANOVA. @@ -4010,12 +4013,12 @@ def f_oneway(*samples, axis=0): Warns ----- `~scipy.stats.ConstantInputWarning` - Raised if all values within each of the input arrays are identical. + Emitted if all values within each of the input arrays are identical. In this case the F statistic is either infinite or isn't defined, so ``np.inf`` or ``np.nan`` is returned. - `~scipy.stats.DegenerateDataWarning` - Raised if the length of any input array is 0, or if all the input + RuntimeWarning + Emitted if the length of any input array is 0, or if all the input arrays have length 1. ``np.nan`` is returned for the F statistic and the p-value in these cases. @@ -4228,7 +4231,7 @@ def alexandergovern(*samples, nan_policy='propagate'): ---------- sample1, sample2, ... : array_like The sample measurements for each group. There must be at least - two samples. + two samples, and each sample must contain at least two observations. nan_policy : {'propagate', 'raise', 'omit'}, optional Defines how to handle when input contains nan. The following options are available (default is 'propagate'): @@ -4916,11 +4919,9 @@ def statistic(x, y, axis): # As explained in the docstring, the distribution of `r` under the null # hypothesis is the beta distribution on (-1, 1) with a = b = n/2 - 1. - # This needs to be done with NumPy arrays given the existing infrastructure. - ab = n/2 - 1 - dist = stats.beta(ab, ab, loc=-1, scale=2) - pvalue = _get_pvalue(np.asarray(r), dist, alternative, xp=np) - pvalue = xp.asarray(pvalue, dtype=dtype) + ab = xp.asarray(n/2 - 1) + dist = _SimpleBeta(ab, ab, loc=-1, scale=2) + pvalue = _get_pvalue(r, dist, alternative, xp=xp) r = r[()] if r.ndim == 0 else r pvalue = pvalue[()] if pvalue.ndim == 0 else pvalue @@ -5539,7 +5540,8 @@ def spearmanr(a, b=None, axis=0, nan_policy='propagate', # errors before taking the square root t = rs * np.sqrt((dof/((rs+1.0)*(1.0-rs))).clip(0)) - prob = _get_pvalue(t, distributions.t(dof), alternative, xp=np) + dist = _SimpleStudentT(dof) + prob = _get_pvalue(t, dist, alternative, xp=np) # For backwards compatibility, return scalars when comparing 2 columns if rs.shape == (2, 2): @@ -6276,8 +6278,7 @@ def unpack_TtestResult(res): result_to_tuple=unpack_TtestResult, n_outputs=6) # nan_policy handled by `_axis_nan_policy`, but needs to be left # in signature to preserve use as a positional argument -def ttest_1samp(a, popmean, axis=0, nan_policy='propagate', - alternative="two-sided"): +def ttest_1samp(a, popmean, axis=0, nan_policy="propagate", alternative="two-sided"): """Calculate the T-test for the mean of ONE group of scores. This is a test for the null hypothesis that the expected value @@ -6437,6 +6438,13 @@ def ttest_1samp(a, popmean, axis=0, nan_policy='propagate', n = a.shape[axis] df = n - 1 + if n == 0: + # This is really only needed for *testing* _axis_nan_policy decorator + # It won't happen when the decorator is used. + NaN = _get_nan(a) + return TtestResult(NaN, NaN, df=NaN, alternative=NaN, + standard_error=NaN, estimate=NaN) + mean = xp.mean(a, axis=axis) try: popmean = xp.asarray(popmean) @@ -6450,12 +6458,9 @@ def ttest_1samp(a, popmean, axis=0, nan_policy='propagate', with np.errstate(divide='ignore', invalid='ignore'): t = xp.divide(d, denom) t = t[()] if t.ndim == 0 else t - # This will only work for CPU backends for now. That's OK. In time, - # `from_dlpack` will enable the transfer from other devices, and - # `_get_pvalue` will even be reworked to support the native backend. - t_np = np.asarray(t) - prob = _get_pvalue(t_np, distributions.t(df), alternative, xp=np) - prob = xp.asarray(prob, dtype=t.dtype) + + dist = _SimpleStudentT(xp.asarray(df, dtype=t.dtype)) + prob = _get_pvalue(t, dist, alternative, xp=xp) prob = prob[()] if prob.ndim == 0 else prob # when nan_policy='omit', `df` can be different for different axis-slices @@ -6465,7 +6470,7 @@ def ttest_1samp(a, popmean, axis=0, nan_policy='propagate', alternative_num = {"less": -1, "two-sided": 0, "greater": 1}[alternative] return TtestResult(t, prob, df=df, alternative=alternative_num, standard_error=denom, estimate=mean, - statistic_np=t_np, xp=xp) + statistic_np=xp.asarray(t), xp=xp) def _t_confidence_interval(df, t, confidence_level, alternative, dtype=None, xp=None): @@ -6500,17 +6505,26 @@ def _t_confidence_interval(df, t, confidence_level, alternative, dtype=None, xp= high = high[()] if high.ndim == 0 else high return low, high -def _ttest_ind_from_stats(mean1, mean2, denom, df, alternative): + +def _ttest_ind_from_stats(mean1, mean2, denom, df, alternative, xp=None): + xp = array_namespace(mean1, mean2, denom) if xp is None else xp d = mean1 - mean2 with np.errstate(divide='ignore', invalid='ignore'): - t = np.divide(d, denom)[()] - prob = _get_pvalue(t, distributions.t(df), alternative, xp=np) + t = xp.divide(d, denom) + + t_np = np.asarray(t) + df_np = np.asarray(df) + prob = _get_pvalue(t_np, distributions.t(df_np), alternative, xp=np) + prob = xp.asarray(prob, dtype=t.dtype) - return (t, prob) + t = t[()] if t.ndim == 0 else t + prob = prob[()] if prob.ndim == 0 else prob + return t, prob -def _unequal_var_ttest_denom(v1, n1, v2, n2): +def _unequal_var_ttest_denom(v1, n1, v2, n2, xp=None): + xp = array_namespace(v1, v2) if xp is None else xp vn1 = v1 / n1 vn2 = v2 / n2 with np.errstate(divide='ignore', invalid='ignore'): @@ -6518,23 +6532,26 @@ def _unequal_var_ttest_denom(v1, n1, v2, n2): # If df is undefined, variances are zero (assumes n1 > 0 & n2 > 0). # Hence it doesn't matter what df is as long as it's not NaN. - df = np.where(np.isnan(df), 1, df) - denom = np.sqrt(vn1 + vn2) + df = xp.where(xp.isnan(df), xp.asarray(1.), df) + denom = xp.sqrt(vn1 + vn2) return df, denom -def _equal_var_ttest_denom(v1, n1, v2, n2): +def _equal_var_ttest_denom(v1, n1, v2, n2, xp=None): + xp = array_namespace(v1, v2) if xp is None else xp + # If there is a single observation in one sample, this formula for pooled # variance breaks down because the variance of that sample is undefined. # The pooled variance is still defined, though, because the (n-1) in the # numerator should cancel with the (n-1) in the denominator, leaving only # the sum of squared differences from the mean: zero. - v1 = np.where(n1 == 1, 0, v1)[()] - v2 = np.where(n2 == 1, 0, v2)[()] + zero = xp.asarray(0.) + v1 = xp.where(xp.asarray(n1 == 1), zero, v1) + v2 = xp.where(xp.asarray(n2 == 1), zero, v2) df = n1 + n2 - 2.0 svar = ((n1 - 1) * v1 + (n2 - 1) * v2) / df - denom = np.sqrt(svar * (1.0 / n1 + 1.0 / n2)) + denom = xp.sqrt(svar * (1.0 / n1 + 1.0 / n2)) return df, denom @@ -6636,7 +6653,9 @@ def ttest_ind_from_stats(mean1, std1, nobs1, mean2, std2, nobs2, >>> b = np.array([2, 4, 6, 9, 11, 13, 14, 15, 18, 19, 21]) >>> from scipy.stats import ttest_ind >>> ttest_ind(a, b) - Ttest_indResult(statistic=0.905135809331027, pvalue=0.3751996797581486) + TtestResult(statistic=0.905135809331027, + pvalue=0.3751996797581486, + df=22.0) Suppose we instead have binary data and would like to apply a t-test to compare the proportion of 1s in two independent groups:: @@ -6660,18 +6679,22 @@ def ttest_ind_from_stats(mean1, std1, nobs1, mean2, std2, nobs2, >>> group1 = np.array([1]*30 + [0]*(150-30)) >>> group2 = np.array([1]*45 + [0]*(200-45)) >>> ttest_ind(group1, group2) - Ttest_indResult(statistic=-0.5627179589855622, pvalue=0.573989277115258) + TtestResult(statistic=-0.5627179589855622, + pvalue=0.573989277115258, + df=348.0) """ - mean1 = np.asarray(mean1) - std1 = np.asarray(std1) - mean2 = np.asarray(mean2) - std2 = np.asarray(std2) + xp = array_namespace(mean1, std1, mean2, std2) + + mean1 = xp.asarray(mean1) + std1 = xp.asarray(std1) + mean2 = xp.asarray(mean2) + std2 = xp.asarray(std2) + if equal_var: - df, denom = _equal_var_ttest_denom(std1**2, nobs1, std2**2, nobs2) + df, denom = _equal_var_ttest_denom(std1**2, nobs1, std2**2, nobs2, xp=xp) else: - df, denom = _unequal_var_ttest_denom(std1**2, nobs1, - std2**2, nobs2) + df, denom = _unequal_var_ttest_denom(std1**2, nobs1, std2**2, nobs2, xp=xp) res = _ttest_ind_from_stats(mean1, mean2, denom, df, alternative) return Ttest_indResult(*res) @@ -6875,34 +6898,50 @@ def ttest_ind(a, b, axis=0, equal_var=True, nan_policy='propagate', >>> rvs1 = stats.norm.rvs(loc=5, scale=10, size=500, random_state=rng) >>> rvs2 = stats.norm.rvs(loc=5, scale=10, size=500, random_state=rng) >>> stats.ttest_ind(rvs1, rvs2) - Ttest_indResult(statistic=-0.4390847099199348, pvalue=0.6606952038870015) + TtestResult(statistic=-0.4390847099199348, + pvalue=0.6606952038870015, + df=998.0) >>> stats.ttest_ind(rvs1, rvs2, equal_var=False) - Ttest_indResult(statistic=-0.4390847099199348, pvalue=0.6606952553131064) + TtestResult(statistic=-0.4390847099199348, + pvalue=0.6606952553131064, + df=997.4602304121448) `ttest_ind` underestimates p for unequal variances: >>> rvs3 = stats.norm.rvs(loc=5, scale=20, size=500, random_state=rng) >>> stats.ttest_ind(rvs1, rvs3) - Ttest_indResult(statistic=-1.6370984482905417, pvalue=0.1019251574705033) + TtestResult(statistic=-1.6370984482905417, + pvalue=0.1019251574705033, + df=998.0) >>> stats.ttest_ind(rvs1, rvs3, equal_var=False) - Ttest_indResult(statistic=-1.637098448290542, pvalue=0.10202110497954867) + TtestResult(statistic=-1.637098448290542, + pvalue=0.10202110497954867, + df=765.1098655246868) When ``n1 != n2``, the equal variance t-statistic is no longer equal to the unequal variance t-statistic: >>> rvs4 = stats.norm.rvs(loc=5, scale=20, size=100, random_state=rng) >>> stats.ttest_ind(rvs1, rvs4) - Ttest_indResult(statistic=-1.9481646859513422, pvalue=0.05186270935842703) + TtestResult(statistic=-1.9481646859513422, + pvalue=0.05186270935842703, + df=598.0) >>> stats.ttest_ind(rvs1, rvs4, equal_var=False) - Ttest_indResult(statistic=-1.3146566100751664, pvalue=0.1913495266513811) + TtestResult(statistic=-1.3146566100751664, + pvalue=0.1913495266513811, + df=110.41349083985212) T-test with different means, variance, and n: >>> rvs5 = stats.norm.rvs(loc=8, scale=20, size=100, random_state=rng) >>> stats.ttest_ind(rvs1, rvs5) - Ttest_indResult(statistic=-2.8415950600298774, pvalue=0.0046418707568707885) + TtestResult(statistic=-2.8415950600298774, + pvalue=0.0046418707568707885, + df=598.0) >>> stats.ttest_ind(rvs1, rvs5, equal_var=False) - Ttest_indResult(statistic=-1.8686598649188084, pvalue=0.06434714193919686) + TtestResult(statistic=-1.8686598649188084, + pvalue=0.06434714193919686, + df=109.32167496550137) When performing a permutation test, more permutations typically yields more accurate results. Use a ``np.random.Generator`` to ensure @@ -6910,7 +6949,9 @@ def ttest_ind(a, b, axis=0, equal_var=True, nan_policy='propagate', >>> stats.ttest_ind(rvs1, rvs5, permutations=10000, ... random_state=rng) - Ttest_indResult(statistic=-2.8415950600298774, pvalue=0.0052994700529947) + TtestResult(statistic=-2.8415950600298774, + pvalue=0.0052994700529947, + df=nan) Take these two samples, one of which has an extreme tail. @@ -6923,26 +6964,39 @@ def ttest_ind(a, b, axis=0, equal_var=True, nan_policy='propagate', have no effect on sample `b` because ``np.floor(trim*len(b))`` is 0. >>> stats.ttest_ind(a, b, trim=.2) - Ttest_indResult(statistic=3.4463884028073513, - pvalue=0.01369338726499547) + TtestResult(statistic=3.4463884028073513, + pvalue=0.01369338726499547, + df=6.0) """ + xp = array_namespace(a, b) + + default_float = xp.asarray(1.).dtype + if xp.isdtype(a.dtype, 'integral'): + a = xp.astype(a, default_float) + if xp.isdtype(b.dtype, 'integral'): + b = xp.astype(b, default_float) + if not (0 <= trim < .5): raise ValueError("Trimming percentage should be 0 <= `trim` < .5.") - NaN = _get_nan(a, b) - - if a.size == 0 or b.size == 0: - # _axis_nan_policy decorator ensures this only happens with 1d input + result_shape = _broadcast_array_shapes_remove_axis((a, b), axis=axis) + NaN = xp.full(result_shape, _get_nan(a, b, xp=xp)) + NaN = NaN[()] if NaN.ndim == 0 else NaN + if xp_size(a) == 0 or xp_size(b) == 0: return TtestResult(NaN, NaN, df=NaN, alternative=NaN, standard_error=NaN, estimate=NaN) + alternative_nums = {"less": -1, "two-sided": 0, "greater": 1} + + # This probably should be deprecated and replaced with a `method` argument if permutations is not None and permutations != 0: + message = "Use of `permutations` is compatible only with NumPy arrays." + if not is_numpy(xp): + raise NotImplementedError(message) + + message = "Use of `permutations` is incompatible with with use of `trim`." if trim != 0: - raise ValueError("Permutations are currently not supported " - "with trimming.") - if permutations < 0 or (np.isfinite(permutations) and - int(permutations) != permutations): - raise ValueError("Permutations must be a non-negative integer.") + raise NotImplementedError(message) t, prob = _permutation_ttest(a, b, permutations=permutations, axis=axis, equal_var=equal_var, @@ -6951,37 +7005,40 @@ def ttest_ind(a, b, axis=0, equal_var=True, nan_policy='propagate', alternative=alternative) df, denom, estimate = NaN, NaN, NaN + # _axis_nan_policy decorator doesn't play well with strings + return TtestResult(t, prob, df=df, alternative=alternative_nums[alternative], + standard_error=denom, estimate=estimate) + + n1 = xp.asarray(a.shape[axis], dtype=a.dtype) + n2 = xp.asarray(b.shape[axis], dtype=b.dtype) + + if trim == 0: + with np.errstate(divide='ignore', invalid='ignore'): + v1 = _var(a, axis, ddof=1, xp=xp) + v2 = _var(b, axis, ddof=1, xp=xp) + + m1 = xp.mean(a, axis=axis) + m2 = xp.mean(b, axis=axis) else: - n1 = a.shape[axis] - n2 = b.shape[axis] - - if trim == 0: - if equal_var: - old_errstate = np.geterr() - np.seterr(divide='ignore', invalid='ignore') - v1 = _var(a, axis, ddof=1) - v2 = _var(b, axis, ddof=1) - if equal_var: - np.seterr(**old_errstate) - m1 = np.mean(a, axis) - m2 = np.mean(b, axis) - else: - v1, m1, n1 = _ttest_trim_var_mean_len(a, trim, axis) - v2, m2, n2 = _ttest_trim_var_mean_len(b, trim, axis) + message = "Use of `trim` is compatible only with NumPy arrays." + if not is_numpy(xp): + raise NotImplementedError(message) - if equal_var: - df, denom = _equal_var_ttest_denom(v1, n1, v2, n2) - else: - df, denom = _unequal_var_ttest_denom(v1, n1, v2, n2) - t, prob = _ttest_ind_from_stats(m1, m2, denom, df, alternative) + v1, m1, n1 = _ttest_trim_var_mean_len(a, trim, axis) + v2, m2, n2 = _ttest_trim_var_mean_len(b, trim, axis) + + if equal_var: + df, denom = _equal_var_ttest_denom(v1, n1, v2, n2, xp=xp) + else: + df, denom = _unequal_var_ttest_denom(v1, n1, v2, n2, xp=xp) + t, prob = _ttest_ind_from_stats(m1, m2, denom, df, alternative) - # when nan_policy='omit', `df` can be different for different axis-slices - df = np.broadcast_to(df, t.shape)[()] - estimate = m1-m2 + # when nan_policy='omit', `df` can be different for different axis-slices + df = xp.broadcast_to(df, t.shape) + df = df[()] if df.ndim ==0 else df + estimate = m1 - m2 - # _axis_nan_policy decorator doesn't play well with strings - alternative_num = {"less": -1, "two-sided": 0, "greater": 1}[alternative] - return TtestResult(t, prob, df=df, alternative=alternative_num, + return TtestResult(t, prob, df=df, alternative=alternative_nums[alternative], standard_error=denom, estimate=estimate) @@ -7092,9 +7149,9 @@ def _calc_t_stat(a, b, equal_var, axis=-1): var_b = _var(b, axis=axis, ddof=1) if not equal_var: - denom = _unequal_var_ttest_denom(var_a, na, var_b, nb)[1] + _, denom = _unequal_var_ttest_denom(var_a, na, var_b, nb) else: - denom = _equal_var_ttest_denom(var_a, na, var_b, nb)[1] + _, denom = _equal_var_ttest_denom(var_a, na, var_b, nb) return (avg_a-avg_b)/denom @@ -7140,6 +7197,10 @@ def _permutation_ttest(a, b, permutations, axis=0, equal_var=True, The p-value. """ + if permutations < 0 or (np.isfinite(permutations) and + int(permutations) != permutations): + raise ValueError("Permutations must be a non-negative integer.") + random_state = check_random_state(random_state) t_stat_observed = _calc_t_stat(a, b, equal_var, axis=axis) @@ -7284,38 +7345,8 @@ def ttest_rel(a, b, axis=0, nan_policy='propagate', alternative="two-sided"): TtestResult(statistic=-5.879467544540889, pvalue=7.540777129099917e-09, df=499) """ - a, b, axis = _chk2_asarray(a, b, axis) - - na = _get_len(a, axis, "first argument") - nb = _get_len(b, axis, "second argument") - if na != nb: - raise ValueError('unequal length arrays') - - if na == 0 or nb == 0: - # _axis_nan_policy decorator ensures this only happens with 1d input - NaN = _get_nan(a, b) - return TtestResult(NaN, NaN, df=NaN, alternative=NaN, - standard_error=NaN, estimate=NaN) - - n = a.shape[axis] - df = n - 1 - - d = (a - b).astype(np.float64) - v = _var(d, axis, ddof=1) - dm = np.mean(d, axis) - denom = np.sqrt(v / n) - - with np.errstate(divide='ignore', invalid='ignore'): - t = np.divide(dm, denom)[()] - prob = _get_pvalue(t, distributions.t(df), alternative, xp=np) - - # when nan_policy='omit', `df` can be different for different axis-slices - df = np.broadcast_to(df, t.shape)[()] - - # _axis_nan_policy decorator doesn't play well with strings - alternative_num = {"less": -1, "two-sided": 0, "greater": 1}[alternative] - return TtestResult(t, prob, df=df, alternative=alternative_num, - standard_error=denom, estimate=dm) + return ttest_1samp(a - b, popmean=0, axis=axis, alternative=alternative, + _no_deco=True) # Map from names to lambda_ values used in power_divergence(). @@ -7960,7 +7991,10 @@ def ks_1samp(x, cdf, args=(), alternative='two-sided', method='auto'): >>> rng = np.random.default_rng() >>> stats.ks_1samp(stats.uniform.rvs(size=100, random_state=rng), ... stats.norm.cdf) - KstestResult(statistic=0.5001899973268688, pvalue=1.1616392184763533e-23) + KstestResult(statistic=0.5001899973268688, + pvalue=1.1616392184763533e-23, + statistic_location=0.00047625268963724654, + statistic_sign=-1) Indeed, the p-value is lower than our threshold of 0.05, so we reject the null hypothesis in favor of the default "two-sided" alternative: the data @@ -7971,7 +8005,10 @@ def ks_1samp(x, cdf, args=(), alternative='two-sided', method='auto'): >>> x = stats.norm.rvs(size=100, random_state=rng) >>> stats.ks_1samp(x, stats.norm.cdf) - KstestResult(statistic=0.05345882212970396, pvalue=0.9227159037744717) + KstestResult(statistic=0.05345882212970396, + pvalue=0.9227159037744717, + statistic_location=-1.2451343873745018, + statistic_sign=1) As expected, the p-value of 0.92 is not below our threshold of 0.05, so we cannot reject the null hypothesis. @@ -7984,7 +8021,10 @@ def ks_1samp(x, cdf, args=(), alternative='two-sided', method='auto'): >>> x = stats.norm.rvs(size=100, loc=0.5, random_state=rng) >>> stats.ks_1samp(x, stats.norm.cdf, alternative='less') - KstestResult(statistic=0.17482387821055168, pvalue=0.001913921057766743) + KstestResult(statistic=0.17482387821055168, + pvalue=0.001913921057766743, + statistic_location=0.3713830565352756, + statistic_sign=-1) and indeed, with p-value smaller than our threshold, we reject the null hypothesis in favor of the alternative. @@ -8322,7 +8362,11 @@ def ks_2samp(data1, data2, alternative='two-sided', method='auto'): >>> sample1 = stats.uniform.rvs(size=100, random_state=rng) >>> sample2 = stats.norm.rvs(size=110, random_state=rng) >>> stats.ks_2samp(sample1, sample2) - KstestResult(statistic=0.5454545454545454, pvalue=7.37417839555191e-15) + KstestResult(statistic=0.5454545454545454, + pvalue=7.37417839555191e-15, + statistic_location=-0.014071496412861274, + statistic_sign=-1) + Indeed, the p-value is lower than our threshold of 0.05, so we reject the null hypothesis in favor of the default "two-sided" alternative: the data @@ -8334,7 +8378,10 @@ def ks_2samp(data1, data2, alternative='two-sided', method='auto'): >>> sample1 = stats.norm.rvs(size=105, random_state=rng) >>> sample2 = stats.norm.rvs(size=95, random_state=rng) >>> stats.ks_2samp(sample1, sample2) - KstestResult(statistic=0.10927318295739348, pvalue=0.5438289009927495) + KstestResult(statistic=0.10927318295739348, + pvalue=0.5438289009927495, + statistic_location=-0.1670157701848795, + statistic_sign=-1) As expected, the p-value of 0.54 is not below our threshold of 0.05, so we cannot reject the null hypothesis. @@ -8347,7 +8394,10 @@ def ks_2samp(data1, data2, alternative='two-sided', method='auto'): >>> sample1 = stats.norm.rvs(size=105, loc=0.5, random_state=rng) >>> stats.ks_2samp(sample1, sample2, alternative='less') - KstestResult(statistic=0.4055137844611529, pvalue=3.5474563068855554e-08) + KstestResult(statistic=0.4055137844611529, + pvalue=3.5474563068855554e-08, + statistic_location=-0.13249370614972575, + statistic_sign=-1) and indeed, with p-value smaller than our threshold, we reject the null hypothesis in favor of the alternative. @@ -8594,7 +8644,10 @@ def kstest(rvs, cdf, args=(), N=20, alternative='two-sided', method='auto'): >>> rng = np.random.default_rng() >>> stats.kstest(stats.uniform.rvs(size=100, random_state=rng), ... stats.norm.cdf) - KstestResult(statistic=0.5001899973268688, pvalue=1.1616392184763533e-23) + KstestResult(statistic=0.5001899973268688, + pvalue=1.1616392184763533e-23, + statistic_location=0.00047625268963724654, + statistic_sign=-1) Indeed, the p-value is lower than our threshold of 0.05, so we reject the null hypothesis in favor of the default "two-sided" alternative: the data @@ -8605,7 +8658,11 @@ def kstest(rvs, cdf, args=(), N=20, alternative='two-sided', method='auto'): >>> x = stats.norm.rvs(size=100, random_state=rng) >>> stats.kstest(x, stats.norm.cdf) - KstestResult(statistic=0.05345882212970396, pvalue=0.9227159037744717) + KstestResult(statistic=0.05345882212970396, + pvalue=0.9227159037744717, + statistic_location=-1.2451343873745018, + statistic_sign=1) + As expected, the p-value of 0.92 is not below our threshold of 0.05, so we cannot reject the null hypothesis. @@ -8618,7 +8675,10 @@ def kstest(rvs, cdf, args=(), N=20, alternative='two-sided', method='auto'): >>> x = stats.norm.rvs(size=100, loc=0.5, random_state=rng) >>> stats.kstest(x, stats.norm.cdf, alternative='less') - KstestResult(statistic=0.17482387821055168, pvalue=0.001913921057766743) + KstestResult(statistic=0.17482387821055168, + pvalue=0.001913921057766743, + statistic_location=0.3713830565352756, + statistic_sign=-1) and indeed, with p-value smaller than our threshold, we reject the null hypothesis in favor of the alternative. @@ -8627,7 +8687,10 @@ def kstest(rvs, cdf, args=(), N=20, alternative='two-sided', method='auto'): distribution as the second argument. >>> stats.kstest(x, "norm", alternative='less') - KstestResult(statistic=0.17482387821055168, pvalue=0.001913921057766743) + KstestResult(statistic=0.17482387821055168, + pvalue=0.001913921057766743, + statistic_location=0.3713830565352756, + statistic_sign=-1) The examples above have all been one-sample tests identical to those performed by `ks_1samp`. Note that `kstest` can also perform two-sample @@ -8638,7 +8701,10 @@ def kstest(rvs, cdf, args=(), N=20, alternative='two-sided', method='auto'): >>> sample1 = stats.laplace.rvs(size=105, random_state=rng) >>> sample2 = stats.laplace.rvs(size=95, random_state=rng) >>> stats.kstest(sample1, sample2) - KstestResult(statistic=0.11779448621553884, pvalue=0.4494256912629795) + KstestResult(statistic=0.11779448621553884, + pvalue=0.4494256912629795, + statistic_location=0.6138814275424155, + statistic_sign=1) As expected, the p-value of 0.45 is not below our threshold of 0.05, so we cannot reject the null hypothesis. @@ -8863,33 +8929,8 @@ def kruskal(*samples, nan_policy='propagate'): if num_groups < 2: raise ValueError("Need at least two groups in stats.kruskal()") - for sample in samples: - if sample.size == 0: - NaN = _get_nan(*samples) - return KruskalResult(NaN, NaN) - elif sample.ndim != 1: - raise ValueError("Samples must be one-dimensional.") - n = np.asarray(list(map(len, samples))) - if nan_policy not in ('propagate', 'raise', 'omit'): - raise ValueError("nan_policy must be 'propagate', 'raise' or 'omit'") - - contains_nan = False - for sample in samples: - cn = _contains_nan(sample, nan_policy) - if cn[0]: - contains_nan = True - break - - if contains_nan and nan_policy == 'omit': - for sample in samples: - sample = ma.masked_invalid(sample) - return mstats_basic.kruskal(*samples) - - if contains_nan and nan_policy == 'propagate': - return KruskalResult(np.nan, np.nan) - alldata = np.concatenate(samples) ranked = rankdata(alldata) ties = tiecorrect(ranked) @@ -9094,12 +9135,9 @@ def brunnermunzel(x, y, alternative="two-sided", distribution="t", 0.0057862086661515377 """ - nx = len(x) ny = len(y) - if nx == 0 or ny == 0: - NaN = _get_nan(x, y) - return BrunnerMunzelResult(NaN, NaN) + rankc = rankdata(np.concatenate((x, y))) rankcx = rankc[0:nx] rankcy = rankc[nx:nx+ny] @@ -9130,7 +9168,7 @@ def brunnermunzel(x, y, alternative="two-sided", distribution="t", "(0/0). Try using `distribution='normal'") warnings.warn(message, RuntimeWarning, stacklevel=2) - distribution = distributions.t(df) + distribution = _SimpleStudentT(df) elif distribution == "normal": distribution = _SimpleNormal() else: @@ -9264,39 +9302,47 @@ def combine_pvalues(pvalues, method='fisher', weights=None): .. [8] https://en.wikipedia.org/wiki/Extensions_of_Fisher%27s_method """ - if pvalues.size == 0: + xp = array_namespace(pvalues) + pvalues = xp.asarray(pvalues) + if xp_size(pvalues) == 0: NaN = _get_nan(pvalues) return SignificanceResult(NaN, NaN) + + n = pvalues.shape[0] + # used to convert Python scalar to the right dtype + one = xp.asarray(1, dtype=pvalues.dtype) if method == 'fisher': - statistic = -2 * np.sum(np.log(pvalues)) - chi2 = _SimpleChi2(2 * len(pvalues)) + statistic = -2 * xp.sum(xp.log(pvalues)) + chi2 = _SimpleChi2(2*n*one) pval = _get_pvalue(statistic, chi2, alternative='greater', - symmetric=False, xp=np) + symmetric=False, xp=xp) elif method == 'pearson': - statistic = 2 * np.sum(np.log1p(-pvalues)) - # _SimpleChi2 doesn't have `cdf` yet; - # add it when `combine_pvalues` is converted to array API - pval = distributions.chi2.cdf(-statistic, 2 * len(pvalues)) + statistic = 2 * xp.sum(xp.log1p(-pvalues)) + chi2 = _SimpleChi2(2*n*one) + pval = _get_pvalue(-statistic, chi2, alternative='less', symmetric=False, xp=xp) elif method == 'mudholkar_george': - normalizing_factor = np.sqrt(3/len(pvalues))/np.pi - statistic = -np.sum(np.log(pvalues)) + np.sum(np.log1p(-pvalues)) - nu = 5 * len(pvalues) + 4 - approx_factor = np.sqrt(nu / (nu - 2)) - pval = distributions.t.sf(statistic * normalizing_factor - * approx_factor, nu) + normalizing_factor = math.sqrt(3/n)/xp.pi + statistic = -xp.sum(xp.log(pvalues)) + xp.sum(xp.log1p(-pvalues)) + nu = 5*n + 4 + approx_factor = math.sqrt(nu / (nu - 2)) + t = _SimpleStudentT(nu*one) + pval = _get_pvalue(statistic * normalizing_factor * approx_factor, t, + alternative="greater", xp=xp) elif method == 'tippett': - statistic = np.min(pvalues) - pval = distributions.beta.cdf(statistic, 1, len(pvalues)) + statistic = xp.min(pvalues) + beta = _SimpleBeta(one, n*one) + pval = _get_pvalue(statistic, beta, alternative='less', symmetric=False, xp=xp) elif method == 'stouffer': if weights is None: - weights = np.ones_like(pvalues) - elif len(weights) != len(pvalues): + weights = xp.ones_like(pvalues, dtype=pvalues.dtype) + elif weights.shape[0] != n: raise ValueError("pvalues and weights must be of the same size.") - Zi = distributions.norm.isf(pvalues) - statistic = np.dot(weights, Zi) / np.linalg.norm(weights) - pval = distributions.norm.sf(statistic) + norm = _SimpleNormal() + Zi = norm.isf(pvalues) + statistic = weights @ Zi / xp.linalg.vector_norm(weights) + pval = _get_pvalue(statistic, norm, alternative="greater", xp=xp) else: raise ValueError( @@ -10691,6 +10737,213 @@ def first_order(t): return res.root +LinregressResult = _make_tuple_bunch('LinregressResult', + ['slope', 'intercept', 'rvalue', + 'pvalue', 'stderr'], + extra_field_names=['intercept_stderr']) + + +def linregress(x, y=None, alternative='two-sided'): + """ + Calculate a linear least-squares regression for two sets of measurements. + + Parameters + ---------- + x, y : array_like + Two sets of measurements. Both arrays should have the same length N. If + only `x` is given (and ``y=None``), then it must be a two-dimensional + array where one dimension has length 2. The two sets of measurements + are then found by splitting the array along the length-2 dimension. In + the case where ``y=None`` and `x` is a 2xN array, ``linregress(x)`` is + equivalent to ``linregress(x[0], x[1])``. + + .. deprecated:: 1.14.0 + Inference of the two sets of measurements from a single argument `x` + is deprecated will result in an error in SciPy 1.16.0; the sets + must be specified separately as `x` and `y`. + alternative : {'two-sided', 'less', 'greater'}, optional + Defines the alternative hypothesis. Default is 'two-sided'. + The following options are available: + + * 'two-sided': the slope of the regression line is nonzero + * 'less': the slope of the regression line is less than zero + * 'greater': the slope of the regression line is greater than zero + + .. versionadded:: 1.7.0 + + Returns + ------- + result : ``LinregressResult`` instance + The return value is an object with the following attributes: + + slope : float + Slope of the regression line. + intercept : float + Intercept of the regression line. + rvalue : float + The Pearson correlation coefficient. The square of ``rvalue`` + is equal to the coefficient of determination. + pvalue : float + The p-value for a hypothesis test whose null hypothesis is + that the slope is zero, using Wald Test with t-distribution of + the test statistic. See `alternative` above for alternative + hypotheses. + stderr : float + Standard error of the estimated slope (gradient), under the + assumption of residual normality. + intercept_stderr : float + Standard error of the estimated intercept, under the assumption + of residual normality. + + See Also + -------- + scipy.optimize.curve_fit : + Use non-linear least squares to fit a function to data. + scipy.optimize.leastsq : + Minimize the sum of squares of a set of equations. + + Notes + ----- + For compatibility with older versions of SciPy, the return value acts + like a ``namedtuple`` of length 5, with fields ``slope``, ``intercept``, + ``rvalue``, ``pvalue`` and ``stderr``, so one can continue to write:: + + slope, intercept, r, p, se = linregress(x, y) + + With that style, however, the standard error of the intercept is not + available. To have access to all the computed values, including the + standard error of the intercept, use the return value as an object + with attributes, e.g.:: + + result = linregress(x, y) + print(result.intercept, result.intercept_stderr) + + Examples + -------- + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from scipy import stats + >>> rng = np.random.default_rng() + + Generate some data: + + >>> x = rng.random(10) + >>> y = 1.6*x + rng.random(10) + + Perform the linear regression: + + >>> res = stats.linregress(x, y) + + Coefficient of determination (R-squared): + + >>> print(f"R-squared: {res.rvalue**2:.6f}") + R-squared: 0.717533 + + Plot the data along with the fitted line: + + >>> plt.plot(x, y, 'o', label='original data') + >>> plt.plot(x, res.intercept + res.slope*x, 'r', label='fitted line') + >>> plt.legend() + >>> plt.show() + + Calculate 95% confidence interval on slope and intercept: + + >>> # Two-sided inverse Students t-distribution + >>> # p - probability, df - degrees of freedom + >>> from scipy.stats import t + >>> tinv = lambda p, df: abs(t.ppf(p/2, df)) + + >>> ts = tinv(0.05, len(x)-2) + >>> print(f"slope (95%): {res.slope:.6f} +/- {ts*res.stderr:.6f}") + slope (95%): 1.453392 +/- 0.743465 + >>> print(f"intercept (95%): {res.intercept:.6f}" + ... f" +/- {ts*res.intercept_stderr:.6f}") + intercept (95%): 0.616950 +/- 0.544475 + + """ + TINY = 1.0e-20 + if y is None: # x is a (2, N) or (N, 2) shaped array_like + message = ('Inference of the two sets of measurements from a single "' + 'argument `x` is deprecated will result in an error in "' + 'SciPy 1.16.0; the sets must be specified separately as "' + '`x` and `y`.') + warnings.warn(message, DeprecationWarning, stacklevel=2) + x = np.asarray(x) + if x.shape[0] == 2: + x, y = x + elif x.shape[1] == 2: + x, y = x.T + else: + raise ValueError("If only `x` is given as input, it has to " + "be of shape (2, N) or (N, 2); provided shape " + f"was {x.shape}.") + else: + x = np.asarray(x) + y = np.asarray(y) + + if x.size == 0 or y.size == 0: + raise ValueError("Inputs must not be empty.") + + if np.amax(x) == np.amin(x) and len(x) > 1: + raise ValueError("Cannot calculate a linear regression " + "if all x values are identical") + + n = len(x) + xmean = np.mean(x, None) + ymean = np.mean(y, None) + + # Average sums of square differences from the mean + # ssxm = mean( (x-mean(x))^2 ) + # ssxym = mean( (x-mean(x)) * (y-mean(y)) ) + ssxm, ssxym, _, ssym = np.cov(x, y, bias=1).flat + + # R-value + # r = ssxym / sqrt( ssxm * ssym ) + if ssxm == 0.0 or ssym == 0.0: + # If the denominator was going to be 0 + r = 0.0 + else: + r = ssxym / np.sqrt(ssxm * ssym) + # Test for numerical error propagation (make sure -1 < r < 1) + if r > 1.0: + r = 1.0 + elif r < -1.0: + r = -1.0 + + slope = ssxym / ssxm + intercept = ymean - slope*xmean + if n == 2: + # handle case when only two points are passed in + if y[0] == y[1]: + prob = 1.0 + else: + prob = 0.0 + slope_stderr = 0.0 + intercept_stderr = 0.0 + else: + df = n - 2 # Number of degrees of freedom + # n-2 degrees of freedom because 2 has been used up + # to estimate the mean and standard deviation + t = r * np.sqrt(df / ((1.0 - r + TINY)*(1.0 + r + TINY))) + + dist = _SimpleStudentT(df) + prob = _get_pvalue(t, dist, alternative, xp=np) + prob = prob[()] if prob.ndim == 0 else prob + + slope_stderr = np.sqrt((1 - r**2) * ssym / ssxm / df) + + # Also calculate the standard error of the intercept + # The following relationship is used: + # ssxm = mean( (x-mean(x))^2 ) + # = ssx - sx*sx + # = mean( x^2 ) - mean(x)^2 + intercept_stderr = slope_stderr * np.sqrt(ssxm + xmean**2) + + return LinregressResult(slope=slope, intercept=intercept, rvalue=r, + pvalue=prob, stderr=slope_stderr, + intercept_stderr=intercept_stderr) + + class _SimpleNormal: # A very simple, array-API compatible normal distribution for use in # hypothesis tests. May be replaced by new infrastructure Normal @@ -10701,6 +10954,9 @@ def cdf(self, x): def sf(self, x): return special.ndtr(-x) + + def isf(self, x): + return -special.ndtri(x) class _SimpleChi2: @@ -10710,5 +10966,47 @@ class _SimpleChi2: def __init__(self, df): self.df = df + def cdf(self, x): + return special.chdtr(self.df, x) + def sf(self, x): return special.chdtrc(self.df, x) + + +class _SimpleBeta: + # A very simple, array-API compatible beta distribution for use in + # hypothesis tests. May be replaced by new infrastructure beta + # distribution in due time. + def __init__(self, a, b, *, loc=None, scale=None): + self.a = a + self.b = b + self.loc = loc + self.scale = scale + + def cdf(self, x): + if self.loc is not None or self.scale is not None: + loc = 0 if self.loc is None else self.loc + scale = 1 if self.scale is None else self.scale + return special.betainc(self.a, self.b, (x - loc)/scale) + return special.betainc(self.a, self.b, x) + + def sf(self, x): + if self.loc is not None or self.scale is not None: + loc = 0 if self.loc is None else self.loc + scale = 1 if self.scale is None else self.scale + return special.betaincc(self.a, self.b, (x - loc)/scale) + return special.betaincc(self.a, self.b, x) + + +class _SimpleStudentT: + # A very simple, array-API compatible t distribution for use in + # hypothesis tests. May be replaced by new infrastructure t + # distribution in due time. + def __init__(self, df): + self.df = df + + def cdf(self, t): + return special.stdtr(self.df, t) + + def sf(self, t): + return special.stdtr(self.df, -t) diff --git a/scipy/stats/meson.build b/scipy/stats/meson.build index bb43e3b2e98a..b6617a944848 100644 --- a/scipy/stats/meson.build +++ b/scipy/stats/meson.build @@ -117,6 +117,7 @@ py3.install_sources([ '_crosstab.py', '_discrete_distns.py', '_distn_infrastructure.py', + '_distribution_infrastructure.py', '_distr_params.py', '_entropy.py', '_fit.py', @@ -130,6 +131,8 @@ py3.install_sources([ '_mstats_extras.py', '_multicomp.py', '_multivariate.py', + '_new_distributions.py', + '_new_distribution_docs.json', '_odds_ratio.py', '_page_trend_test.py', '_qmc.py', @@ -137,7 +140,6 @@ py3.install_sources([ '_relative_risk.py', '_resampling.py', '_result_classes.py', - '_rvs_sampling.py', '_sampling.py', '_sensitivity_analysis.py', '_stats_mstats_common.py', diff --git a/scipy/stats/tests/distribution_infrastructure.ipynb b/scipy/stats/tests/distribution_infrastructure.ipynb new file mode 100644 index 000000000000..905b9744f951 --- /dev/null +++ b/scipy/stats/tests/distribution_infrastructure.ipynb @@ -0,0 +1,2265 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "## Random Variable Infrastructure\n", + "### Basics" + ], + "metadata": { + "collapsed": false + }, + "id": "6cd8eb4e9d719430" + }, + { + "cell_type": "markdown", + "source": [ + "Distributions (or, more accurately, distribution families) are classes named according to `CamelCase` conventions. They must be instantiated before use, with parameters passed as keyword-only arguments.\n", + "*Instances* of the distribution classes can be thought of as random variables, which are commonly denoted in mathematics using capital letters." + ], + "metadata": { + "collapsed": false + }, + "id": "87941d07dd092479" + }, + { + "cell_type": "code", + "execution_count": 1, + "outputs": [ + { + "data": { + "text/plain": "LogUniform(a=1.0, b=2.0)" + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from scipy import stats\n", + "X = stats.LogUniform(a=1, b=2)\n", + "X" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.822594500Z", + "start_time": "2024-04-30T15:27:14.523358500Z" + } + }, + "id": "68dce64c0d289032" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(1.0, 2.0)" + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.support()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.829162400Z", + "start_time": "2024-04-30T15:27:30.821527800Z" + } + }, + "id": "e37f7af9ccb122ad", + "execution_count": 2 + }, + { + "cell_type": "markdown", + "source": [ + "Distributions can support multiple parameterizations, resolving requests like [gh-4538](https://github.com/scipy/scipy/issues/4538). For instance, it is also natural to parameterize the log-uniform distribution using the logarithms of the support endpoints. (If a log-uniform random variable is supported on $[a, b]$, its logarithm follows a uniform distribution with support $[\\log(a), \\log(b)$].)" + ], + "metadata": { + "collapsed": false + }, + "id": "e4eff6e9cd1df13b" + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [ + { + "data": { + "text/plain": "LogUniform(log_a=0.0, log_b=0.6931471805599453)" + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "Y = stats.LogUniform(log_a=np.log(1), log_b=np.log(2))\n", + "Y" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.841926Z", + "start_time": "2024-04-30T15:27:30.825057500Z" + } + }, + "id": "627823ded06e451b" + }, + { + "cell_type": "markdown", + "source": [ + "After being defined, these two random variables are essentially equivalent. As a weak example:" + ], + "metadata": { + "collapsed": false + }, + "id": "77bdbf4334f7d678" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "True" + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.support() == Y.support()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.843920400Z", + "start_time": "2024-04-30T15:27:30.836073400Z" + } + }, + "id": "719ba8a891f3284e", + "execution_count": 4 + }, + { + "cell_type": "markdown", + "source": [ + "All parameters of the distribution underlying the random variable are available as attributes." + ], + "metadata": { + "collapsed": false + }, + "id": "8a96cc18cc79a47b" + }, + { + "cell_type": "code", + "execution_count": 5, + "outputs": [ + { + "data": { + "text/plain": "(1.0, 2.0, 0.0, 0.6931471805599453)" + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.a, X.b, X.log_a, X.log_b" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.956289Z", + "start_time": "2024-04-30T15:27:30.840805600Z" + } + }, + "id": "327a994cafcb5a10" + }, + { + "cell_type": "markdown", + "source": [ + "Currently, distribution parameters are not intended to be changed, since the additional overhead of instantiating a new random variable is small. Nonetheless, support for modification of parameters is one of many planned enhancements." + ], + "metadata": { + "collapsed": false + }, + "id": "e6966cf208cd0df8" + }, + { + "cell_type": "markdown", + "source": [ + "### Defining a distribution" + ], + "metadata": { + "collapsed": false + }, + "id": "619a3e13b6f8532b" + }, + { + "cell_type": "markdown", + "source": [ + "Minimal information is needed to fully define a distribution class. For example, a class representing a uniform distribution parameterized by the lower and upper ends of the support might look like this." + ], + "metadata": { + "collapsed": false + }, + "id": "aa6ab0e386c252ff" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from scipy.stats._distribution_infrastructure import (ContinuousDistribution, _RealDomain,\n", + " _RealParameter, _Parameterization, oo)\n", + "\n", + "class UniformDistribution(ContinuousDistribution):\n", + " _a_param = _RealParameter('a', domain=_RealDomain(endpoints=(-oo, oo)))\n", + " _b_param = _RealParameter('b', domain=_RealDomain(endpoints=('a', oo)))\n", + " _x_param = _RealParameter('x', domain=_RealDomain(endpoints=('a', 'b'), inclusive=(True, True)))\n", + "\n", + " _parameterizations = [_Parameterization(_a_param, _b_param)]\n", + " _variable = _x_param\n", + "\n", + " def _pdf_formula(self, x, *, a, b, **kwargs):\n", + " return np.ones_like(x)/(b-a)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.958363300Z", + "start_time": "2024-04-30T15:27:30.848309400Z" + } + }, + "id": "8190ae7352ec4585", + "execution_count": 6 + }, + { + "cell_type": "markdown", + "source": [ + "The infrastructure automatically validates numerical distribution parameters and method arguments based on their abstract definitions." + ], + "metadata": { + "collapsed": false + }, + "id": "8771cabee5cd40e9" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "a, b = 1, 3\n", + "X = UniformDistribution(a=a, b=b)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.978837100Z", + "start_time": "2024-04-30T15:27:30.852862900Z" + } + }, + "id": "47039d50589e64ae", + "execution_count": 7 + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(1.0, 3.0)" + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.support()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.982597900Z", + "start_time": "2024-04-30T15:27:30.858555200Z" + } + }, + "id": "971788621b427159", + "execution_count": 8 + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(array([0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5]),\n array([0. , 0.5, 0.5, 0.5, 0.5, 0.5, 0. ]))" + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = np.arange(a - 0.5, b + 0.51, 0.5)\n", + "x, X.pdf(x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.997584900Z", + "start_time": "2024-04-30T15:27:30.865581200Z" + } + }, + "id": "82385f400411b21c", + "execution_count": 9 + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "array([0. , 0. , 0.25, 0.5 , 0.75, 1. , 1. ])" + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.cdf(x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:30.999579400Z", + "start_time": "2024-04-30T15:27:30.874027300Z" + } + }, + "id": "a594ed5b72b4eb54", + "execution_count": 10 + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "array([nan, 2., nan])" + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.icdf([-0.5, 0.5, 1.5]) # there are no numbers for which the CDF is negative or greater than 1" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.109816500Z", + "start_time": "2024-04-30T15:27:30.877972300Z" + } + }, + "id": "6473e6c2a92ae04b", + "execution_count": 11 + }, + { + "cell_type": "markdown", + "source": [ + "Above, note that the domain of the argument `x` was set to be inclusive, so the PDF *at* the limits of the support was `0.5` rather than `0.0`. On the other hand, the domains of parameters `a` and `b` are exclusive (by default). Rather than raising errors, out-of-domain shapes and NaNs result in methods returning NaNs. This allows for valid calculations to proceed normally." + ], + "metadata": { + "collapsed": false + }, + "id": "dad23b5fd1ea29b6" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "array([[nan, 0. , nan],\n [nan, 0.5, nan],\n [nan, 0.5, nan],\n [nan, 0.5, nan],\n [nan, 0.5, nan],\n [nan, 0.5, nan],\n [nan, 0. , nan]])" + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X = UniformDistribution(a=[b, a, np.nan],\n", + " b=[a, b, np.nan]) # recall that the domain of b is (a, oo)\n", + "X.pdf(x[:, np.newaxis])" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.111914400Z", + "start_time": "2024-04-30T15:27:30.890331800Z" + } + }, + "id": "f6c2ca9eae94a9a", + "execution_count": 12 + }, + { + "cell_type": "markdown", + "source": [ + "Besides input validation, the parameter information is used to draw numerical values of parameters for property-based tests:" + ], + "metadata": { + "collapsed": false + }, + "id": "2a70e68dd36078ae" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(array([[-0.98949098, -0.63917425, -0.59394394],\n [-0.98949098, -0.63917425, -0.59394394]]),\n array([[1.04129879, 1.04129879, 1.04129879],\n [0.26521913, 0.26521913, 0.26521913]]))" + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "UniformDistribution._a_param.typical = _RealDomain(endpoints=(-2, 0))\n", + "UniformDistribution._b_param.typical = _RealDomain(endpoints=(0, 2))\n", + "X = UniformDistribution._draw(sizes=[(3,), (2, 1)])\n", + "X.a, X.b" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.113908900Z", + "start_time": "2024-04-30T15:27:30.892959400Z" + } + }, + "id": "5de408de6a0b5a71", + "execution_count": 13 + }, + { + "cell_type": "markdown", + "source": [ + "and to generate documentation:" + ], + "metadata": { + "collapsed": false + }, + "id": "26ae148f6b46b109" + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "The pdf is :math:`1 / (b-a)`...\n", + "\n", + "\n", + "for :math:`x` in [a, b].\n", + "This class accepts one parameterization:\n", + "`a` for :math:`a ∈ (-∞, ∞)`, `b` for :math:`b ∈ (a, ∞)`.\n", + "\n", + "\n", + "Parameters\n", + "----------\n", + "tol : positive float, optional\n", + " The desired relative tolerance of calculations. Left unspecified,\n", + " calculations may be faster; when provided, calculations may be\n", + " more likely to meet the desired accuracy.\n", + "iv_policy : {None, \"skip_all\"}\n", + " Specifies the level of input validation to perform. Left unspecified,\n", + " input validation is performed to ensure appropriate behavior in edge\n", + " case (e.g. parameters out of domain, argument outside of distribution\n", + " support, etc.) and improve consistency of output dtype, shape, etc.\n", + " Pass ``'skip_all'`` to avoid the computational overhead of these\n", + " checks when rough edges are acceptable.\n", + "cache_policy : {None, \"no_cache\"}\n", + " Specifies the extent to which intermediate results are cached. Left\n", + " unspecified, intermediate results of some calculations (e.g. distribution\n", + " support, moments, etc.) are cached to improve performance of future\n", + " calculations. Pass ``'no_cache'`` to reduce memory reserved by the class\n", + " instance.\n", + "rng : numpy.random.Generator\n", + " Random number generator to be used by any methods that require\n", + " pseudo-random numbers (e.g. `sample`).\n", + "\n", + "Notes\n", + "-----\n", + "The following abbreviations are used throughout the documentation.\n", + "\n", + "- PDF: probability density function\n", + "- CDF: cumulative distribution function\n", + "- CCDF: complementary CDF\n", + "- entropy: differential entropy\n", + "- log-*F*: logarithm of *F* (e.g. log-CDF)\n", + "- inverse *F*: inverse function of *F* (e.g. inverse CDF)\n", + "\n", + "The API documentation is written to describe the API, not to serve as\n", + "a statistical reference. Effort is made to be correct at the level\n", + "required to use the functionality, not to be mathematically rigorous.\n", + "For example, continuity and differentiability may be implicitly assumed.\n", + "For precise mathematical definitions, consult your preferred mathematical\n", + "text.\n", + "\n", + "Examples\n", + "--------\n", + "To use the distribution class, it must be instantiated using keyword\n", + "parameters corresponding with one of the accepted parameterizations.\n", + "\n", + ">>> import numpy as np\n", + ">>> import matplotlib.pyplot as plt\n", + ">>> from scipy import stats\n", + ">>> from scipy.stats import UniformDistribution\n", + ">>> X = UniformDistribution(a=-1.81, b=0.38)\n", + "\n", + "For convenience, the ``plot`` method can be used to visualize the density\n", + "and other functions of the distribution.\n", + "\n", + ">>> X.plot()\n", + ">>> plt.show()\n", + "\n", + "The support of the underlying distribution is available using the ``support``\n", + "method.\n", + "\n", + ">>> X.support()\n", + "(-1.81, 0.38)\n", + "\n", + "The numerical values of parameters associated with all parameterizations\n", + "are available as attributes.\n", + "\n", + ">>> X.a, X.b\n", + "(-1.81, 0.38)\n", + "\n", + "To evaluate the probability density function of the underlying distribution\n", + "at argument ``x=-1.11``:\n", + "\n", + ">>> x = -1.11\n", + ">>> X.pdf(x)\n", + "0.4566210045662101\n", + "\n", + "The cumulative distribution function, its complement, and the logarithm\n", + "of these functions are evaluated similarly.\n", + "\n", + ">>> np.allclose(np.exp(X.logccdf(x)), 1 - X.cdf(x))\n", + "True\n", + "\n", + "The inverse of these functions with respect to the argument ``x`` is also\n", + "available.\n", + "\n", + ">>> logp = np.log(1 - X.ccdf(x))\n", + ">>> np.allclose(X.ilogcdf(logp), x)\n", + "True\n", + "\n", + "Note that distribution functions and their logarithms also have two-argument\n", + "versions for working with the probability mass between two arguments. The\n", + "result tends to be more accurate than the naive implementation because it avoids\n", + "subtractive cancellation.\n", + "\n", + ">>> y = -0.41\n", + ">>> np.allclose(X.ccdf(x, y), 1 - (X.cdf(y) - X.cdf(x)))\n", + "True\n", + "\n", + "There are methods for computing measures of central tendency,\n", + "dispersion, higher moments, and entropy.\n", + "\n", + ">>> X.mean(), X.median(), X.mode()\n", + "(-0.7150000000000001, -0.7150000000000001, -1.81)\n", + ">>> X.variance(), X.standard_deviation()\n", + "(0.3996750000000001, 0.6321985447626403)\n", + ">>> X.skewness(), X.kurtosis()\n", + "(4.2143442886236857e-16, 1.7999999999999987)\n", + ">>> np.allclose(X.moment(order=6, kind='standardized'),\n", + "... X.moment(order=6, kind='central') / X.variance()**3)\n", + "True\n", + ">>> np.allclose(np.exp(X.logentropy()), X.entropy())\n", + "True\n", + "\n", + "Pseudo-random and quasi-Monte Carlo samples can be drawn from\n", + "the underlying distribution using ``sample``.\n", + "\n", + ">>> rng = np.random.default_rng(2354873452)\n", + ">>> X.sample(shape=(4,), rng=rng)\n", + "array([ 0.06535807, -1.4955256 , -1.23564468, -1.3680038 ])\n", + ">>> n = 200\n", + ">>> s = X.sample(shape=(n,), rng=rng, qmc_engine=stats.qmc.Halton)\n", + ">>> assert np.count_nonzero(s < X.median()) == n/2\n", + "\n", + "\n", + "Attributes\n", + "----------\n", + "All parameters are available as attributes.\n", + "\n", + "Methods\n", + "-------\n", + "support\n", + "plot\n", + "sample\n", + "fit\n", + "moment\n", + "mean\n", + "median\n", + "mode\n", + "variance\n", + "standard_deviation\n", + "skewness\n", + "kurtosis\n", + "pdf\n", + "logpdf\n", + "cdf\n", + "icdf\n", + "ccdf\n", + "iccdf\n", + "logcdf\n", + "ilogcdf\n", + "logccdf\n", + "ilogccdf\n", + "entropy\n", + "logentropy\n" + ] + } + ], + "source": [ + "from scipy.stats._distribution_infrastructure import _combine_docs\n", + "UniformDistribution.__doc__ = \"The pdf is :math:`1 / (b-a)`...\"\n", + "print(_combine_docs(UniformDistribution))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.135029400Z", + "start_time": "2024-04-30T15:27:30.901131900Z" + } + }, + "id": "72dfd8d89b5f1caf", + "execution_count": 14 + }, + { + "cell_type": "markdown", + "source": [ + "\n", + "### Transformations\n", + "\n", + "Transformations can be applied to random variables. For instance, shifted and scaled versions can be created using `ShiftedScaledDistribution`." + ], + "metadata": { + "collapsed": false + }, + "id": "3ea488862163cb9b" + }, + { + "cell_type": "code", + "execution_count": 15, + "outputs": [], + "source": [ + "from scipy.stats._distribution_infrastructure import ShiftedScaledDistribution\n", + "x = 1.\n", + "loc = np.asarray([1, 2, 3])\n", + "scale = np.asarray([2, 3])[:, np.newaxis]\n", + "X = stats.Normal()\n", + "Y = stats.ShiftedScaledDistribution(X, loc=loc, scale=scale)\n", + "np.testing.assert_equal(Y.cdf(x), X.cdf((x-loc)/scale))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.154510200Z", + "start_time": "2024-04-30T15:27:30.950086200Z" + } + }, + "id": "377b44afd3b579e1" + }, + { + "cell_type": "code", + "execution_count": 16, + "outputs": [ + { + "data": { + "text/plain": "(array([[1., 2., 3.],\n [1., 2., 3.]]),\n array([[2., 2., 2.],\n [3., 3., 3.]]))" + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Y.loc, Y.scale" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.156553300Z", + "start_time": "2024-04-30T15:27:30.965182900Z" + } + }, + "id": "a2d7011ee52e8bd9" + }, + { + "cell_type": "markdown", + "source": [ + "For convenience, a `ShiftedScaledDistribution` can be created simply by performing an elementary arithmetic operation between a random variable and a numerical array." + ], + "metadata": { + "collapsed": false + }, + "id": "ec4b62fe565806ff" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "Y = X*scale + loc\n", + "np.testing.assert_equal(Y.cdf(x), X.cdf((x-loc)/scale))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.167840500Z", + "start_time": "2024-04-30T15:27:30.969493700Z" + } + }, + "id": "595075985b8c7832", + "execution_count": 17 + }, + { + "cell_type": "markdown", + "source": [ + " There are several advantages of this architecture compared to building transformations directly into the `ContinuousDistribution` class:\n", + "- It allows distributions to use common parameterizations. By contrast, `rv_continuous` requires parameterizations to consider `loc` and `scale` or risk overparameterization (e.g. [gh-14716](https://github.com/scipy/scipy/issues/14716)). For example,\n", + " - `stats.uniform` does not allow parameterization with the left and right support endpoints; it only accepts `loc` and `scale`.\n", + " - `stats.loguniform` accepts the left and right support endpoints as shape parameters `a` and `b`; consequently, `a`, `b`, and `scale` are not independent parameters.\n", + "- Any overhead associated with a transformation is avoided unless the transformation is intentionally applied. (Although this is possible to achieve even if the transformation capabilities are built into the class, it may require special care.)\n", + "- It is highly extensible. For instance, transformations can also be used to generically define:\n", + " - truncated distributions\n", + " - half/double distributions\n", + " - wrapped distributions\n", + " - order statistic distributions\n", + " - $\\log$/$\\exp$ transformed distributions\n", + "\n", + " and these transformations can be applied in any order.\n", + "- It avoids common pitfalls when fitting distributions to data. For instance, in the current infrastructure:\n", + " - Users often forget to fix the location of distributions which almost always have fixed locations. This often results in poor fits or unexpected values of fit parameters.\n", + " - It is impossible to fix the truncation points of truncated distributions because the loc-scale transformation is applied *after* the shape parameters truncate the support. It is more naturable to use the distribution if the these transformations are applied in the opposite order." + ], + "metadata": { + "collapsed": false + }, + "id": "a2cd087763b265c1" + }, + { + "cell_type": "markdown", + "source": [ + "Negative scale (multiplication by negative values) is supported. This eliminates the need to have separate left and right versions of some distributions (e.g. Weibull, Gumbel)." + ], + "metadata": { + "collapsed": false + }, + "id": "1d4e659c852eea59" + }, + { + "cell_type": "code", + "execution_count": 18, + "outputs": [ + { + "data": { + "text/plain": "((1.0, 2.0), (-2.0, -1.0))" + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X = stats.LogUniform(a=1, b=2)\n", + "Y = stats.ShiftedScaledDistribution(X, loc=0, scale=-1)\n", + "X.support(), Y.support()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:31.171941600Z", + "start_time": "2024-04-30T15:27:30.975701200Z" + } + }, + "id": "6f10caa3f86e68c8" + }, + { + "cell_type": "markdown", + "source": [ + "### Performance\n", + "#### Overhead\n", + "I've been careful to reduce overhead where possible." + ], + "metadata": { + "collapsed": false + }, + "id": "7d0155ff218ac194" + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5.54 µs ± 33.4 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)\n" + ] + } + ], + "source": [ + "x = 1.\n", + "X = stats.Normal()\n", + "%timeit X.pdf(x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:35.495700400Z", + "start_time": "2024-04-30T15:27:30.981560500Z" + } + }, + "id": "ac9934d57874eb09", + "execution_count": 19 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "70 µs ± 1.13 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" + ] + } + ], + "source": [ + "dist = stats.norm() # old infrastructure\n", + "%timeit dist.pdf(x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:41.221859700Z", + "start_time": "2024-04-30T15:27:35.494655200Z" + } + }, + "id": "8f37cf4d237c9e2e", + "execution_count": 20 + }, + { + "cell_type": "markdown", + "source": [ + "Even though these are meant to be instantiated once and used many times, instantiation followed by use is still tends to be faster than in the old infrastructure." + ], + "metadata": { + "collapsed": false + }, + "id": "608943624453549e" + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "14.7 µs ± 94 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)\n" + ] + } + ], + "source": [ + "%timeit stats.Normal().pdf(x) # new infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:54.133646600Z", + "start_time": "2024-04-30T15:27:41.219811300Z" + } + }, + "id": "ffb9f7f068ce8184", + "execution_count": 21 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "69.4 µs ± 403 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" + ] + } + ], + "source": [ + "%timeit stats.norm.pdf(x) # old infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:27:59.787683Z", + "start_time": "2024-04-30T15:27:54.131571100Z" + } + }, + "id": "f2466b855f766756", + "execution_count": 22 + }, + { + "cell_type": "markdown", + "source": [ + "If there's still too much overhead, the user can disable input validation." + ], + "metadata": { + "collapsed": false + }, + "id": "2367e2cb3ae7785f" + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.85 µs ± 74.7 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)\n" + ] + } + ], + "source": [ + "X = stats.Normal(iv_policy='skip_all')\n", + "%timeit X.pdf(x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:15.081966700Z", + "start_time": "2024-04-30T15:27:59.786660500Z" + } + }, + "id": "eb1dc88d984f5365", + "execution_count": 23 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8.32 µs ± 52 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)\n" + ] + } + ], + "source": [ + "%timeit stats.Normal(iv_policy='skip_all').pdf(x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:22.911292400Z", + "start_time": "2024-04-30T15:28:15.080943600Z" + } + }, + "id": "a100f2519bbd6b6a", + "execution_count": 24 + }, + { + "cell_type": "markdown", + "source": [ + "Overhead increases when shape parameters are invalid, need to be broadcast, or need to be converted to a floating point type for calculations. In these cases, there has been substantial effort to keep the overhead low and provide performance comparable to or better than `rv_continuous`." + ], + "metadata": { + "collapsed": false + }, + "id": "db8afc8c5161b31a" + }, + { + "cell_type": "markdown", + "source": [ + "#### Numerical calculations\n", + "Another important aspect of performance is that of methods for which analytical formulas are not available. For example, the Gauss hypergeometric distribution can be defined as follows." + ], + "metadata": { + "collapsed": false + }, + "id": "1c36e2c0e3865d32" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from scipy.stats._distribution_infrastructure import (ContinuousDistribution, _RealDomain,\n", + " _RealParameter, _Parameterization, oo)\n", + "from scipy import special\n", + "\n", + "class GaussHyper(ContinuousDistribution):\n", + " \"\"\"Gauss hypergeometric distribution\"\"\"\n", + "\n", + " _a_param = _RealParameter('a', domain=_RealDomain(endpoints=(0, oo)))\n", + " _b_param = _RealParameter('b', domain=_RealDomain(endpoints=(0, oo)))\n", + " _c_param = _RealParameter('c', domain=_RealDomain(endpoints=(-oo, oo)))\n", + " _z_param = _RealParameter('z', domain=_RealDomain(endpoints=(-1, oo)))\n", + " _x_param = _RealParameter('x', domain=_RealDomain(endpoints=(0, 1), inclusive=(True, True)))\n", + "\n", + " _parameterizations = [_Parameterization(_a_param, _b_param, _c_param, _z_param)]\n", + " _variable = _x_param\n", + "\n", + " def _pdf_formula(self, x, *, a, b, c, z, **kwargs):\n", + " Cinv = special.gamma(a) * special.gamma(b) / special.gamma(a + b) * special.hyp2f1(c, a, a + b, -z)\n", + " return 1.0 / Cinv * x ** (a - 1.0) * (1.0 - x) ** (b - 1.0) / (1.0 + z * x) ** c\n", + "\n", + "a, b, c, z = 1.5, 2.5, 2, 0\n", + "X = GaussHyper(a=a, b=b, c=c, z=z)\n", + "x = 0.5" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:22.915552Z", + "start_time": "2024-04-30T15:28:22.909220Z" + } + }, + "id": "14fb96ede602dfb1", + "execution_count": 25 + }, + { + "cell_type": "markdown", + "source": [ + "For scalar shapes and argument, performance of the new and old infrastructures are comparable." + ], + "metadata": { + "collapsed": false + }, + "id": "69b73e50c00a6800" + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "745 µs ± 1.61 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" + ] + } + ], + "source": [ + "%timeit X.cdf(x) # new infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:28.976157600Z", + "start_time": "2024-04-30T15:28:22.916574800Z" + } + }, + "id": "7ffb1e9e129b511f", + "execution_count": 26 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "891 µs ± 8.1 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" + ] + } + ], + "source": [ + "%timeit stats.gausshyper.cdf(x, a, b, c, z) # old infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:36.206376300Z", + "start_time": "2024-04-30T15:28:28.975134700Z" + } + }, + "id": "c5c6eb2f9a84a148", + "execution_count": 27 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8.93 ms ± 29.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%timeit X.icdf(x) # new infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:43.459817100Z", + "start_time": "2024-04-30T15:28:36.208420300Z" + } + }, + "id": "553d6a732fa84614", + "execution_count": 28 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7.38 ms ± 67.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%timeit stats.gausshyper.ppf(x, a, b, c, z) # old infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:49.492254100Z", + "start_time": "2024-04-30T15:28:43.458791200Z" + } + }, + "id": "ef5206416beb2d61", + "execution_count": 29 + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "np.testing.assert_allclose(X.cdf(x), stats.gausshyper.cdf(x, a, b, c, z))\n", + "np.testing.assert_allclose(X.icdf(x), stats.gausshyper.ppf(x, a, b, c, z))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:49.537692400Z", + "start_time": "2024-04-30T15:28:49.491220600Z" + } + }, + "id": "2c161c025a6927a4", + "execution_count": 30 + }, + { + "cell_type": "markdown", + "source": [ + "But the quadrature and rootfinding code of the new infrastructure is vectorized (and eventually will be Array-API compatible), so it is much faster when arrays are involved." + ], + "metadata": { + "collapsed": false + }, + "id": "d04c76171a2ad406" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "x = np.linspace(0, 1, 1000)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:49.539736500Z", + "start_time": "2024-04-30T15:28:49.514562400Z" + } + }, + "id": "89826e8181e13e7", + "execution_count": 31 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5.84 ms ± 35.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%timeit X.cdf(x) # new infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:28:54.286813300Z", + "start_time": "2024-04-30T15:28:49.517150400Z" + } + }, + "id": "64db9452fdf042", + "execution_count": 32 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "789 ms ± 6.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%timeit stats.gausshyper.cdf(x, a, b, c, z) # old infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:00.604668100Z", + "start_time": "2024-04-30T15:28:54.286813300Z" + } + }, + "id": "37aa5e8d338ff0f8", + "execution_count": 33 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "71.3 ms ± 245 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%timeit X.icdf(x) # new infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:06.391149Z", + "start_time": "2024-04-30T15:29:00.602599700Z" + } + }, + "id": "826d67f351f1450f", + "execution_count": 34 + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7.35 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "# Warning: takes a long time\n", + "%timeit -r 1 -n 1 stats.gausshyper.ppf(x, a, b, c, z) # old infrastructure" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.746184400Z", + "start_time": "2024-04-30T15:29:06.391149Z" + } + }, + "id": "cc3639a8c25d9cfe", + "execution_count": 35 + }, + { + "cell_type": "markdown", + "source": [ + "There are plans for the new infrastructure to use interpolation for additional performance gains with very large arrays." + ], + "metadata": { + "collapsed": false + }, + "id": "286bf566f1a6d322" + }, + { + "cell_type": "markdown", + "source": [ + "### Distribution properties\n", + "The new infrastructure has the distribution \"properties\" one would expect. `mode`, `skewness`, `kurtosis`, and `logentropy` are new." + ], + "metadata": { + "collapsed": false + }, + "id": "616ec775ae781135" + }, + { + "cell_type": "code", + "execution_count": 36, + "outputs": [ + { + "data": { + "text/plain": "(0.0, 0.0, 0.0)" + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X = stats.Normal()\n", + "X.mean(), X.median(), X.mode()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.750280100Z", + "start_time": "2024-04-30T15:29:13.744130700Z" + } + }, + "id": "2761d62a0d440d5d" + }, + { + "cell_type": "code", + "execution_count": 37, + "outputs": [ + { + "data": { + "text/plain": "(1.0, 1.0)" + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.standard_deviation(), X.variance()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.765029Z", + "start_time": "2024-04-30T15:29:13.748226Z" + } + }, + "id": "5a56829847701733" + }, + { + "cell_type": "code", + "execution_count": 38, + "outputs": [ + { + "data": { + "text/plain": "(0.0, 3.0)" + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.skewness(), X.kurtosis() # *Pearson* kurtosis" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.767021100Z", + "start_time": "2024-04-30T15:29:13.753933300Z" + } + }, + "id": "1fd5acab1427e5cd" + }, + { + "cell_type": "code", + "execution_count": 39, + "outputs": [ + { + "data": { + "text/plain": "(1.4189385332046727, (0.34990908025919965+0j))" + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.entropy(), X.logentropy()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.845229300Z", + "start_time": "2024-04-30T15:29:13.759331300Z" + } + }, + "id": "d82789a8439dedb1" + }, + { + "cell_type": "markdown", + "source": [ + "Note that the `logentropy` method returns a complex value because the entropy can be negative. The logarithm of a negative number is the logarithm of the number's magnitude plus an odd multiple of $\\pi i$." + ], + "metadata": { + "collapsed": false + }, + "id": "54603d725a49793" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(-0.019939330301691684, (-3.9150611006848894+3.141592653589793j))" + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Y = stats.LogUniform(a=1, b=2)\n", + "Y.entropy(), Y.logentropy()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.869417900Z", + "start_time": "2024-04-30T15:29:13.766023900Z" + } + }, + "id": "7b6a31e308ef63b4", + "execution_count": 40 + }, + { + "cell_type": "markdown", + "source": [ + "These are implemented as methods rather than `@property`s because they accept arguments. For instance, the entropy can be computed using the analytical formula, by exponentiating the log-entropy, or by quadrature." + ], + "metadata": { + "collapsed": false + }, + "id": "b133dd97ea4268d3" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(1.4189385332046727, 1.4189385332046727, 1.4189385332046731)" + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.entropy(), X.entropy(method='logexp'), X.entropy(method='quadrature')" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.884931500Z", + "start_time": "2024-04-30T15:29:13.775097400Z" + } + }, + "id": "f66869b8978fc9fd", + "execution_count": 41 + }, + { + "cell_type": "markdown", + "source": [ + "### Distribution functions\n", + "Functions of the distributions underlying the random variables follow a consistent naming scheme.\n", + "- prefix `i` is for \"inverse\"\n", + "- prefix `c` is for \"complementary\"\n", + "- prefix `log` is for \"logarithm of\"" + ], + "metadata": { + "collapsed": false + }, + "id": "11ac9788b33169d0" + }, + { + "cell_type": "code", + "execution_count": 42, + "outputs": [], + "source": [ + "x = 1.\n", + "np.testing.assert_allclose(X.icdf(X.cdf(x)), x)\n", + "np.testing.assert_allclose(X.iccdf(X.ccdf(x)), x)\n", + "np.testing.assert_allclose(X.ilogcdf(X.logcdf(x)), x)\n", + "np.testing.assert_allclose(X.ilogccdf(X.logccdf(x)), x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.887991800Z", + "start_time": "2024-04-30T15:29:13.780300300Z" + } + }, + "id": "f9bb468cbe78ee13" + }, + { + "cell_type": "markdown", + "source": [ + "Note the addition of new methods for the inverse of the logarithm of distribution functions. These are useful when the argument of `icdf` would be too small or too close to `1.0` to represent accurately using floating point numbers." + ], + "metadata": { + "collapsed": false + }, + "id": "9c872296fe2d2258" + }, + { + "cell_type": "code", + "execution_count": 43, + "outputs": [], + "source": [ + "np.testing.assert_allclose(X.ilogcdf(X.logcdf(-1000.)), -1000)\n", + "np.testing.assert_allclose(X.ilogccdf(X.logccdf(1000.)), 1000)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.889021200Z", + "start_time": "2024-04-30T15:29:13.786508600Z" + } + }, + "id": "8a265987f2a1b9a5" + }, + { + "cell_type": "markdown", + "source": [ + "The distribution methods also have two-argument versions." + ], + "metadata": { + "collapsed": false + }, + "id": "a3ccc22fa4ae3ce6" + }, + { + "cell_type": "code", + "execution_count": 44, + "outputs": [], + "source": [ + "x1, x2 = 1., 2.\n", + "np.testing.assert_allclose(X.cdf(x1, x2),\n", + " X.cdf(x2) - X.cdf(x1))\n", + "np.testing.assert_allclose(X.ccdf(x1, x2),\n", + " 1 - X.cdf(x1, x2))\n", + "np.testing.assert_allclose(X.logcdf(x1, x2),\n", + " np.log(X.cdf(x1, x2)))\n", + "np.testing.assert_allclose(X.logccdf(x1, x2),\n", + " np.log(X.ccdf(x1, x2)))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.900751800Z", + "start_time": "2024-04-30T15:29:13.793133Z" + } + }, + "id": "f10a6436be7ccdda" + }, + { + "cell_type": "markdown", + "source": [ + "Besides convenience, this avoids catastropic cancellation where possible." + ], + "metadata": { + "collapsed": false + }, + "id": "958b7469f941be91" + }, + { + "cell_type": "code", + "execution_count": 45, + "outputs": [ + { + "data": { + "text/plain": "(2.7535164718735247e-89, 0.0)" + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x1, x2 = 20., 20.5\n", + "X.cdf(20, 20.5), X.cdf(x2) - X.cdf(x1)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:13.903842600Z", + "start_time": "2024-04-30T15:29:13.800384400Z" + } + }, + "id": "d89c00e5116d4b8c" + }, + { + "cell_type": "markdown", + "source": [ + "For numerically challenging cases, there are alternative `method` options available." + ], + "metadata": { + "collapsed": false + }, + "id": "54636c538c3b702a" + }, + { + "cell_type": "code", + "execution_count": 46, + "outputs": [], + "source": [ + "eps = 1e-100\n", + "res = X.logcdf(0., eps, method='quadrature')\n", + "ref = X.logpdf(0.) + np.log(eps)\n", + "np.testing.assert_equal(res, ref)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.038448100Z", + "start_time": "2024-04-30T15:29:13.805600900Z" + } + }, + "id": "ed5fa2b90a7f4beb" + }, + { + "cell_type": "markdown", + "source": [ + "All distribution functions from the old distribution infrastructure are available in the new infrastructure (albeit under different names) with the following exceptions.\n", + "- `interval` is not available as a separate method, but the same values can be calculated using `iccdf` and `icdf`. However, the probability interval is in some sense an inverse of the two-argument `cdf`, so we could consider adding the capabilities to `icdf`.\n", + "- `expect` will not be supported. In the old infrastructure, this was little more than a light wrapper around an integrator, and we cannot do much better in general cases. The bug report to convenience ratio was too unfavorable to justify inclusion in the new infrastructure." + ], + "metadata": { + "collapsed": false + }, + "id": "c88a08812726ffda" + }, + { + "cell_type": "markdown", + "source": [ + "### Random Sampling\n", + "Technically, \"observe\" might be a better name for this method, since instances like `X` represent a random variable. In any case, `sample` is easier to interpret than `rvs`:" + ], + "metadata": { + "collapsed": false + }, + "id": "a4b61b57321f2670" + }, + { + "cell_type": "code", + "execution_count": 47, + "outputs": [ + { + "data": { + "text/plain": "-1.0874652320186675" + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.sample()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.041628300Z", + "start_time": "2024-04-30T15:29:13.813440800Z" + } + }, + "id": "969af810b6747cc" + }, + { + "cell_type": "markdown", + "source": [ + "Currently, a Generator can be passed either during construction or when calling the `sample` method." + ], + "metadata": { + "collapsed": false + }, + "id": "2ed728ba01c88028" + }, + { + "cell_type": "code", + "execution_count": 48, + "outputs": [], + "source": [ + "rng = np.random.default_rng(872438745698345)\n", + "X = stats.Normal(rng=rng)\n", + "sample1 = X.sample()\n", + "\n", + "rng2 = np.random.default_rng(872438745698345)\n", + "sample2 = X.sample(rng=rng2)\n", + "\n", + "np.testing.assert_equal(sample1, sample2)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.043622400Z", + "start_time": "2024-04-30T15:29:13.818294500Z" + } + }, + "id": "94e158097dc6373" + }, + { + "cell_type": "markdown", + "source": [ + "The parameter that controls the shape of the sample is called `shape`." + ], + "metadata": { + "collapsed": false + }, + "id": "bf1e1c31751c7ae7" + }, + { + "cell_type": "code", + "execution_count": 49, + "outputs": [ + { + "data": { + "text/plain": "array([[-0.52931289, 0.88922355, 0.1664083 ],\n [ 0.39524223, 0.30158984, -0.87769806]])" + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.sample(shape=(2, 3))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.072371300Z", + "start_time": "2024-04-30T15:29:13.825275700Z" + } + }, + "id": "21185b9af0cf6a2c" + }, + { + "cell_type": "markdown", + "source": [ + "`QMCEngine`s can also be used. Each slice along the last axis is generated from an independent low-discrepancy sequence. (*Note: currently, this is not the way it works, but that is what is slated to happen.*)" + ], + "metadata": { + "collapsed": false + }, + "id": "1366b899f48f9064" + }, + { + "cell_type": "code", + "execution_count": 50, + "outputs": [], + "source": [ + "qrng = stats.qmc.Halton\n", + "n_observations = 10000\n", + "sample1 = X.sample(shape=(n_observations,), qmc_engine=qrng)\n", + "# Verify a property we would expect to hold exactly\n", + "np.testing.assert_equal((sample1 > 0).sum(), n_observations/2)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.084673800Z", + "start_time": "2024-04-30T15:29:13.830520Z" + } + }, + "id": "66482a31e0faa1b8" + }, + { + "cell_type": "markdown", + "source": [ + "An important change is that the user does not need to consider the shape of the distribution parameters when specifying the `shape` of the sample. Instead, the shape of the output array is the specified `shape` concatenated with the distribution shape." + ], + "metadata": { + "collapsed": false + }, + "id": "93a13b430549a855" + }, + { + "cell_type": "code", + "execution_count": 51, + "outputs": [ + { + "data": { + "text/plain": "True" + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_observations = 4\n", + "X_temp = stats.LogUniform(a=[0.5, 0.9],\n", + " b=[[1], [2], [3]])\n", + "sample = X_temp.sample(shape=n_observations)\n", + "sample.shape == (n_observations,) + X_temp._shape" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.089771500Z", + "start_time": "2024-04-30T15:29:13.841124500Z" + } + }, + "id": "5357a662fafd529d" + }, + { + "cell_type": "markdown", + "source": [ + "### Moments\n", + "\n", + "The `moment` method can compute raw, central, and standard moments of any order." + ], + "metadata": { + "collapsed": false + }, + "id": "94380ae1dbac08a3" + }, + { + "cell_type": "code", + "execution_count": 52, + "outputs": [ + { + "data": { + "text/plain": "944.9999999999995" + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.testing.assert_equal(X.moment(order=1, kind='raw'),\n", + " X.mean())\n", + "np.testing.assert_equal(X.moment(order=2, kind='central'),\n", + " X.variance())\n", + "np.testing.assert_equal(X.moment(order=3, kind='standardized'),\n", + " X.skewness())\n", + "\n", + "X.moment(order=10, kind='standardized')" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.101532500Z", + "start_time": "2024-04-30T15:29:13.856834800Z" + } + }, + "id": "668c3b69b36f4906" + }, + { + "cell_type": "markdown", + "source": [ + "### Fitting\n", + "There is a draft of a generalized `fit` method. The method would unify techniques like maximum likelihood estimation with other needs, such as inverting distribution functions with respect to distribution parameters. We begin by initializing a normal distribution." + ], + "metadata": { + "collapsed": false + }, + "id": "b0d30a0e3523bc25" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from scipy.stats._new_distributions import Normal\n", + "X = Normal(mu=-1, sigma=0.5)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.103610100Z", + "start_time": "2024-04-30T15:29:13.864187800Z" + } + }, + "id": "ee8b0f613e240b28", + "execution_count": 53 + }, + { + "cell_type": "markdown", + "source": [ + "Suppose we know the desired mean and standard deviation and wish to fit the `mu` and `sigma` parameters of the distribution to achieve them." + ], + "metadata": { + "collapsed": false + }, + "id": "dc25c2386d80951e" + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.4999999949560409 1.4999999948222957\n" + ] + } + ], + "source": [ + "parameters = ['mu', 'sigma']\n", + "objective = {'f': lambda: [X.mean(), X.standard_deviation()],\n", + " 'output': [0.5, 1.5]}\n", + "X.fit(parameters, objective)\n", + "print(X.mean(), X.standard_deviation())" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.108725200Z", + "start_time": "2024-04-30T15:29:13.868392300Z" + } + }, + "id": "4ac12091fc4ea6fe", + "execution_count": 54 + }, + { + "cell_type": "markdown", + "source": [ + "Or if we know the desired values of the `pdf` and `cdf` when the argument is `0`:" + ], + "metadata": { + "collapsed": false + }, + "id": "2334484c1bcaeef8" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(0.5000000014883259, 0.3499999993773727)" + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "objective = dict(f=lambda x: [X.pdf(x), X.cdf(x)],\n", + " input=[0.],\n", + " output=[0.5, 0.35])\n", + "X.fit(parameters, objective)\n", + "X.pdf(0), X.cdf(0)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.137465100Z", + "start_time": "2024-04-30T15:29:13.884931500Z" + } + }, + "id": "b85ee31c07b1924b", + "execution_count": 55 + }, + { + "cell_type": "markdown", + "source": [ + "Of course, we can still perform maximum likelihood optimization." + ], + "metadata": { + "collapsed": false + }, + "id": "d3d8e404bc5ec310" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "(0.32331186434434367, 0.7563037248012252)" + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data = X.sample(1000, rng=rng)\n", + "objective = dict(f=X.llf, input=(data,))\n", + "X.fit(parameters, objective)\n", + "X.mu, X.sigma" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.139514400Z", + "start_time": "2024-04-30T15:29:13.908436900Z" + } + }, + "id": "454539b6026961", + "execution_count": 56 + }, + { + "cell_type": "markdown", + "source": [ + "Currently, `fit` relies entirely on generic optimization procedures. In future work, the behavior can be overridden depending on the distribution, parameters, and objectives.." + ], + "metadata": { + "collapsed": false + }, + "id": "bc1710fa260f16db" + }, + { + "cell_type": "markdown", + "source": [ + "### Visualization\n", + "\n", + "We can visualize the results of the fit above using the convenience method, `plot`." + ], + "metadata": { + "collapsed": false + }, + "id": "a7e4ec232384701e" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "
", + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "ax = X.plot()\n", + "ax = plt.hist(data, density=True, alpha=0.5, bins=50)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.461421700Z", + "start_time": "2024-04-30T15:29:13.916884300Z" + } + }, + "id": "c5cc49cd89576a56", + "execution_count": 57 + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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KI9wyywpKQlPT8/b7ldRX8fs7Gz27duX5zUA6N69e6Gvwc6dO/Pt36NHD/bu3Yter7darJaSlJQEUKzXrUWLFvj7+3PvvfeyefNma4dWJmfOnCEgIICwsDD69+/P+fPnC923sr+G2dnZrFixgueee67QmxPnqEyv4a1K+/1Y2Gtbmb5TNRoNHh4eRe6XmppKSEgItWrVonfv3hw4cKB8AryFJDf/sXjxYnr06EFQUFCR+/Xs2ZOVK1eyadMmPvroI/bs2cM999xDVlZWOUVaMi+//DJff/01mzdv5sUXX2TWrFmMGDGi0P1jYmIA8PX1zbPe19c3d1tFdu7cOT777DOGDx9e5H4V+XWMi4vDaDSW6DWIiYkpcH+DwUBcXJzVYrUERVEYO3Ysd955J02aNCl0P39/fz7//HPWrFnD2rVradCgAffeey9//fVXOUZbfG3btmX58uWsX7+eL774gpiYGDp06EB8fHyB+1fm1xDghx9+IDExsch/ECvba/hfpf1+LOy1rQzfqZmZmYwbN44nn3yyyBtmhoeHs2zZMn766SdWrVqFg4MDHTt25MyZM+UYLVW3WWrSpEkKUORjz549eY6JiopSbGxslO+++67E57t69apia2urrFmzxlKXcFulucYc3333nQIocXFxBW7fvn27AihXr17Ns37IkCFKjx49LH4thSnNNV65ckWpW7euMnjw4BKfT43XsTBXrlxRAGXHjh151k+dOlVp0KBBgcfUq1dPmTZtWp51f//9twIo0dHRVovVEkaMGKGEhIQoUVFRJT62d+/eSp8+fawQleWlpqYqvr6+ykcffVTg9sr8GiqKonTv3l3p3bt3iY+ryK8h/2myKe33o62trfLVV1/lWbdixQrF3t7eovGWxn+v8VbZ2dnKgw8+qLRo0UJJSkoqUblGo1Fp3ry5MmrUKAtEWXw666dP6njxxRfp379/kfuEhobmeb506VK8vLyK1WHqv/z9/QkJCSnX7LQ015gjZ0TQ2bNn8fLyyrc9ZzRHTEwM/v7+uetjY2Pz/edhTSW9xqtXr9KlSxfat2/P559/XuLzqfE6Fsbb2xutVpvvv7qiXgM/P78C99fpdAW+zhXFqFGj+Omnn/jrr7+oVatWiY9v164dK1assEJklufs7EzTpk0LfY9V1tcQ4OLFi/z555+sXbu2xMdWptewtN+P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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "X = stats.Normal(mu=[1, 2, 3], sigma=[1, 2, 3])\n", + "X.plot(y='cdf')" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.627103900Z", + "start_time": "2024-04-30T15:29:14.459363Z" + } + }, + "id": "512c256dd8d0cf3e", + "execution_count": 58 + }, + { + "cell_type": "markdown", + "source": [ + "The `plot` method is relatively flexible, with a signature inspired by grammar of graphics. For instance, with the argument `x` on [-10, 10], plot the `pdf` against the `cdf`." + ], + "metadata": { + "collapsed": false + }, + "id": "d7176a7e55a19bc0" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "X.plot('cdf', 'pdf', t=('x', -10, 10))" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:14.795812900Z", + "start_time": "2024-04-30T15:29:14.623858800Z" + } + }, + "id": "495124dd56043827", + "execution_count": 59 + }, + { + "cell_type": "markdown", + "source": [ + "### Order statistics distributions\n", + "There is draft support for distributions of [order statistics](https://en.wikipedia.org/wiki/Order_statistic) of distributions, partially to demonstrate the flexibility of distribution transformations. For example, we can plot the probability density functions of the order statistics of a normal distribution with sample size 4." + ], + "metadata": { + "collapsed": false + }, + "id": "433736dc830e43ba" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjoAAAHFCAYAAAD7ZFORAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAADCTklEQVR4nOydeXhU9d237zNbJvtKFkIgCfu+g4AsimBRse5WUequRW3V+lZ99HlU2oK1Kmhbdy3aolKrtS6gouwge0CQsAVCAmTf99nO+8eZM0nIQpaZOWfgd1/XXAkzZ875ZCHzme8qybIsIxAIBAKBQHAOYtBagEAgEAgEAoGvEEZHIBAIBALBOYswOgKBQCAQCM5ZhNERCAQCgUBwziKMjkAgEAgEgnMWYXQEAoFAIBCcswijIxAIBAKB4JxFGB2BQCAQCATnLMLoCAQCgUAgOGcRRkcQMGzbto2rr76a3r17ExQUREJCApMmTeK3v/1ts+NeffVVli1bpo1IN8888wySJPnlWtnZ2UiS1OxrVq8fHx9PVVVVi+ekpqZyxRVX+EWfL5gxYwYzZsxocX9WVhZBQUH88MMP/hflA5YtW4YkSezcuVNrKT7DbrczaNAgnnvuuTaPefvtt5EkibCwsA6d85133iE5OZmampoW1+rbty9Lly7tjmRBgCGMjiAg+Oqrr5g8eTKVlZU8//zzfPvtt7z88stMmTKFFStWNDtWD0ZHLxQVFfH8889rLcNvPProo8yaNYtJkyZpLUXQQV599VXKysp48MEHW3381KlTPProo/Ts2bPD5/zlL39JaGhoi999s9nM//3f/7Fw4UJKSkq6pVsQOAijIwgInn/+edLS0vjmm2/4xS9+wfTp0/nFL37BCy+8QE5OjtbyfE5tbW2Xnvezn/2MJUuWkJ+f72VFjciyTF1dnc/O31EyMzP57LPP2nzB9DVd/RmdzzgcDv785z9zxx13EBoa2uox9913H9OmTWPWrFkdPq/JZOLee+/l5ZdfbvFzuemmm5AkiTfeeKNb2gWBgzA6goCgpKSEuLg4TCZTi8cMhsZf49TUVH766SfWr1+PJElIkkRqaioA9fX1/Pa3v2XUqFFERkYSExPDpEmT+O9//9vinJIk8cADD/CPf/yDwYMHExISwsiRI/nyyy9bHPvVV18xatQogoKCSEtL44UXXmj1a/jb3/7GtGnTiI+PJzQ0lOHDh/P8889jt9ubHTdjxgyGDRvGhg0bmDx5MiEhIdxxxx0AnD59mhtuuIHw8HAiIyO58cYb2zUxf/jDH3A4HDzzzDNtHqNSWlrKggULSE5OxmKxkJ6ezpNPPklDQ0Or35vXX3+dwYMHExQUxHvvvedJs6xZs4a7776b2NhYIiIimD9/PjU1NeTn53PDDTcQFRVFUlISjz76aIuv/dlnn2XixInExMQQERHBmDFjeOedd+jI7uHXXnuNxMTEFi+I6vdzx44dTJ06lZCQENLT03nuuedwuVxnPW9r3HbbbYSFhbFv3z5mz55NeHg4M2fO7PR5XnvtNUaOHElYWBjh4eEMGjSI//mf/2lxXFVVFb/61a+Ii4sjNjaWa665htOnTzc7ZsWKFcyePZukpCSCg4MZPHgwjz/+eIv0jar9p59+YubMmYSGhtKjRw8eeOCBFqZAlmVeffVVRo0aRXBwMNHR0Vx33XUcO3as019ra3z++eecOnWKW2+9tdXH//nPf7J+/XpeffXVTp973rx5VFZW8tFHHzW732KxcOONN/Lmm2926PdKcA4gCwQBwF133SUD8oMPPihv3bpVttlsrR63e/duOT09XR49erT8ww8/yD/88IO8e/duWZZluby8XL7tttvkf/zjH/KaNWvkr7/+Wn700Udlg8Egv/fee83OA8ipqanyhAkT5H/961/yypUr5RkzZsgmk0nOysryHPfdd9/JRqNRvvDCC+VPP/1U/vjjj+Xx48fLvXv3ls/87/Xwww/Lr732mvz111/La9askZcsWSLHxcXJt99+e7Pjpk+fLsfExMgpKSnyX/7yF3nt2rXy+vXr5draWnnw4MFyZGSk/Je//EX+5ptv5F//+teea/3973/3nOPpp5+WAbmoqEh++OGHZZPJJB86dMjzeJ8+feTLL7/c8++6ujp5xIgRcmhoqPzCCy/I3377rfy///u/sslkki+77LIW35vk5GR5xIgR8gcffCCvWbNG3r9/v/z3v/9dBuS0tDT5t7/9rfztt9/Kf/rTn2Sj0SjfdNNN8pgxY+Q//OEP8urVq+XHHntMBuQXX3yx2blvu+02+Z133pFXr14tr169Wv79738vBwcHy88++2yL79H06dOb3Zeeni7fcMMNLX4npk+fLsfGxsr9+/eXX3/9dXn16tXyggULZKDFz72j/PKXv5TNZrOcmpoqL168WP7+++/lb775plPn+PDDDz2/099++6383Xffya+//rr861//2nOM+j1NT0+XH3zwQfmbb76R3377bTk6Olq+6KKLmp3v97//vbxkyRL5q6++ktetWye//vrrclpaWovjfvnLX8oWi0Xu3bu3/Mc//lH+9ttv5WeeeUY2mUzyFVdc0ezYu+++WzabzfJvf/tb+euvv5Y/+OADedCgQXJCQoKcn5/vOc7pdMp2u/2sN4fD0ez8d9xxhxwfH9/q96egoECOjY2V//a3v3l0h4aGdup7PHjwYPmaa65pcf+KFStkQP7xxx87dT5BYCKMjiAgKC4uli+88EIZkAHZbDbLkydPlhcvXixXVVU1O3bo0KEtXgRbw+FwyHa7Xb7zzjvl0aNHN3sMkBMSEuTKykrPffn5+bLBYJAXL17suW/ixIlyz5495bq6Os99lZWVckxMTAuj0xT1heH999+XjUajXFpa6nls+vTpMiB///33zZ7z2muvyYD83//+t9n9d999d7tGp7i4WI6MjJSvvfZaz+NnGp3XX39dBuR//etfzc79pz/9SQbkb7/9ttn3JjIysplmWW58UX7wwQeb3X/VVVfJgPzSSy81u3/UqFHymDFjzvo9WrhwoRwbGyu7XK5m36OmP+OCggIZkJ977rkW51G/n9u2bWt2/5AhQ+RLL720zeu3xy9/+UsZkN99990uPV+WZfmBBx6Qo6Ki2j1G/Z4uWLCg2f3PP/+8DMh5eXmtPs/lcsl2u11ev369DMh79+5tof3ll19u9pw//vGPMiBv2rRJlmVZ/uGHH1o1o7m5uXJwcLD8u9/9znOf+vt2tlufPn2anWvw4MHyz372s1a/hmuvvVaePHmy5+feFaMzb948OSEhocX9R44ckQH5tdde69T5BIGJSF0JAoLY2Fg2btzIjh07eO655/j5z3/O4cOHeeKJJxg+fDjFxcUdOs/HH3/MlClTCAsLw2QyYTabeeedd8jMzGxx7EUXXUR4eLjn3wkJCcTHx3PixAkAampq2LFjB9dccw1Wq9VzXHh4OHPnzm1xvoyMDK688kpiY2MxGo2YzWbmz5+P0+nk8OHDzY6Njo7m4osvbnbf2rVrCQ8P58orr2x2/80339zu1xwbG8tjjz3GJ598wrZt21o9Zs2aNYSGhnLdddc1u/+2224D4Pvvv292/8UXX0x0dHSr5zqzm2vw4MEAXH755S3uV7+XTXVccsklREZGer5H//d//0dJSQmFhYVtfo1qGic+Pr7VxxMTE5kwYUKz+0aMGNHi+p3l2muv7fJzJ0yYQHl5OTfddBP//e9/2/0dPvNnPmLECIBm+o8dO8bNN99MYmKi53s3ffp0gFZ/v+fNm9fs3+rv0dq1awH48ssvkSSJW265BYfD4bklJiYycuRI1q1b53nuPffcw44dO856++KLL5pd8/Tp063+zD755BO++OIL3nrrrW51L8bHx1NYWIjD4WhxPyiFzoJzn5YFDwKBjhk3bhzjxo0DlFbRxx57jCVLlvD888+ftbvo008/5YYbbuD666/n//2//0diYiImk4nXXnuNd999t8XxsbGxLe4LCgryFN6WlZXhcrlITExscdyZ9+Xk5DB16lQGDhzIyy+/TGpqKlarle3bt3P//fe3KOZNSkpqcc6SkhISEhLOeq3WeOihh/jrX//K7373O9avX9/quRMTE1u8qMTHx2MymVp0qLSmTyUmJqbZvy0WS5v319fXe/69fft2Zs+ezYwZM3jrrbfo1asXFouFzz77jD/+8Y/tFjyrjzU1nE0528+yK4SEhBAREdHl59966604HA7eeustrr32WlwuF+PHj+cPf/hDizqjM/UHBQUBjV93dXU1U6dOxWq18oc//IEBAwYQEhJCbm4u11xzTYuv02QytTin+nuk/qwLCgqQZbnV3zmA9PT0Zs9ty2Q25czfr7q6uhY/s+rqau6//34efPBBevbsSXl5OQA2mw2A8vJyzGZzm8XLTbFarciyTH19fbPWdPWaeiiiF/geYXQEAYvZbObpp59myZIl7N+//6zH//Of/yQtLY0VK1Y0+4N7ZrFtR4mOjkaSpFaLgc+877PPPqOmpoZPP/2UPn36eO7fs2dPq+du7V1sbGws27dvP+u1WiM4OJhnnnmGe+65h6+++qrVc2/btg1ZlptdW303HBcXd1Z93eWjjz7CbDbz5ZdfNnvx++yzz876XFVfaWmp13W1hTe+B7fffju33347NTU1bNiwgaeffporrriCw4cPN/s9ORtr1qzh9OnTrFu3zhPFATwm4UwcDgclJSXNzI76e6TeFxcXhyRJbNy40WOsmtL0voULF/Lss8+eVWefPn3Izs72/DsuLq7Fz6y4uJiCggJefPFFXnzxxRbniI6O5uc//3mHfi9KS0sJCgpqMX9HveaZv9eCcxNhdAQBQV5eXqtRBDUk33TGRlvv1CVJwmKxNHuBys/Pb7XrqiOEhoYyYcIEPv30U/785z97XpyrqqpahOjVazZ9cZBlmbfeeqvD17vooov417/+xeeff94slfHBBx906Pl33HEHS5Ys4fHHH2/RbTRz5kz+9a9/8dlnn3H11Vd77n///fc9j/saSZIwmUwYjUbPfXV1dfzjH/8463P79OlDcHAwWVlZvpToM0JDQ5kzZw42m42rrrqKn376qVNGp7XfL6DdFurly5fz61//2vNv9fdIHcR4xRVX8Nxzz3Hq1CluuOGGdq9/zz33dGgA5Zn6Bg0a1OJnlpiY6EmfNeW5555j/fr1rFq1qsMG5dixYwwZMqTV+4FWHxOcewijIwgILr30Unr16sXcuXMZNGgQLpeLPXv28OKLLxIWFsZvfvMbz7HDhw/no48+YsWKFaSnp2O1Whk+fDhXXHEFn376KQsWLOC6664jNzeX3//+9yQlJXHkyJEu6fr973/Pz372M2bNmsVvf/tbnE4nf/rTnwgNDW32TnXWrFlYLBZuuukmfve731FfX89rr71GWVlZh681f/58lixZwvz58/njH/9I//79WblyJd98802Hnm80Glm0aJHHyKh1Huq5//a3v/HLX/6S7Oxshg8fzqZNm1i0aBGXXXYZl1xySYd1dpXLL7+cl156iZtvvpl77rmHkpISXnjhhVajCWdisViYNGkSW7du7ZaGGTNmsH79+m61Ha9bt46LLrqIp59+ut22/rvvvpvg4GCmTJlCUlIS+fn5LF68mMjISMaPH9+pa06ePJno6Gjuu+8+nn76acxmM8uXL2fv3r2tHm+xWHjxxReprq5m/PjxbNmyhT/84Q/MmTOHCy+8EIApU6Zwzz33cPvtt7Nz506mTZtGaGgoeXl5bNq0ieHDh/OrX/0KUN5odGagn8qMGTNYuHAhtbW1hISEAEpaqbWp18uWLcNoNLZ4bNmyZdx+++38/e9/99SUAbhcLrZv386dd97Z4lxbt27FaDQybdq0TmsWBB6iGFkQEDz11FNER0ezZMkSrrzySubMmcMrr7zCJZdcwvbt2xk+fLjn2GeffZbp06dz9913M2HCBE9h8O23385zzz3HqlWruOyyy/jTn/7E448/ftZi3vaYNWsWn332GZWVldx444088sgjXHvttZ65NyqDBg3ik08+oaysjGuuuYYHH3yQUaNG8corr3T4WiEhIZ5i3ccff5zrrruOkydPtpgT0h5XXXUVkydPbnG/1Wpl7dq1zJs3jz//+c/MmTOHZcuW8eijj/Lpp592+Pzd4eKLL+bdd99l3759zJ07lyeffJLrrruOxx9/vEPPnzdvHtu3bycvL6/LGqqrqztU83S2c0D7dUwAU6dOZf/+/fzmN79h1qxZPPzwwwwYMICNGzfSo0ePTl0zNjaWr776ipCQEG655RbuuOMOwsLCWkwNV1FThKtXr+bnP/85r7zyCnfffTcff/xxs+PeeOMN/vrXv7JhwwZ+8YtfcPnll/N///d/1NTUtCju7go333wzTqez1XRqR2nr+71u3ToqKipaFF2Dkg697LLLiIqK6vJ1BYGDJHfnrYtAIBDohPr6enr37s1vf/tbHnvssU4/v6qqipiYGJYuXcr999/fZR2/+93v+PDDDzly5EibxdFactttt/Hvf//bYxC0Zu7cuTgcDlatWtWl599www0cP36cHTt2NLv/1ltv5dixY2zevLnZ/VlZWfTv359vvvmmU9OWBYGLiOgIBIJzAqvVyrPPPstLL73UYhpwR9iwYQPJycncfffd3dKxdu1a/vd//1eXJkePLF68mO+++66FUekIsiyzbt06/vjHPza7PysrixUrVvCnP/2pxXP+8Ic/MHPmTGFyziNEjY5AIDhnuOeeeygvL+fYsWPN0pkd4fLLL28x66crdOUF+3xm2LBh/P3vf+/SPjZJklqdr5STk8Nf//pXT72RisPhoG/fvjzxxBNd1isIPETqSiAQCAQCwTmLSF0JBAKBQCA4Z9Hc6Lz66qukpaVhtVoZO3YsGzdubPPY2267zbORuult6NChflQsEAgEAoEgUNDU6KxYsYKHHnqIJ598koyMDKZOncqcOXPIyclp9fiXX36ZvLw8zy03N5eYmBiuv/56PysXCAQCgUAQCGhaozNx4kTGjBnDa6+95rlv8ODBXHXVVSxevPisz//ss8+45pprOH78eIeniLpcLk6fPk14eLhPxtgLBAKBQCDwPrIsU1VVRc+ePTEYOh6n0azrymazsWvXrhbDwGbPns2WLVs6dI533nmHSy65pF2T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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from scipy.stats._new_distributions import OrderStatisticDistribution\n", + "n = 4\n", + "r = np.arange(1, n+1)\n", + "X = stats.Normal()\n", + "Y = OrderStatisticDistribution(X, r=r, n=n)\n", + "Y.plot()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:15.018238100Z", + "start_time": "2024-04-30T15:29:14.793764800Z" + } + }, + "id": "a88f7887cfbf96e2", + "execution_count": 60 + }, + { + "cell_type": "markdown", + "source": [ + "Compute the expected values of these order statistics." + ], + "metadata": { + "collapsed": false + }, + "id": "69eafc1013cdef5d" + }, + { + "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": "array([-1.02937537, -0.29701138, 0.29701138, 1.02937537])" + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Y.mean()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:15.023429800Z", + "start_time": "2024-04-30T15:29:15.017209800Z" + } + }, + "id": "5ace78c0ee084b0d", + "execution_count": 61 + }, + { + "cell_type": "markdown", + "source": [ + "The `OrderStatisticDistribution` can be shifted and scaled, or we can generate an `OrderStatsticDistribution` from a shifted and scaled distribution. (In this case, the order of operations doesn't matter, but that is not the case for all transformations.)" + ], + "metadata": { + "collapsed": false + }, + "id": "31bcef789921995a" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "loc, scale= 1, 2\n", + "Y1 = stats.ShiftedScaledDistribution(OrderStatisticDistribution(stats.Normal(), r=r, n=n), loc=loc, scale=scale)\n", + "Y2 = OrderStatisticDistribution(stats.ShiftedScaledDistribution(stats.Normal(), loc=loc, scale=scale), r=r, n=n)\n", + "np.testing.assert_allclose(Y1.mean(), Y.mean()*scale+loc)\n", + "np.testing.assert_allclose(Y2.mean(), Y.mean()*scale+loc)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-30T15:29:15.037132200Z", + "start_time": "2024-04-30T15:29:15.024454800Z" + } + }, + "id": "691fedff38142822", + "execution_count": 62 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scipy/stats/tests/meson.build b/scipy/stats/tests/meson.build index a94e3c7cd502..3ece8d4c21aa 100644 --- a/scipy/stats/tests/meson.build +++ b/scipy/stats/tests/meson.build @@ -5,6 +5,7 @@ py3.install_sources([ 'test_binned_statistic.py', 'test_censored_data.py', 'test_contingency.py', + 'test_continuous.py', 'test_continuous_basic.py', 'test_continuous_fit_censored.py', 'test_crosstab.py', diff --git a/scipy/stats/tests/test_axis_nan_policy.py b/scipy/stats/tests/test_axis_nan_policy.py index 14a3fba372e0..bf01307a14ae 100644 --- a/scipy/stats/tests/test_axis_nan_policy.py +++ b/scipy/stats/tests/test_axis_nan_policy.py @@ -9,12 +9,16 @@ import re import pickle import pytest +import warnings import numpy as np -from numpy.testing import assert_allclose, assert_equal, suppress_warnings +from numpy.testing import assert_allclose, assert_equal from scipy import stats from scipy.stats import norm # type: ignore[attr-defined] -from scipy.stats._axis_nan_policy import _masked_arrays_2_sentinel_arrays +from scipy.stats._axis_nan_policy import (_masked_arrays_2_sentinel_arrays, + SmallSampleWarning, + too_small_nd_omit, too_small_nd_not_omit, + too_small_1d_omit, too_small_1d_not_omit) from scipy._lib._util import AxisError from scipy.conftest import skip_xp_invalid_arg @@ -72,7 +76,7 @@ def ttest_ci(*args, **kwargs): (stats.differential_entropy, tuple(), dict(), 1, 1, False, lambda x: (x,)), (stats.variation, tuple(), dict(), 1, 1, False, lambda x: (x,)), (stats.friedmanchisquare, tuple(), dict(), 3, 2, True, None), - (stats.brunnermunzel, tuple(), dict(), 2, 2, False, None), + (stats.brunnermunzel, tuple(), dict(distribution='normal'), 2, 2, False, None), (stats.mood, tuple(), {}, 2, 2, False, None), (stats.shapiro, tuple(), {}, 1, 2, False, None), (stats.ks_1samp, (norm().cdf,), dict(), 1, 4, False, @@ -115,8 +119,7 @@ def ttest_ci(*args, **kwargs): # If the message is one of those expected, put nans in # appropriate places of `statistics` and `pvalues` -too_small_messages = {"The input contains nan", # for nan_policy="raise" - "Degrees of freedom <= 0 for slice", +too_small_messages = {"Degrees of freedom <= 0 for slice", "x and y should have at least 5 elements", "Data must be at least length 3", "The sample must contain at least two", @@ -139,7 +142,9 @@ def ttest_ci(*args, **kwargs): "`kurtosistest` requires at least", "attempt to get argmax of an empty sequence", "No array values within given limits", - "Input sample size must be greater than one.",} + "Input sample size must be greater than one.", + "invalid value encountered", + "divide by zero encountered",} # If the message is one of these, results of the function may be inaccurate, # but NaNs are not to be placed @@ -152,6 +157,9 @@ def ttest_ci(*args, **kwargs): # For some functions, empty arrays produce non-NaN results empty_special_case_funcs = {stats.entropy} +# Some functions don't follow the usual "too small" warning rules +too_small_special_case_funcs = {stats.entropy} + def _mixed_data_generator(n_samples, n_repetitions, axis, rng, paired=False): # generate random samples to check the response of hypothesis tests to @@ -239,8 +247,24 @@ def nan_policy_1d(hypotest, data1d, unpacker, *args, n_outputs=2, return unpacker(hypotest(*data1d, *args, _no_deco=_no_deco, **kwds)) -@pytest.mark.filterwarnings('ignore::RuntimeWarning') -@pytest.mark.filterwarnings('ignore::UserWarning') +# These three warnings are intentional +# For `wilcoxon` when the sample size < 50 +@pytest.mark.filterwarnings('ignore:Sample size too small for normal:UserWarning') +# `kurtosistest` and `normaltest` when sample size < 20 +@pytest.mark.filterwarnings('ignore:`kurtosistest` p-value may be:UserWarning') +# `foneway` +@pytest.mark.filterwarnings('ignore:all input arrays have length 1.:RuntimeWarning') + +# The rest of these may or may not be desirable. They need further investigation +# to determine whether the function's decorator should define `too_small. +# `bartlett`, `tvar`, `tstd`, `tsem` +@pytest.mark.filterwarnings('ignore:Degrees of freedom <= 0 for slice:RuntimeWarning') +# kstat, kstatvar, ttest_1samp, ttest_rel, ttest_ind, ttest_ci, brunnermunzel +# mood, levene, fligner, bartlett +@pytest.mark.filterwarnings('ignore:Invalid value encountered in:RuntimeWarning') +# kstatvar, ttest_1samp, ttest_rel, ttest_ci, brunnermunzel, levene, bartlett +@pytest.mark.filterwarnings('ignore:divide by zero encountered:RuntimeWarning') + @pytest.mark.parametrize(("hypotest", "args", "kwds", "n_samples", "n_outputs", "paired", "unpacker"), axis_nan_policy_cases) @pytest.mark.parametrize(("nan_policy"), ("propagate", "omit", "raise")) @@ -259,8 +283,25 @@ def test_axis_nan_policy_fast(hypotest, args, kwds, n_samples, n_outputs, # Takes O(1 min) to run, and even skipping with the `xslow` decorator takes # about 3 sec because this is >3,000 tests. So ensure pytest doesn't see # them at all unless `SCIPY_XSLOW` is defined. - @pytest.mark.filterwarnings('ignore::RuntimeWarning') - @pytest.mark.filterwarnings('ignore::UserWarning') + + # These three warnings are intentional + # For `wilcoxon` when the sample size < 50 + @pytest.mark.filterwarnings('ignore:Sample size too small for normal:UserWarning') + # `kurtosistest` and `normaltest` when sample size < 20 + @pytest.mark.filterwarnings('ignore:`kurtosistest` p-value may be:UserWarning') + # `foneway` + @pytest.mark.filterwarnings('ignore:all input arrays have length 1.:RuntimeWarning') + + # The rest of these may or may not be desirable. They need further investigation + # to determine whether the function's decorator should define `too_small. + # `bartlett`, `tvar`, `tstd`, `tsem` + @pytest.mark.filterwarnings('ignore:Degrees of freedom <= 0 for:RuntimeWarning') + # kstat, kstatvar, ttest_1samp, ttest_rel, ttest_ind, ttest_ci, brunnermunzel + # mood, levene, fligner, bartlett + @pytest.mark.filterwarnings('ignore:Invalid value encountered in:RuntimeWarning') + # kstatvar, ttest_1samp, ttest_rel, ttest_ci, brunnermunzel, levene, bartlett + @pytest.mark.filterwarnings('ignore:divide by zero encountered:RuntimeWarning') + @pytest.mark.parametrize(("hypotest", "args", "kwds", "n_samples", "n_outputs", "paired", "unpacker"), axis_nan_policy_cases) @pytest.mark.parametrize(("nan_policy"), ("propagate", "omit", "raise")) @@ -312,89 +353,89 @@ def unpacker(res): data_b = [np.moveaxis(sample, axis, -1) for sample in data] data_b = [np.broadcast_to(sample, output_shape + [sample.shape[-1]]) for sample in data_b] - statistics = np.zeros(output_shape) - pvalues = np.zeros(output_shape) + res_1d = np.zeros(output_shape + [n_outputs]) - for i, _ in np.ndenumerate(statistics): + for i, _ in np.ndenumerate(np.zeros(output_shape)): data1d = [sample[i] for sample in data_b] - with np.errstate(divide='ignore', invalid='ignore'): - try: - res1d = nan_policy_1d(hypotest, data1d, unpacker, *args, - n_outputs=n_outputs, - nan_policy=nan_policy, - paired=paired, _no_deco=True, **kwds) - - # Eventually we'll check the results of a single, vectorized - # call of `hypotest` against the arrays `statistics` and - # `pvalues` populated using the reference `nan_policy_1d`. - # But while we're at it, check the results of a 1D call to - # `hypotest` against the reference `nan_policy_1d`. - res1db = unpacker(hypotest(*data1d, *args, - nan_policy=nan_policy, **kwds)) - assert_equal(res1db[0], res1d[0]) - if len(res1db) == 2: - assert_equal(res1db[1], res1d[1]) - - # When there is not enough data in 1D samples, many existing - # hypothesis tests raise errors instead of returning nans . - # For vectorized calls, we put nans in the corresponding elements - # of the output. - except (RuntimeWarning, UserWarning, ValueError, - ZeroDivisionError) as e: - - # whatever it is, make sure same error is raised by both - # `nan_policy_1d` and `hypotest` - with pytest.raises(type(e), match=re.escape(str(e))): - nan_policy_1d(hypotest, data1d, unpacker, *args, - n_outputs=n_outputs, nan_policy=nan_policy, - paired=paired, _no_deco=True, **kwds) - with pytest.raises(type(e), match=re.escape(str(e))): - hypotest(*data1d, *args, nan_policy=nan_policy, **kwds) - - if any([str(e).startswith(message) - for message in too_small_messages]): - res1d = np.full(n_outputs, np.nan) - elif any([str(e).startswith(message) - for message in inaccuracy_messages]): - with suppress_warnings() as sup: - sup.filter(RuntimeWarning) - sup.filter(UserWarning) - res1d = nan_policy_1d(hypotest, data1d, unpacker, - *args, n_outputs=n_outputs, - nan_policy=nan_policy, - paired=paired, _no_deco=True, - **kwds) - else: - raise e - statistics[i] = res1d[0] - if len(res1d) == 2: - pvalues[i] = res1d[1] + contains_nan = any([np.isnan(sample).any() for sample in data1d]) + + # Take care of `nan_policy='raise'`. + # Afterward, the 1D part of the test is over + message = "The input contains nan values" + if nan_policy == 'raise' and contains_nan: + with pytest.raises(ValueError, match=message): + nan_policy_1d(hypotest, data1d, unpacker, *args, + n_outputs=n_outputs, + nan_policy=nan_policy, + paired=paired, _no_deco=True, **kwds) + + with pytest.raises(ValueError, match=message): + hypotest(*data1d, *args, nan_policy=nan_policy, **kwds) + + continue + + # Take care of `nan_policy='propagate'` and `nan_policy='omit'` + + # Get results of simple reference implementation + try: + res_1da = nan_policy_1d(hypotest, data1d, unpacker, *args, + n_outputs=n_outputs, + nan_policy=nan_policy, + paired=paired, _no_deco=True, **kwds) + except (ValueError, RuntimeWarning, ZeroDivisionError) as ea: + ea_str = str(ea) + if any([str(ea_str).startswith(msg) for msg in too_small_messages]): + res_1da = np.full(n_outputs, np.nan) + else: + raise + + # Get results of public function with 1D slices + # Should warn for all slices + if (nan_policy == 'omit' and data_generator == "all_nans" + and hypotest not in too_small_special_case_funcs): + with pytest.warns(SmallSampleWarning, match=too_small_1d_omit): + res = hypotest(*data1d, *args, nan_policy=nan_policy, **kwds) + # warning depends on slice + elif (nan_policy == 'omit' and data_generator == "mixed" + and hypotest not in too_small_special_case_funcs): + with np.testing.suppress_warnings() as sup: + sup.filter(SmallSampleWarning, too_small_1d_omit) + res = hypotest(*data1d, *args, nan_policy=nan_policy, **kwds) + # shouldn't complain if there are no NaNs + else: + res = hypotest(*data1d, *args, nan_policy=nan_policy, **kwds) + res_1db = unpacker(res) + + assert_equal(res_1db, res_1da) + res_1d[i] = res_1db + + res_1d = np.moveaxis(res_1d, -1, 0) # Perform a vectorized call to the hypothesis test. + # If `nan_policy == 'raise'`, check that it raises the appropriate error. - # If not, compare against the output against `statistics` and `pvalues` + # Test is done, so return if nan_policy == 'raise' and not data_generator == "all_finite": message = 'The input contains nan values' with pytest.raises(ValueError, match=message): hypotest(*data, axis=axis, nan_policy=nan_policy, *args, **kwds) + return + + # If `nan_policy == 'omit', we might be left with a small sample. + # Check for the appropriate warning. + if (nan_policy == 'omit' and data_generator in {"all_nans", "mixed"} + and hypotest not in too_small_special_case_funcs): + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + res = hypotest(*data, axis=axis, nan_policy=nan_policy, *args, **kwds) + else: # otherwise, there should be no warning + res = hypotest(*data, axis=axis, nan_policy=nan_policy, *args, **kwds) + + # Compare against the output against looping over 1D slices + res_nd = unpacker(res) + + assert_allclose(res_nd, res_1d, rtol=1e-14) + - else: - with suppress_warnings() as sup, \ - np.errstate(divide='ignore', invalid='ignore'): - sup.filter(RuntimeWarning, "Precision loss occurred in moment") - sup.filter(UserWarning, "Sample size too small for normal " - "approximation.") - res = unpacker(hypotest(*data, axis=axis, nan_policy=nan_policy, - *args, **kwds)) - assert_allclose(res[0], statistics, rtol=1e-14) - assert_equal(res[0].dtype, statistics.dtype) - - if len(res) == 2: - assert_allclose(res[1], pvalues, rtol=1e-14) - assert_equal(res[1].dtype, pvalues.dtype) - - -@pytest.mark.filterwarnings('ignore::RuntimeWarning') @pytest.mark.parametrize(("hypotest", "args", "kwds", "n_samples", "n_outputs", "paired", "unpacker"), axis_nan_policy_cases) @pytest.mark.parametrize(("nan_policy"), ("propagate", "omit", "raise")) @@ -404,7 +445,6 @@ def test_axis_nan_policy_axis_is_None(hypotest, args, kwds, n_samples, n_outputs, paired, unpacker, nan_policy, data_generator): # check for correct behavior when `axis=None` - if not unpacker: def unpacker(res): return res @@ -436,45 +476,68 @@ def unpacker(res): hypotest(*data_raveled, axis=None, nan_policy=nan_policy, *args, **kwds) - else: - # behavior of reference implementation with 1d input, hypotest with 1d - # input, and hypotest with Nd input should match, whether that means - # that outputs are equal or they raise the same exception + return - ea_str, eb_str, ec_str = None, None, None - with np.errstate(divide='ignore', invalid='ignore'): - try: - res1da = nan_policy_1d(hypotest, data_raveled, unpacker, *args, - n_outputs=n_outputs, - nan_policy=nan_policy, paired=paired, - _no_deco=True, **kwds) - except (RuntimeWarning, ValueError, ZeroDivisionError) as ea: - ea_str = str(ea) + # behavior of reference implementation with 1d input, public function with 1d + # input, and public function with Nd input and `axis=None` should be consistent. + # This means: + # - If the reference version raises an error or emits a warning, it's because + # the sample is too small, so check that the public function emits an + # appropriate "too small" warning + # - Any results returned by the three versions should be the same. + with warnings.catch_warnings(): # treat warnings as errors + warnings.simplefilter("error") - try: - res1db = unpacker(hypotest(*data_raveled, *args, - nan_policy=nan_policy, **kwds)) - except (RuntimeWarning, ValueError, ZeroDivisionError) as eb: - eb_str = str(eb) + ea_str, eb_str, ec_str = None, None, None + try: + res1da = nan_policy_1d(hypotest, data_raveled, unpacker, *args, + n_outputs=n_outputs, nan_policy=nan_policy, + paired=paired, _no_deco=True, **kwds) + except (RuntimeWarning, ValueError, ZeroDivisionError) as ea: + res1da = None + ea_str = str(ea) + + try: + res1db = hypotest(*data_raveled, *args, nan_policy=nan_policy, **kwds) + except SmallSampleWarning as eb: + eb_str = str(eb) + + try: + res1dc = hypotest(*data, *args, axis=None, nan_policy=nan_policy, **kwds) + except SmallSampleWarning as ec: + ec_str = str(ec) + + if ea_str or eb_str or ec_str: # *if* there is some sort of error or warning + # If the reference implemented generated an error or warning, make sure the + # message was one of the expected "too small" messages. Note that some + # functions don't complain at all without the decorator; that's OK, too. + ok_msg = any([str(ea_str).startswith(msg) for msg in too_small_messages]) + assert (ea_str is None) or ok_msg + + # make sure the wrapped function emits the *intended* warning + desired_warnings = {too_small_1d_omit, too_small_1d_not_omit} + assert str(eb_str) in desired_warnings + assert str(ec_str) in desired_warnings + + with warnings.catch_warnings(): # ignore warnings to get return value + warnings.simplefilter("ignore") + res1db = hypotest(*data_raveled, *args, nan_policy=nan_policy, **kwds) + res1dc = hypotest(*data, *args, axis=None, nan_policy=nan_policy, **kwds) + + # Make sure any results returned by reference/public function are identical + # and all attributes are *NumPy* scalars + res1db, res1dc = unpacker(res1db), unpacker(res1dc) + assert_equal(res1dc, res1db) + all_results = list(res1db) + list(res1dc) + + if res1da is not None: + assert_equal(res1db, res1da) + all_results += list(res1da) + + for item in all_results: + assert np.issubdtype(item.dtype, np.number) + assert np.isscalar(item) - try: - res1dc = unpacker(hypotest(*data, *args, axis=None, - nan_policy=nan_policy, **kwds)) - except (RuntimeWarning, ValueError, ZeroDivisionError) as ec: - ec_str = str(ec) - - if ea_str or eb_str or ec_str: - assert any([str(ea_str).startswith(message) - for message in too_small_messages]) - assert ea_str == eb_str == ec_str - else: - assert_equal(res1db, res1da) - assert_equal(res1dc, res1da) - for item in list(res1da) + list(res1db) + list(res1dc): - # Most functions naturally return NumPy numbers, which - # are drop-in replacements for the Python versions but with - # desirable attributes. Make sure this is consistent. - assert np.issubdtype(item.dtype, np.number) # Test keepdims for: # - single-output and multi-output functions (gmean and mannwhitneyu) @@ -482,11 +545,17 @@ def unpacker(res): # - 1D with no NaNs # - 1D with NaN propagation # - Zero-sized output +@pytest.mark.filterwarnings('ignore:All axis-slices of one...') +@pytest.mark.filterwarnings('ignore:After omitting NaNs...') +# These were added in gh-20734 for `ttest_1samp`; they should be addressed and removed +@pytest.mark.filterwarnings('ignore:divide by zero encountered...') +@pytest.mark.filterwarnings('ignore:invalid value encountered...') @pytest.mark.parametrize("nan_policy", ("omit", "propagate")) @pytest.mark.parametrize( ("hypotest", "args", "kwds", "n_samples", "unpacker"), ((stats.gmean, tuple(), dict(), 1, lambda x: (x,)), - (stats.mannwhitneyu, tuple(), {'method': 'asymptotic'}, 2, None)) + (stats.mannwhitneyu, tuple(), {'method': 'asymptotic'}, 2, None), + (stats.ttest_1samp, (0,), dict(), 1, unpack_ttest_result)) ) @pytest.mark.parametrize( ("sample_shape", "axis_cases"), @@ -713,7 +782,7 @@ def small_sample_generator(n_dims): gens = [small_sample_generator(n_dims) for i in range(n_samples)] yield from product(*gens) - n_dims = [2, 3] + n_dims = [1, 2, 3] for samples in small_data_generator(n_samples, n_dims): # this test is only for arrays of zero size @@ -737,13 +806,21 @@ def small_sample_generator(n_dims): if hypotest in empty_special_case_funcs: empty_val = hypotest(*([[]]*len(samples)), *args, **kwds) + expected = np.asarray(expected) mask = np.isnan(expected) expected[mask] = empty_val + expected = expected[()] - with np.testing.suppress_warnings() as sup: - # generated by f_oneway for too_small inputs - sup.filter(stats.DegenerateDataWarning) - res = hypotest(*samples, *args, axis=axis, **kwds) + if expected.size and hypotest not in too_small_special_case_funcs: + message = (too_small_1d_not_omit if max_axis == 1 + else too_small_nd_not_omit) + with pytest.warns(SmallSampleWarning, match=message): + res = hypotest(*samples, *args, axis=axis, **kwds) + else: + with np.testing.suppress_warnings() as sup: + # f_oneway special case + sup.filter(SmallSampleWarning, "all input arrays have length 1") + res = hypotest(*samples, *args, axis=axis, **kwds) res = unpacker(res) for i in range(n_outputs): @@ -891,6 +968,8 @@ def test_masked_stat_1d(): np.testing.assert_array_equal(res6, res) +@pytest.mark.filterwarnings('ignore:After omitting NaNs...') +@pytest.mark.filterwarnings('ignore:One or more axis-slices of one...') @skip_xp_invalid_arg @pytest.mark.parametrize(("axis"), range(-3, 3)) def test_masked_stat_3d(axis): @@ -915,6 +994,8 @@ def test_masked_stat_3d(axis): np.testing.assert_array_equal(res, res2) +@pytest.mark.filterwarnings('ignore:After omitting NaNs...') +@pytest.mark.filterwarnings('ignore:One or more axis-slices of one...') @skip_xp_invalid_arg def test_mixed_mask_nan_1(): # targeted test of _axis_nan_policy_factory with 2D masked sample: @@ -963,6 +1044,8 @@ def test_mixed_mask_nan_1(): np.testing.assert_array_equal(res4, res) +@pytest.mark.filterwarnings('ignore:After omitting NaNs...') +@pytest.mark.filterwarnings('ignore:One or more axis-slices of one...') @skip_xp_invalid_arg def test_mixed_mask_nan_2(): # targeted test of _axis_nan_policy_factory with 2D masked sample: @@ -1082,6 +1165,8 @@ def test_other_axis_tuples(axis): np.testing.assert_array_equal(res, res2) +@pytest.mark.filterwarnings('ignore:After omitting NaNs...') +@pytest.mark.filterwarnings('ignore:One or more axis-slices of one...') @skip_xp_invalid_arg @pytest.mark.parametrize( ("weighted_fun_name, unpacker"), diff --git a/scipy/stats/tests/test_continuous.py b/scipy/stats/tests/test_continuous.py new file mode 100644 index 000000000000..12f6cff46805 --- /dev/null +++ b/scipy/stats/tests/test_continuous.py @@ -0,0 +1,1054 @@ +import functools +import pickle +from copy import deepcopy + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_equal +from hypothesis import strategies, given, reproduce_failure # noqa: F401 +import hypothesis.extra.numpy as npst + +from scipy import stats +from scipy.stats._fit import _kolmogorov_smirnov +from scipy.stats._ksstats import kolmogn + +from scipy.stats._distribution_infrastructure import ( + oo, _Domain, _RealDomain, _Parameter, _Parameterization, _RealParameter, + ContinuousDistribution, ShiftedScaledDistribution, _fiinfo, + _generate_domain_support) +from scipy.stats._new_distributions import LogUniform, StandardNormal, Normal + +class Test_RealDomain: + rng = np.random.default_rng(349849812549824) + + def test_iv(self): + domain = _RealDomain(endpoints=('a', 'b')) + message = "The endpoints of the distribution are defined..." + with pytest.raises(TypeError, match=message): + domain.get_numerical_endpoints(dict) + + + @pytest.mark.parametrize('x', [rng.uniform(10, 10, size=(2, 3, 4)), + -np.inf, np.pi]) + def test_contains_simple(self, x): + # Test `contains` when endpoints are defined by constants + a, b = -np.inf, np.pi + domain = _RealDomain(endpoints=(a, b), inclusive=(False, True)) + assert_equal(domain.contains(x), (a < x) & (x <= b)) + + @given(shapes=npst.mutually_broadcastable_shapes(num_shapes=3, min_side=0), + inclusive_a=strategies.booleans(), + inclusive_b=strategies.booleans(), + data=strategies.data()) + def test_contains(self, shapes, inclusive_a, inclusive_b, data): + # Test `contains` when endpoints are defined by parameters + input_shapes, result_shape = shapes + shape_a, shape_b, shape_x = input_shapes + + # Without defining min and max values, I spent forever trying to set + # up a valid test without overflows or similar just drawing arrays. + a_elements = dict(allow_nan=False, allow_infinity=False, + min_value=-1e3, max_value=1) + b_elements = dict(allow_nan=False, allow_infinity=False, + min_value=2, max_value=1e3) + a = data.draw(npst.arrays(npst.floating_dtypes(), + shape_a, elements=a_elements)) + b = data.draw(npst.arrays(npst.floating_dtypes(), + shape_b, elements=b_elements)) + # ensure some points are to the left, some to the right, and some + # are exactly on the boundary + d = b - a + x = np.concatenate([np.linspace(a-d, a, 10), + np.linspace(a, b, 10), + np.linspace(b, b+d, 10)]) + # Domain is defined by two parameters, 'a' and 'b' + domain = _RealDomain(endpoints=('a', 'b'), + inclusive=(inclusive_a, inclusive_b)) + domain.define_parameters(_RealParameter('a', domain=_RealDomain()), + _RealParameter('b', domain=_RealDomain())) + # Check that domain and string evaluation give the same result + res = domain.contains(x, dict(a=a, b=b)) + + # Apparently, `np.float16([2]) < np.float32(2.0009766)` is False + # but `np.float16([2]) < np.float32([2.0009766])` is True + # dtype = np.result_type(a.dtype, b.dtype, x.dtype) + # a, b, x = a.astype(dtype), b.astype(dtype), x.astype(dtype) + # unclear whether we should be careful about this, since it will be + # fixed with NEP50. Just do what makes the test pass. + left_comparison = '<=' if inclusive_a else '<' + right_comparison = '<=' if inclusive_b else '<' + ref = eval(f'(a {left_comparison} x) & (x {right_comparison} b)') + assert_equal(res, ref) + + @pytest.mark.parametrize('case', [ + (-np.inf, np.pi, False, True, "(-∞, π]"), + ('a', 5, True, False, "[a, 5)") + ]) + def test_str(self, case): + domain = _RealDomain(endpoints=case[:2], inclusive=case[2:4]) + assert str(domain) == case[4] + + @given(a=strategies.one_of(strategies.decimals(allow_nan=False), + strategies.characters(whitelist_categories="L"), + strategies.sampled_from(list(_Domain.symbols))), + b=strategies.one_of(strategies.decimals(allow_nan=False), + strategies.characters(whitelist_categories="L"), + strategies.sampled_from(list(_Domain.symbols))), + inclusive_a=strategies.booleans(), + inclusive_b=strategies.booleans(), + ) + def test_str2(self, a, b, inclusive_a, inclusive_b): + # I wrote this independently from the implementation of __str__, but + # I imagine it looks pretty similar to __str__. + a = _Domain.symbols.get(a, a) + b = _Domain.symbols.get(b, b) + left_bracket = '[' if inclusive_a else '(' + right_bracket = ']' if inclusive_b else ')' + domain = _RealDomain(endpoints=(a, b), + inclusive=(inclusive_a, inclusive_b)) + ref = f"{left_bracket}{a}, {b}{right_bracket}" + assert str(domain) == ref + +def draw_distribution_from_family(family, data, rng, proportions, min_side=0): + # If the distribution has parameters, choose a parameterization and + # draw broadcastable shapes for the parameter arrays. + n_parameterizations = family._num_parameterizations() + if n_parameterizations > 0: + i = data.draw(strategies.integers(0, max_value=n_parameterizations-1)) + n_parameters = family._num_parameters(i) + shapes, result_shape = data.draw( + npst.mutually_broadcastable_shapes(num_shapes=n_parameters, + min_side=min_side)) + dist = family._draw(shapes, rng=rng, proportions=proportions, + i_parameterization=i) + else: + dist = family._draw(rng=rng) + result_shape = tuple() + + # Draw a broadcastable shape for the arguments, and draw values for the + # arguments. + x_shape = data.draw(npst.broadcastable_shapes(result_shape, + min_side=min_side)) + x = dist._variable.draw(x_shape, parameter_values=dist._parameters, + proportions=proportions, rng=rng) + x_result_shape = np.broadcast_shapes(x_shape, result_shape) + y_shape = data.draw(npst.broadcastable_shapes(x_result_shape, + min_side=min_side)) + y = dist._variable.draw(y_shape, parameter_values=dist._parameters, + proportions=proportions, rng=rng) + xy_result_shape = np.broadcast_shapes(y_shape, x_result_shape) + p_domain = _RealDomain((0, 1), (True, True)) + p = p_domain.draw(x_shape, proportions=proportions, rng=rng) + logp = np.log(p) + + return dist, x, y, p, logp, result_shape, x_result_shape, xy_result_shape + + +class TestDistributions: + @pytest.mark.filterwarnings("ignore") + # @pytest.mark.parametrize('family', (LogUniform,)) + # @pytest.mark.parametrize('family', (StandardNormal,)) + # @pytest.mark.parametrize('family', (Normal,)) + @pytest.mark.parametrize('family', (StandardNormal, LogUniform, Normal)) + @given(data=strategies.data(), seed=strategies.integers(min_value=0)) + def test_basic(self, family, data, seed): + rng = np.random.default_rng(seed) + + # relative proportions of valid, endpoint, out of bounds, and NaN params + proportions = (1, 1, 1, 1) + tmp = draw_distribution_from_family(family, data, rng, proportions) + dist, x, y, p, logp, result_shape, x_result_shape, xy_result_shape = tmp + sample_shape = data.draw(npst.array_shapes(min_dims=0, min_side=0, + max_side=20)) + + check_support(dist) + + methods = {'log/exp', 'quadrature'} + check_dist_func(dist, 'entropy', None, result_shape, methods) + check_dist_func(dist, 'logentropy', None, result_shape, methods) + + methods = {'icdf'} + check_dist_func(dist, 'median', None, result_shape, methods) + + methods = {'optimization'} + check_dist_func(dist, 'mode', None, result_shape, methods) + + methods = {'cache'} # weak test right now + check_dist_func(dist, 'mean', None, result_shape, methods) + check_dist_func(dist, 'variance', None, result_shape, methods) + check_dist_func(dist, 'skewness', None, result_shape, methods) + check_dist_func(dist, 'kurtosis', None, result_shape, methods) + assert_allclose(dist.standard_deviation()**2, dist.variance()) + + check_moment_funcs(dist, result_shape) + check_sample_shape_NaNs(dist, 'sample', sample_shape, result_shape) + check_sample_shape_NaNs(dist, 'qmc_sample', sample_shape, result_shape) + + methods = {'log/exp'} + check_dist_func(dist, 'pdf', x, x_result_shape, methods) + check_dist_func(dist, 'logpdf', x, x_result_shape, methods) + + methods = {'log/exp', 'complement', 'quadrature'} + check_dist_func(dist, 'logcdf', x, x_result_shape, methods) + check_dist_func(dist, 'cdf', x, x_result_shape, methods) + check_dist_func(dist, 'logccdf', x, x_result_shape, methods) + check_dist_func(dist, 'ccdf', x, x_result_shape, methods) + + if not isinstance(dist, ShiftedScaledDistribution): + methods = {'quadrature'} + check_cdf2(dist, False, x, y, xy_result_shape, methods) + check_cdf2(dist, True, x, y, xy_result_shape, methods) + methods = {'addition'} + check_ccdf2(dist, False, x, y, xy_result_shape, methods) + check_ccdf2(dist, True, x, y, xy_result_shape, methods) + + methods = {'complement', 'inversion'} + check_dist_func(dist, 'ilogcdf', logp, x_result_shape, methods) + check_dist_func(dist, 'icdf', p, x_result_shape, methods) + check_dist_func(dist, 'ilogccdf', logp, x_result_shape, methods) + check_dist_func(dist, 'iccdf', p, x_result_shape, methods) + + def test_plot(self): + try: + import matplotlib.pyplot as plt + except ImportError: + return + + X = stats.Uniform(a=0., b=1.) + ax = X.plot() + assert ax == plt.gca() + + +def check_sample_shape_NaNs(dist, fname, sample_shape, result_shape): + full_shape = sample_shape + result_shape + if fname == 'sample': + sample_method = dist.sample + elif fname == 'qmc_sample': + sample_method = functools.partial(dist.sample, + qmc_engine=stats.qmc.Halton) + methods = {'inverse_transform'} + if dist._overrides(f'_{fname}_formula'): + methods.add('formula') + + for method in methods: + res = sample_method(sample_shape, method=method) + valid_parameters = np.broadcast_to(get_valid_parameters(dist), + res.shape) + assert_equal(res.shape, full_shape) + np.testing.assert_equal(res.dtype, dist._dtype) + + if full_shape == (): + # NumPy random makes a distinction between a 0d array and a scalar. + # In stats, we consistently turn 0d arrays into scalars, so + # maintain that behavior here. (With Array API arrays, this will + # change.) + assert np.isscalar(res) + assert np.all(np.isfinite(res[valid_parameters])) + assert_equal(res[~valid_parameters], np.nan) + + sample1 = sample_method(sample_shape, method=method, + rng=np.random.default_rng(42)) + sample2 = sample_method(sample_shape, method=method, + rng=np.random.default_rng(42)) + assert not np.any(np.equal(res, sample1)) + assert_equal(sample1, sample2) + + +def check_support(dist): + a, b = dist.support() + check_nans_and_edges(dist, 'support', None, a) + check_nans_and_edges(dist, 'support', None, b) + assert a.shape == dist._shape + assert b.shape == dist._shape + assert a.dtype == dist._dtype + assert b.dtype == dist._dtype + + +def check_dist_func(dist, fname, arg, result_shape, methods): + # Check that all computation methods of all distribution functions agree + # with one another, effectively testing the correctness of the generic + # computation methods and confirming the consistency of specific + # distributions with their pdf/logpdf. + + args = tuple() if arg is None else (arg,) + methods = methods.copy() + + if "cache" in methods: + # If "cache" is specified before the value has been evaluated, it + # raises an error. After the value is evaluated, it will succeed. + with pytest.raises(NotImplementedError): + getattr(dist, fname)(*args, method="cache") + + ref = getattr(dist, fname)(*args) + check_nans_and_edges(dist, fname, arg, ref) + + # Remove this after fixing `draw` + tol_override = {'atol': 1e-15} + # Mean can be 0, which makes logmean -oo. + if fname in {'logmean', 'mean', 'logskewness', 'skewness'}: + tol_override = {'atol': 1e-15} + elif fname in {'mode'}: + # can only expect about half of machine precision for optimization + # because math + tol_override = {'atol': 1e-6} + + if dist._overrides(f'_{fname}_formula'): + methods.add('formula') + + np.testing.assert_equal(ref.shape, result_shape) + # Until we convert to array API, let's do the familiar thing: + # 0d things are scalars, not arrays + if result_shape == tuple(): + assert np.isscalar(ref) + + for method in methods: + res = getattr(dist, fname)(*args, method=method) + if 'log' in fname: + np.testing.assert_allclose(np.exp(res), np.exp(ref), + **tol_override) + else: + np.testing.assert_allclose(res, ref, **tol_override) + + # for now, make sure dtypes are consistent; later, we can check whether + # they are correct. + np.testing.assert_equal(res.dtype, ref.dtype) + np.testing.assert_equal(res.shape, result_shape) + if result_shape == tuple(): + assert np.isscalar(res) + +def check_cdf2(dist, log, x, y, result_shape, methods): + # Specialized test for 2-arg cdf since the interface is a bit different + # from the other methods. Here, we'll use 1-arg cdf as a reference, and + # since we have already checked 1-arg cdf in `check_nans_and_edges`, this + # checks the equivalent of both `check_dist_func` and + # `check_nans_and_edges`. + methods = methods.copy() + + if log: + if dist._overrides('_logcdf2_formula'): + methods.add('formula') + if dist._overrides('_logcdf_formula') or dist._overrides('_logccdf_formula'): + methods.add('subtraction') + if (dist._overrides('_cdf_formula') + or dist._overrides('_ccdf_formula')): + methods.add('log/exp') + else: + if dist._overrides('_cdf2_formula'): + methods.add('formula') + if dist._overrides('_cdf_formula') or dist._overrides('_ccdf_formula'): + methods.add('subtraction') + if (dist._overrides('_logcdf_formula') + or dist._overrides('_logccdf_formula')): + methods.add('log/exp') + + ref = dist.cdf(y) - dist.cdf(x) + np.testing.assert_equal(ref.shape, result_shape) + + if result_shape == tuple(): + assert np.isscalar(ref) + + for method in methods: + res = (np.exp(dist.logcdf(x, y, method=method)) if log + else dist.cdf(x, y, method=method)) + np.testing.assert_allclose(res, ref, atol=1e-14) + if log and np.any(x > y) and ref.size: + np.testing.assert_equal(res.dtype, (ref + 0j).dtype) + else: + np.testing.assert_equal(res.dtype, ref.dtype) + np.testing.assert_equal(res.shape, result_shape) + if result_shape == tuple(): + assert np.isscalar(res) + + +def check_ccdf2(dist, log, x, y, result_shape, methods): + # Specialized test for 2-arg ccdf since the interface is a bit different + # from the other methods. Could be combined with check_cdf2 above, but + # writing it separately is simpler. + methods = methods.copy() + + if dist._overrides(f'_{"log" if log else ""}ccdf2_formula'): + methods.add('formula') + + ref = dist.cdf(x) + dist.ccdf(y) + np.testing.assert_equal(ref.shape, result_shape) + + if result_shape == tuple(): + assert np.isscalar(ref) + + for method in methods: + res = (np.exp(dist.logccdf(x, y, method=method)) if log + else dist.ccdf(x, y, method=method)) + np.testing.assert_allclose(res, ref, atol=1e-14) + np.testing.assert_equal(res.dtype, ref.dtype) + np.testing.assert_equal(res.shape, result_shape) + if result_shape == tuple(): + assert np.isscalar(res) + + +def check_nans_and_edges(dist, fname, arg, res): + + valid_parameters = get_valid_parameters(dist) + if fname in {'icdf', 'iccdf'}: + arg_domain = _RealDomain(endpoints=(0, 1), inclusive=(True, True)) + elif fname in {'ilogcdf', 'ilogccdf'}: + arg_domain = _RealDomain(endpoints=(-oo, 0), inclusive=(True, True)) + else: + arg_domain = dist._variable.domain + + classified_args = classify_arg(dist, arg, arg_domain) + valid_parameters, *classified_args = np.broadcast_arrays(valid_parameters, + *classified_args) + valid_arg, endpoint_arg, outside_arg, nan_arg = classified_args + all_valid = valid_arg & valid_parameters + + # Check NaN pattern and edge cases + assert_equal(res[~valid_parameters], np.nan) + assert_equal(res[nan_arg], np.nan) + + a, b = dist.support() + a = np.broadcast_to(a, res.shape) + b = np.broadcast_to(b, res.shape) + + # Writing this independently of how the are set in the distribution + # infrastructure. That is very compact; this is very verbose. + if fname in {'logpdf'}: + assert_equal(res[outside_arg == -1], -np.inf) + assert_equal(res[outside_arg == 1], -np.inf) + assert_equal(res[(endpoint_arg == -1) & ~valid_arg], -np.inf) + assert_equal(res[(endpoint_arg == 1) & ~valid_arg], -np.inf) + elif fname in {'pdf'}: + assert_equal(res[outside_arg == -1], 0) + assert_equal(res[outside_arg == 1], 0) + assert_equal(res[(endpoint_arg == -1) & ~valid_arg], 0) + assert_equal(res[(endpoint_arg == 1) & ~valid_arg], 0) + elif fname in {'logcdf'}: + assert_equal(res[outside_arg == -1], -oo) + assert_equal(res[outside_arg == 1], 0) + assert_equal(res[endpoint_arg == -1], -oo) + assert_equal(res[endpoint_arg == 1], 0) + elif fname in {'cdf'}: + assert_equal(res[outside_arg == -1], 0) + assert_equal(res[outside_arg == 1], 1) + assert_equal(res[endpoint_arg == -1], 0) + assert_equal(res[endpoint_arg == 1], 1) + elif fname in {'logccdf'}: + assert_equal(res[outside_arg == -1], 0) + assert_equal(res[outside_arg == 1], -oo) + assert_equal(res[endpoint_arg == -1], 0) + assert_equal(res[endpoint_arg == 1], -oo) + elif fname in {'ccdf'}: + assert_equal(res[outside_arg == -1], 1) + assert_equal(res[outside_arg == 1], 0) + assert_equal(res[endpoint_arg == -1], 1) + assert_equal(res[endpoint_arg == 1], 0) + elif fname in {'ilogcdf', 'icdf'}: + assert_equal(res[outside_arg == -1], np.nan) + assert_equal(res[outside_arg == 1], np.nan) + assert_equal(res[endpoint_arg == -1], a[endpoint_arg == -1]) + assert_equal(res[endpoint_arg == 1], b[endpoint_arg == 1]) + elif fname in {'ilogccdf', 'iccdf'}: + assert_equal(res[outside_arg == -1], np.nan) + assert_equal(res[outside_arg == 1], np.nan) + assert_equal(res[endpoint_arg == -1], b[endpoint_arg == -1]) + assert_equal(res[endpoint_arg == 1], a[endpoint_arg == 1]) + + if fname not in {'logmean', 'mean', 'logskewness', 'skewness', 'support'}: + assert np.isfinite(res[all_valid & (endpoint_arg == 0)]).all() + +def check_moment_funcs(dist, result_shape): + # Check that all computation methods of all distribution functions agree + # with one another, effectively testing the correctness of the generic + # computation methods and confirming the consistency of specific + # distributions with their pdf/logpdf. + + atol = 1e-9 # make this tighter (e.g. 1e-13) after fixing `draw` + + def check(order, kind, method=None, ref=None, success=True): + if success: + res = dist.moment(order, kind, method=method) + assert_allclose(res, ref, atol=atol*10**order) + assert res.shape == ref.shape + else: + with pytest.raises(NotImplementedError): + dist.moment(order, kind, method=method) + + def has_formula(order, kind): + formula_name = f'_moment_{kind}_formula' + overrides = dist._overrides(formula_name) + if not overrides: + return False + formula = getattr(dist, formula_name) + orders = getattr(formula, 'orders', set(range(6))) + return order in orders + + + dist.reset_cache() + + ### Check Raw Moments ### + for i in range(6): + check(i, 'raw', 'cache', success=False) # not cached yet + ref = dist.moment(i, 'raw', method='quadrature') + check_nans_and_edges(dist, 'moment', None, ref) + assert ref.shape == result_shape + check(i, 'raw','cache', ref, success=True) # cached now + check(i, 'raw', 'formula', ref, success=has_formula(i, 'raw')) + check(i, 'raw', 'general', ref, i == 0) + + # Clearing caches to better check their behavior + dist.reset_cache() + + # If we have central or standard moment formulas, or if there are + # values in their cache, we can use method='transform' + dist.moment(0, 'central') # build up the cache + dist.moment(1, 'central') + for i in range(2, 6): + ref = dist.moment(i, 'raw', method='quadrature') + check(i, 'raw', 'transform', ref, + success=has_formula(i, 'central') or has_formula(i, 'standardized')) + dist.moment(i, 'central') # build up the cache + check(i, 'raw', 'transform', ref) + + dist.reset_cache() + + ### Check Central Moments ### + + for i in range(6): + check(i, 'central', 'cache', success=False) + ref = dist.moment(i, 'central', method='quadrature') + assert ref.shape == result_shape + check(i, 'central', 'cache', ref, success=True) + check(i, 'central', 'formula', ref, success=has_formula(i, 'central')) + check(i, 'central', 'general', ref, success=i <= 1) + check(i, 'central', 'transform', ref, + success=has_formula(i, 'raw') or (i <= 1)) + if not has_formula(i, 'raw'): + dist.moment(i, 'raw') + check(i, 'central', 'transform', ref) + + dist.reset_cache() + + # If we have standard moment formulas, or if there are + # values in their cache, we can use method='normalize' + dist.moment(0, 'standardized') # build up the cache + dist.moment(1, 'standardized') + dist.moment(2, 'standardized') + for i in range(3, 6): + ref = dist.moment(i, 'central', method='quadrature') + check(i, 'central', 'normalize', ref, + success=has_formula(i, 'standardized')) + dist.moment(i, 'standardized') # build up the cache + check(i, 'central', 'normalize', ref) + + ### Check Standard Moments ### + + var = dist.moment(2, 'central', method='quadrature') + dist.reset_cache() + + for i in range(6): + check(i, 'standardized', 'cache', success=False) + ref = dist.moment(i, 'central', method='quadrature') / var ** (i / 2) + assert ref.shape == result_shape + check(i, 'standardized', 'formula', ref, + success=has_formula(i, 'standardized')) + check(i, 'standardized', 'general', ref, success=i <= 2) + check(i, 'standardized', 'normalize', ref) + + if isinstance(dist, ShiftedScaledDistribution): + # logmoment is not fully fleshed out; no need to test + # ShiftedScaledDistribution here + return + + ### Check Against _logmoment ### + logmean = dist._logmoment(1, logcenter=-np.inf) + for i in range(6): + ref = np.exp(dist._logmoment(i, logcenter=-np.inf)) + assert_allclose(dist.moment(i, 'raw'), ref, atol=atol*10**i) + + ref = np.exp(dist._logmoment(i, logcenter=logmean)) + assert_allclose(dist.moment(i, 'central'), ref, atol=atol*10**i) + + ref = np.exp(dist._logmoment(i, logcenter=logmean, standardized=True)) + assert_allclose(dist.moment(i, 'standardized'), ref, atol=atol*10**i) + + +@pytest.mark.parametrize('family', (LogUniform, StandardNormal)) +@pytest.mark.parametrize('x_shape', [tuple(), (2, 3)]) +@pytest.mark.parametrize('dist_shape', [tuple(), (4, 1)]) +@pytest.mark.parametrize('fname', ['qmc_sample', 'sample']) +def test_sample_against_cdf(family, dist_shape, x_shape, fname): + rng = np.random.default_rng(842582438235635) + num_parameters = family._num_parameters() + + if dist_shape and num_parameters == 0: + pytest.skip("Distribution can't have a shape without parameters.") + + dist = family._draw(dist_shape, rng) + + n = 1000 + sample_size = (n,) + x_shape + sample_array_shape = sample_size + dist_shape + + if fname == 'sample': + sample_method = dist.sample + elif fname == 'qmc_sample': + sample_method = functools.partial(dist.sample, + qmc_engine=stats.qmc.Halton) + x = sample_method(sample_size, rng=rng) + assert x.shape == sample_array_shape + + # probably should give `axis` argument to ks_1samp, review that separately + statistic = _kolmogorov_smirnov(dist, x, axis=0) + pvalue = kolmogn(x.shape[0], statistic, cdf=False) + p_threshold = 0.01 + num_pvalues = pvalue.size + num_small_pvalues = np.sum(pvalue < p_threshold) + assert num_small_pvalues < p_threshold * num_pvalues + + +def get_valid_parameters(dist): + # Given a distribution, return a logical array that is true where all + # distribution parameters are within their respective domains. The code + # here is probably quite similar to that used to form the `_invalid` + # attribute of the distribution, but this was written about a week later + # without referring to that code, so it is a somewhat independent check. + + # Get all parameter values and `_Parameter` objects + parameter_values = dist._parameters + parameters = {} + for parameterization in dist._parameterizations: + parameters.update(parameterization.parameters) + + all_valid = np.ones(dist._shape, dtype=bool) + for name, value in parameter_values.items(): + if name not in parameters: # cached value not part of parameterization + continue + parameter = parameters[name] + + # Check that the numerical endpoints and inclusivity attribute + # agree with the `contains` method about which parameter values are + # within the domain. + a, b = parameter.domain.get_numerical_endpoints( + parameter_values=parameter_values) + a_included, b_included = parameter.domain.inclusive + valid = (a <= value) if a_included else a < value + valid &= (value <= b) if b_included else value < b + assert_equal(valid, parameter.domain.contains( + value, parameter_values=parameter_values)) + + # Form `all_valid` mask that is True where *all* parameters are valid + all_valid &= valid + + # Check that the `all_valid` mask formed here is the complement of the + # `dist._invalid` mask stored by the infrastructure + assert_equal(~all_valid, dist._invalid) + + return all_valid + +def classify_arg(dist, arg, arg_domain): + if arg is None: + valid_args = np.ones(dist._shape, dtype=bool) + endpoint_args = np.zeros(dist._shape, dtype=bool) + outside_args = np.zeros(dist._shape, dtype=bool) + nan_args = np.zeros(dist._shape, dtype=bool) + return valid_args, endpoint_args, outside_args, nan_args + + a, b = arg_domain.get_numerical_endpoints( + parameter_values=dist._parameters) + + a, b, arg = np.broadcast_arrays(a, b, arg) + a_included, b_included = arg_domain.inclusive + + inside = (a <= arg) if a_included else a < arg + inside &= (arg <= b) if b_included else arg < b + # TODO: add `supported` method and check here + on = np.zeros(a.shape, dtype=int) + on[a == arg] = -1 + on[b == arg] = 1 + outside = np.zeros(a.shape, dtype=int) + outside[(arg < a) if a_included else arg <= a] = -1 + outside[(b < arg) if b_included else b <= arg] = 1 + nan = np.isnan(arg) + + return inside, on, outside, nan + + +def test_input_validation(): + class Test(ContinuousDistribution): + _variable = _RealParameter('x', domain=_RealDomain()) + + message = ("The `Test` distribution family does not accept parameters, " + "but parameters `{'a'}` were provided.") + with pytest.raises(ValueError, match=message): + Test(a=1, ) + + message = "Attribute `tol` of `Test` must be a positive float, if specified." + with pytest.raises(ValueError, match=message): + Test(tol=np.asarray([])) + with pytest.raises(ValueError, match=message): + Test(tol=[1, 2, 3]) + with pytest.raises(ValueError, match=message): + Test(tol=np.nan) + with pytest.raises(ValueError, match=message): + Test(tol=-1) + + message = ("Argument `order` of `Test.moment` must be a " + "finite, positive integer.") + with pytest.raises(ValueError, match=message): + Test().moment(-1) + with pytest.raises(ValueError, match=message): + Test().moment(np.inf) + + message = "Argument `kind` of `Test.moment` must be one of..." + with pytest.raises(ValueError, match=message): + Test().moment(2, kind='coconut') + + message = ("Argument `rng` passed to the `Test` distribution family is of " + "type ``, but it must be a NumPy `Generator`.") + with pytest.raises(ValueError, match=message): + Test(rng=1) + + class Test2(ContinuousDistribution): + _p1 = _RealParameter('c', domain=_RealDomain()) + _p2 = _RealParameter('d', domain=_RealDomain()) + _parameterizations = [_Parameterization(_p1, _p2)] + _variable = _RealParameter('x', domain=_RealDomain()) + + message = ("The provided parameters `{a}` do not match a supported " + "parameterization of the `Test2` distribution family.") + with pytest.raises(ValueError, match=message): + Test2(a=1) + + message = ("The `Test2` distribution family requires parameters, but none " + "were provided.") + with pytest.raises(ValueError, match=message): + Test2() + + message = ("The parameters `{c, d}` provided to the `Test2` " + "distribution family cannot be broadcast to the same shape.") + with pytest.raises(ValueError, match=message): + Test2(c=[1, 2], d=[1, 2, 3]) + + message = ("The argument provided to `Test2.pdf` cannot be be broadcast to " + "the same shape as the distribution parameters.") + with pytest.raises(ValueError, match=message): + dist = Test2(c=[1, 2, 3], d=[1, 2, 3]) + dist.pdf([1, 2]) + + message = "Parameter `c` must be of real dtype." + with pytest.raises(ValueError, match=message): + Test2(c=[1, object()], d=[1, 2]) + + message = "Parameter `convention` of `Test2.kurtosis` must be one of..." + with pytest.raises(ValueError, match=message): + dist = Test2(c=[1, 2, 3], d=[1, 2, 3]) + dist.kurtosis(convention='coconut') + + + + +# I removed `None` from this list. The current behavior is to generate a new +# `default_rng()` every time it is needed. We should not generate it during +# initialization because it increases the time by more than 50%! +@pytest.mark.parametrize('seed', [23434924629239023]) +def test_deepcopy_pickle(seed): + kwargs = dict(a=[-1, 2], b=10) + if seed: + kwargs['rng'] = np.random.default_rng(seed) + dist1 = LogUniform(**kwargs) + dist2 = deepcopy(dist1) + dist3 = pickle.loads(pickle.dumps(dist1)) + res1, res2, res3 = dist1.sample(), dist2.sample(), dist3.sample() + assert_equal(res2, res1) + assert_equal(res3, res1) + + +class TestAttributes: + def test_cache_policy(self): + dist = StandardNormal(cache_policy="no_cache") + # make error message more appropriate + message = "`StandardNormal` does not provide an accurate implementation of the " + with pytest.raises(NotImplementedError, match=message): + dist.mean(method='cache') + mean = dist.mean() + with pytest.raises(NotImplementedError, match=message): + dist.mean(method='cache') + + # add to enum + dist.cache_policy = None + with pytest.raises(NotImplementedError, match=message): + dist.mean(method='cache') + mean = dist.mean() # method is 'formula' by default + cached_mean = dist.mean(method='cache') + assert_equal(cached_mean, mean) + + # cache is overridden by latest evaluation + quadrature_mean = dist.mean(method='quadrature') + cached_mean = dist.mean(method='cache') + assert_equal(cached_mean, quadrature_mean) + assert not np.all(mean == quadrature_mean) + + # We can turn the cache off, and it won't change, but the old cache is + # still available + dist.cache_policy = "no_cache" + mean = dist.mean(method='formula') + cached_mean = dist.mean(method='cache') + assert_equal(cached_mean, quadrature_mean) + assert not np.all(mean == quadrature_mean) + + dist.reset_cache() + with pytest.raises(NotImplementedError, match=message): + dist.mean(method='cache') + + message = "Attribute `cache_policy` of `StandardNormal`..." + with pytest.raises(ValueError, match=message): + dist.cache_policy = "invalid" + + def test_tol(self): + x = 3. + X = stats.Normal() + + message = "Attribute `tol` of `StandardNormal` must..." + with pytest.raises(ValueError, match=message): + X.tol = -1. + with pytest.raises(ValueError, match=message): + X.tol = (0.1,) + with pytest.raises(ValueError, match=message): + X.tol = np.nan + + X1 = stats.Normal(tol=1e-1) + X2 = stats.Normal(tol=1e-12) + ref = X.cdf(x) + res1 = X1.cdf(x, method='quadrature') + res2 = X2.cdf(x, method='quadrature') + assert_allclose(res1, ref, rtol=X1.tol) + assert_allclose(res2, ref, rtol=X2.tol) + assert abs(res1 - ref) > abs(res2 - ref) + + p = 0.99 + X1.tol, X2.tol = X2.tol, X1.tol + ref = X.icdf(p) + res1 = X1.icdf(p, method='inversion') + res2 = X2.icdf(p, method='inversion') + assert_allclose(res1, ref, rtol=X1.tol) + assert_allclose(res2, ref, rtol=X2.tol) + assert abs(res2 - ref) > abs(res1 - ref) + + # Test the tolerance logic in one dispatch method + # When tol is set, quadrature should be used -> correct entropy. + # When tol is not set, logexp should be used -> incorrect entropy. + wrong_entropy = 1.23456 + + class TestDist(ContinuousDistribution): + _variable = _RealParameter('x', domain=_RealDomain(endpoints=(0, 0.5))) + def _logpdf_formula(self, x, *args, **kwargs): + return np.full_like(x, np.log(2.)) + def _entropy_formula(self, *args, **kwargs): + return wrong_entropy + + X0 = stats.Uniform(a=0., b=0.5) + assert_allclose(TestDist(tol=1e-10).logentropy(), X0.logentropy()) + assert_allclose(TestDist().logentropy(), np.log(wrong_entropy)) + + + def test_iv_policy(self): + X = stats.Uniform(a=0, b=1) + assert X.pdf(2) == 0 + + X.iv_policy = 'skip_all' + assert X.pdf(np.asarray(2.)) == 1 + + # Tests _set_invalid_nan + a, b = np.asarray(1.), np.asarray(0.) # invalid parameters + X = stats.Uniform(a=a, b=b, iv_policy='skip_all') + assert X.pdf(np.asarray(2.)) == -1 + + # Tests _set_invalid_nan_property + class MyUniform(stats.Uniform): + def _entropy_formula(self, *args, **kwargs): + return 'incorrect' + + def _moment_raw_formula(self, order, **params): + return 'incorrect' + + X = MyUniform(a=a, b=b, iv_policy='skip_all') + assert X.entropy() == 'incorrect' + + # Tests _validate_order_kind + assert X.moment(kind='raw', order=-1) == 'incorrect' + + # Test input validation + message = "Attribute `iv_policy` of `MyUniform`..." + with pytest.raises(ValueError, match=message): + X.iv_policy = "invalid" + + +class TestTransforms: + @pytest.mark.filterwarnings("ignore") + @given(data=strategies.data(), seed=strategies.integers(min_value=0)) + def test_loc_scale(self, data, seed): + # Need tests with negative scale + rng = np.random.default_rng(seed) + + class TransformedNormal(ShiftedScaledDistribution): + def __init__(self, *args, **kwargs): + super().__init__(StandardNormal(), *args, **kwargs) + + tmp = draw_distribution_from_family( + TransformedNormal, data, rng, proportions=(1, 0, 0, 0), min_side=1) + dist, x, y, p, logp, result_shape, x_result_shape, xy_result_shape = tmp + + loc = dist.loc + scale = dist.scale + dist0 = StandardNormal() + dist_ref = stats.norm(loc=loc, scale=scale) + + x0 = (x - loc) / scale + y0 = (y - loc) / scale + + a, b = dist.support() + a0, b0 = dist0.support() + assert_allclose(a, a0 + loc) + assert_allclose(b, b0 + loc) + assert_allclose(dist.logentropy(), np.log(dist.entropy() + 0j)) + assert_allclose(dist.entropy(), dist_ref.entropy()) + assert_allclose(dist.median(), dist0.median() + loc) + assert_allclose(dist.mode(), dist0.mode() + loc) + assert_allclose(dist.mean(), dist0.mean() + loc) + assert_allclose(dist.variance(), dist0.variance() * scale**2) + assert_allclose(dist.standard_deviation(), dist.variance()**0.5) + assert_allclose(dist.skewness(), dist0.skewness() * np.sign(scale)) + assert_allclose(dist.kurtosis(), dist0.kurtosis()) + assert_allclose(dist.logpdf(x), dist0.logpdf(x0) - np.log(scale)) + assert_allclose(dist.pdf(x), dist0.pdf(x0) / scale) + assert_allclose(dist.logcdf(x), dist0.logcdf(x0)) + assert_allclose(dist.cdf(x), dist0.cdf(x0)) + assert_allclose(dist.logccdf(x), dist0.logccdf(x0)) + assert_allclose(dist.ccdf(x), dist0.ccdf(x0)) + assert_allclose(dist.logcdf(x, y), dist0.logcdf(x0, y0)) + assert_allclose(dist.cdf(x, y), dist0.cdf(x0, y0)) + assert_allclose(dist.logccdf(x, y), dist0.logccdf(x0, y0)) + assert_allclose(dist.ccdf(x, y), dist0.ccdf(x0, y0)) + assert_allclose(dist.ilogcdf(logp), dist0.ilogcdf(logp)*scale + loc) + assert_allclose(dist.icdf(p), dist0.icdf(p)*scale + loc) + assert_allclose(dist.ilogccdf(logp), dist0.ilogccdf(logp)*scale + loc) + assert_allclose(dist.iccdf(p), dist0.iccdf(p)*scale + loc) + for i in range(1, 5): + assert_allclose(dist.moment(i, 'raw'), dist_ref.moment(i)) + assert_allclose(dist.moment(i, 'central'), + dist0.moment(i, 'central') * scale**i) + assert_allclose(dist.moment(i, 'standardized'), + dist0.moment(i, 'standardized') * np.sign(scale)**i) + + # Transform back to the original distribution using all arithmetic + # operations; check that it behaves as expected. + dist = (dist - 2*loc) + loc + dist = dist/scale**2 * scale + z = np.zeros(dist._shape) # compact broadcasting + + a, b = dist.support() + a0, b0 = dist0.support() + assert_allclose(a, a0 + z) + assert_allclose(b, b0 + z) + assert_allclose(dist.logentropy(), dist0.logentropy() + z) + assert_allclose(dist.entropy(), dist0.entropy() + z) + assert_allclose(dist.median(), dist0.median() + z) + assert_allclose(dist.mode(), dist0.mode() + z) + assert_allclose(dist.mean(), dist0.mean() + z) + assert_allclose(dist.variance(), dist0.variance() + z) + assert_allclose(dist.standard_deviation(), dist0.standard_deviation() + z) + assert_allclose(dist.skewness(), dist0.skewness() + z) + assert_allclose(dist.kurtosis(), dist0.kurtosis() + z) + assert_allclose(dist.logpdf(x), dist0.logpdf(x)+z) + assert_allclose(dist.pdf(x), dist0.pdf(x) + z) + assert_allclose(dist.logcdf(x), dist0.logcdf(x) + z) + assert_allclose(dist.cdf(x), dist0.cdf(x) + z) + assert_allclose(dist.logccdf(x), dist0.logccdf(x) + z) + assert_allclose(dist.ccdf(x), dist0.ccdf(x) + z) + assert_allclose(dist.ilogcdf(logp), dist0.ilogcdf(logp) + z) + assert_allclose(dist.icdf(p), dist0.icdf(p) + z) + assert_allclose(dist.ilogccdf(logp), dist0.ilogccdf(logp) + z) + assert_allclose(dist.iccdf(p), dist0.iccdf(p) + z) + for i in range(1, 5): + assert_allclose(dist.moment(i, 'raw'), dist0.moment(i, 'raw')) + assert_allclose(dist.moment(i, 'central'), dist0.moment(i, 'central')) + assert_allclose(dist.moment(i, 'standardized'), + dist0.moment(i, 'standardized')) + + # These are tough to compare because of the way the shape works + # rng = np.random.default_rng(seed) + # rng0 = np.random.default_rng(seed) + # assert_allclose(dist.sample(x_result_shape, rng=rng), + # dist0.sample(x_result_shape, rng=rng0) * scale + loc) + # assert_allclose(dist.qmc_sample(x_result_shape, rng=rng), + # dist0.qmc_sample(x_result_shape, rng=rng0) * scale + loc) + # Should also try to test fit, plot? + + +class TestFullCoverage: + # Adds tests just to get to 100% test coverage; this way it's more obvious + # if new lines are untested. + def test_Domain(self): + with pytest.raises(NotImplementedError): + _Domain.contains(None, 1.) + with pytest.raises(NotImplementedError): + _Domain.get_numerical_endpoints(None, 1.) + with pytest.raises(NotImplementedError): + _Domain.__str__(None) + + def test_Parameter(self): + with pytest.raises(NotImplementedError): + _Parameter.validate(None, 1.) + + @pytest.mark.parametrize(("dtype_in", "dtype_out"), + [(np.float16, np.float16), + (np.int16, np.float64), + (np.complex128, np.float64)]) + def test_RealParameter_uncommon_dtypes(self, dtype_in, dtype_out): + domain = _RealDomain((-1, 1)) + parameter = _RealParameter('x', domain=domain) + + x = np.asarray([0.5, 2.5], dtype=dtype_in) + arr, dtype, valid = parameter.validate(x, parameter_values={}) + assert_equal(arr, x) + assert dtype == dtype_out + assert_equal(valid, [True, False]) + + def test_ContinuousDistribution_set_invalid_nan(self): + # Exercise code paths when formula returns wrong shape and dtype + # We could consider making this raise an error to force authors + # to return the right shape and dytpe, but this would need to be + # configurable. + class TestDist(ContinuousDistribution): + _variable = _RealParameter('x', domain=_RealDomain(endpoints=(0., 1.))) + def _logpdf_formula(self, x, *args, **kwargs): + return 0 + + X = TestDist() + dtype = np.float32 + X._dtype = dtype + x = np.asarray([0.5], dtype=dtype) + assert X.logpdf(x).dtype == dtype + + def test_fiinfo(self): + assert _fiinfo(np.float64(1.)).max == np.finfo(np.float64).max + assert _fiinfo(np.int64(1)).max == np.iinfo(np.int64).max + + def test_generate_domain_support(self): + msg = _generate_domain_support(StandardNormal) + assert "accepts no distribution parameters" in msg + + msg = _generate_domain_support(Normal) + assert "accepts one parameterization" in msg + + msg = _generate_domain_support(LogUniform) + assert "accepts two parameterizations" in msg + + def test_ContinuousDistribution__str__(self): + X = stats.Uniform(a=0, b=1) + assert str(X) == "Uniform(a=0.0, b=1.0)" + + X = stats.Uniform(a=np.zeros(4), b=1) + assert str(X) == "Uniform(a, b, shape=(4,))" + + X = stats.Uniform(a=np.zeros(4, dtype=np.float32), b=np.ones(4, dtype=np.float32)) + assert str(X) == "Uniform(a, b, shape=(4,), dtype=float32)" diff --git a/scipy/stats/tests/test_distributions.py b/scipy/stats/tests/test_distributions.py index 4e8dfc5257df..ae3ffa7a6f8e 100644 --- a/scipy/stats/tests/test_distributions.py +++ b/scipy/stats/tests/test_distributions.py @@ -7338,7 +7338,20 @@ def test_trapezoid_vect(self): def test_trapz(self): # Basic test for alias x = np.linspace(0, 1, 10) - assert_almost_equal(stats.trapz.pdf(x, 0, 1), stats.uniform.pdf(x)) + with pytest.deprecated_call(match="`trapz.pdf` is deprecated"): + result = stats.trapz.pdf(x, 0, 1) + assert_almost_equal(result, stats.uniform.pdf(x)) + + @pytest.mark.parametrize('method', ['pdf', 'logpdf', 'cdf', 'logcdf', + 'sf', 'logsf', 'ppf', 'isf']) + def test_trapz_deprecation(self, method): + c, d = 0.2, 0.8 + expected = getattr(stats.trapezoid, method)(1, c, d) + with pytest.deprecated_call( + match=f"`trapz.{method}` is deprecated", + ): + result = getattr(stats.trapz, method)(1, c, d) + assert result == expected class TestTriang: diff --git a/scipy/stats/tests/test_fast_gen_inversion.py b/scipy/stats/tests/test_fast_gen_inversion.py index e2d00134ccbf..63052f748bef 100644 --- a/scipy/stats/tests/test_fast_gen_inversion.py +++ b/scipy/stats/tests/test_fast_gen_inversion.py @@ -142,6 +142,7 @@ def test_geninvgauss_uerror(): # TODO: add more distributions +@pytest.mark.fail_slow(5) @pytest.mark.parametrize(("distname, args"), [("beta", (0.11, 0.11))]) def test_error_extreme_params(distname, args): # take extreme parameters where u-error might not be below the tolerance diff --git a/scipy/stats/tests/test_fit.py b/scipy/stats/tests/test_fit.py index f187794d6fa3..5fae9aa4cb66 100644 --- a/scipy/stats/tests/test_fit.py +++ b/scipy/stats/tests/test_fit.py @@ -589,6 +589,7 @@ def test_truncpareto(self): assert_nlff_less_or_close(dist, data, res.params, shapes, **self.tols) + @pytest.mark.fail_slow(5) def test_truncweibull_min(self): # Can't guarantee that all distributions will fit all data with # arbitrary bounds. This distribution just happens to fail above. diff --git a/scipy/stats/tests/test_hypotests.py b/scipy/stats/tests/test_hypotests.py index 516d5b080b25..7783c65d8d0a 100644 --- a/scipy/stats/tests/test_hypotests.py +++ b/scipy/stats/tests/test_hypotests.py @@ -17,6 +17,7 @@ from scipy.stats._mannwhitneyu import mannwhitneyu, _mwu_state from .common_tests import check_named_results from scipy._lib._testutils import _TestPythranFunc +from scipy.stats._axis_nan_policy import SmallSampleWarning, too_small_1d_not_omit class TestEppsSingleton: @@ -55,9 +56,12 @@ def test_epps_singleton_array_like(self): assert_(p1 == p2 == p3) def test_epps_singleton_size(self): - # raise error if less than 5 elements + # warns if sample contains fewer than 5 elements x, y = (1, 2, 3, 4), np.arange(10) - assert_raises(ValueError, epps_singleton_2samp, x, y) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = epps_singleton_2samp(x, y) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) def test_epps_singleton_nonfinite(self): # raise error if there are non-finite values @@ -128,9 +132,12 @@ def test_low_p(self): assert_(_cdf_cvm(res.statistic, n) > 1.0) assert_equal(res.pvalue, 0) - def test_invalid_input(self): - assert_raises(ValueError, cramervonmises, [1.5], "norm") - assert_raises(ValueError, cramervonmises, (), "norm") + @pytest.mark.parametrize('x', [(), [1.5]]) + def test_invalid_input(self, x): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = cramervonmises(x, "norm") + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) def test_values_R(self): # compared against R package goftest, version 1.1.1 @@ -168,13 +175,21 @@ class TestMannWhitneyU: # --- Test Input Validation --- + @pytest.mark.parametrize('kwargs_update', [{'x': []}, {'y': []}, + {'x': [], 'y': []}]) + def test_empty(self, kwargs_update): + x = np.array([1, 2]) # generic, valid inputs + y = np.array([3, 4]) + kwargs = dict(x=x, y=y) + kwargs.update(kwargs_update) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = mannwhitneyu(**kwargs) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) + def test_input_validation(self): x = np.array([1, 2]) # generic, valid inputs y = np.array([3, 4]) - with assert_raises(ValueError, match="`x` and `y` must be of nonzero"): - mannwhitneyu([], y) - with assert_raises(ValueError, match="`x` and `y` must be of nonzero"): - mannwhitneyu(x, []) with assert_raises(ValueError, match="`use_continuity` must be one"): mannwhitneyu(x, y, use_continuity='ekki') with assert_raises(ValueError, match="`alternative` must be one of"): @@ -572,11 +587,6 @@ def test_gh_9184(self, use_continuity, alternative, method, pvalue_exp): assert_equal(res.statistic, statistic_exp) assert_allclose(res.pvalue, pvalue_exp) - def test_gh_6897(self): - # Test for correct behavior with empty input - with assert_raises(ValueError, match="`x` and `y` must be of nonzero"): - mannwhitneyu([], []) - def test_gh_4067(self): # Test for correct behavior with all NaN input - default is propagate a = np.array([np.nan, np.nan, np.nan, np.nan, np.nan]) @@ -1314,13 +1324,16 @@ def test_against_fisher_exact(self, alternative): class TestCvm_2samp: + @pytest.mark.parametrize('args', [([], np.arange(5)), + (np.arange(5), [1])]) + def test_too_small_input(self, args): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = cramervonmises_2samp(*args) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) + def test_invalid_input(self): y = np.arange(5) - msg = 'x and y must contain at least two observations.' - with pytest.raises(ValueError, match=msg): - cramervonmises_2samp([], y) - with pytest.raises(ValueError, match=msg): - cramervonmises_2samp(y, [1]) msg = 'method must be either auto, exact or asymptotic' with pytest.raises(ValueError, match=msg): cramervonmises_2samp(y, y, 'xyz') diff --git a/scipy/stats/tests/test_mgc.py b/scipy/stats/tests/test_mgc.py index c82b81530d58..320f0b1edf98 100644 --- a/scipy/stats/tests/test_mgc.py +++ b/scipy/stats/tests/test_mgc.py @@ -194,7 +194,7 @@ def test_dist_perm(self): assert_approx_equal(stat_dist, 0.163, significant=1) assert_approx_equal(pvalue_dist, 0.001, significant=1) - @pytest.mark.fail_slow(10) # all other tests are XSLOW; we need at least one to run + @pytest.mark.fail_slow(20) # all other tests are XSLOW; we need at least one to run @pytest.mark.slow def test_pvalue_literature(self): np.random.seed(12345678) diff --git a/scipy/stats/tests/test_morestats.py b/scipy/stats/tests/test_morestats.py index 24e6428cd71b..e4bd35bd8573 100644 --- a/scipy/stats/tests/test_morestats.py +++ b/scipy/stats/tests/test_morestats.py @@ -7,6 +7,7 @@ from functools import partial import numpy as np +import numpy.testing from numpy.random import RandomState from numpy.testing import (assert_array_equal, assert_almost_equal, assert_array_less, assert_array_almost_equal, @@ -21,10 +22,12 @@ from .._hypotests import _get_wilcoxon_distr, _get_wilcoxon_distr2 from scipy.stats._binomtest import _binary_search_for_binom_tst from scipy.stats._distr_params import distcont +from scipy.stats._axis_nan_policy import (SmallSampleWarning, too_small_nd_omit, + too_small_1d_omit, too_small_1d_not_omit) from scipy.conftest import array_api_compatible from scipy._lib._array_api import (array_namespace, xp_assert_close, xp_assert_less, - SCIPY_ARRAY_API, xp_assert_equal) + xp_assert_equal, is_numpy) skip_xp_backends = pytest.mark.skip_xp_backends @@ -191,19 +194,12 @@ def test_2d(self): assert_almost_equal(pw, 0.52460, decimal=3) assert_almost_equal(shapiro_test.pvalue, 0.52460, decimal=3) - def test_empty_input(self): - assert_raises(ValueError, stats.shapiro, []) - assert_raises(ValueError, stats.shapiro, [[], [], []]) - - def test_not_enough_values(self): - assert_raises(ValueError, stats.shapiro, [1, 2]) - error_type = TypeError if SCIPY_ARRAY_API else ValueError - assert_raises(error_type, stats.shapiro, np.array([[], [2]], dtype=object)) - - def test_bad_arg(self): - # Length of x is less than 3. - x = [1] - assert_raises(ValueError, stats.shapiro, x) + @pytest.mark.parametrize('x', ([], [1], [1, 2])) + def test_not_enough_values(self, x): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.shapiro(x) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) def test_nan_input(self): x = np.arange(10.) @@ -634,9 +630,12 @@ def test_exact(self): assert_almost_equal(W, 10.0, 11) assert_almost_equal(pval, 0.533333333333333333, 7) - def test_bad_arg(self): - assert_raises(ValueError, stats.ansari, [], [1]) - assert_raises(ValueError, stats.ansari, [1], []) + @pytest.mark.parametrize('args', [([], [1]), ([1], [])]) + def test_bad_arg(self, args): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.ansari(*args) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) def test_result_attributes(self): x = [1, 2, 3, 3, 4] @@ -757,10 +756,23 @@ def test_result_attributes(self, xp): attributes = ('statistic', 'pvalue') check_named_results(res, attributes) + @pytest.mark.skip_xp_backends( + "jax.numpy", cpu_only=True, + reasons=['`var` incorrect when `correction > n` (google/jax#21330)']) + @pytest.mark.usefixtures("skip_xp_backends") def test_empty_arg(self, xp): args = (g1, g2, g3, g4, g5, g6, g7, g8, g9, g10, []) args = [xp.asarray(arg) for arg in args] - res = stats.bartlett(*args) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.bartlett(*args) + else: + with np.testing.suppress_warnings() as sup: + # torch/array_api_strict + sup.filter(RuntimeWarning, "invalid value encountered") + sup.filter(UserWarning, r"var\(\): degrees of freedom is <= 0.") + sup.filter(RuntimeWarning, "Degrees of freedom <= 0 for slice") + res = stats.bartlett(*args) NaN = xp.asarray(xp.nan) xp_assert_equal(res.statistic, NaN) xp_assert_equal(res.pvalue, NaN) @@ -1141,7 +1153,10 @@ def test_bad_num_args(self): def test_empty_arg(self): x = np.arange(5) - assert_equal((np.nan, np.nan), stats.fligner(x, x**2, [])) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.fligner(x, x**2, []) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) def mood_cases_with_ties(): @@ -1309,9 +1324,11 @@ def test_mood_3d(self): stats.mood(slice1, slice2)) def test_mood_bad_arg(self): - # Raise ValueError when the sum of the lengths of the args is - # less than 3 - assert_raises(ValueError, stats.mood, [1], []) + # Warns when the sum of the lengths of the args is less than 3 + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.mood([1], []) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) def test_mood_alternative(self): @@ -1722,9 +1739,13 @@ def test_moments_normal_distribution(self, xp): xp_assert_close(xp.asarray((m1, m2, m3)), expected[:-1], atol=0.02, rtol=1e-2) def test_empty_input(self, xp): - message = 'Data input must not be empty' - with pytest.raises(ValueError, match=message): - stats.kstat(xp.asarray([])) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.kstat(xp.asarray([])) + else: + with np.errstate(invalid='ignore'): # for array_api_strict + res = stats.kstat(xp.asarray([])) + xp_assert_equal(res, xp.asarray(xp.nan)) def test_nan_input(self, xp): data = xp.arange(10.) @@ -1758,13 +1779,17 @@ def test_against_R(self, case, xp): xp_assert_close(res, xp.asarray(ref)) - @array_api_compatible class TestKstatVar: def test_empty_input(self, xp): - message = 'Data input must not be empty' - with pytest.raises(ValueError, match=message): - stats.kstatvar(xp.asarray([])) + x = xp.asarray([]) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.kstatvar(x) + else: + with np.errstate(invalid='ignore'): # for array_api_strict + res = stats.kstatvar(x) + xp_assert_equal(res, xp.asarray(xp.nan)) def test_nan_input(self, xp): data = xp.arange(10.) @@ -1774,7 +1799,8 @@ def test_nan_input(self, xp): @skip_xp_backends(np_only=True, reasons=['input validation of `n` does not depend on backend']) - def test_bad_arg(self, xp): + @pytest.mark.usefixtures("skip_xp_backends") + def test_bad_arg(self): # Raise ValueError is n is not 1 or 2. data = [1] n = 10 @@ -2624,7 +2650,16 @@ def test_circfuncs_array_like(self, test_func, expected, xp): def test_empty(self, test_func, xp): dtype = xp.float64 x = xp.asarray([], dtype=dtype) - xp_assert_equal(test_func(x), xp.asarray(xp.nan, dtype=dtype)) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = test_func(x) + else: + with np.testing.suppress_warnings() as sup: + # for array_api_strict + sup.filter(RuntimeWarning, "Mean of empty slice") + sup.filter(RuntimeWarning, "invalid value encountered") + res = test_func(x) + xp_assert_equal(res, xp.asarray(xp.nan, dtype=dtype)) @pytest.mark.parametrize("test_func", [stats.circmean, stats.circvar, stats.circstd]) @@ -2705,12 +2740,14 @@ def test_nan_omit_array(self, test_func, expected): [351, 7, 4, 352, 9, 349, np.nan], [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]]) for axis in expected.keys(): - out = test_func(x, high=360, nan_policy='omit', axis=axis) if axis is None: + out = test_func(x, high=360, nan_policy='omit', axis=axis) assert_allclose(out, expected[axis], rtol=1e-7) else: - assert_allclose(out[:-1], expected[axis], rtol=1e-7) - assert_(np.isnan(out[-1])) + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + out = test_func(x, high=360, nan_policy='omit', axis=axis) + assert_allclose(out[:-1], expected[axis], rtol=1e-7) + assert_(np.isnan(out[-1])) @pytest.mark.parametrize("test_func,expected", [(stats.circmean, 0.167690146), @@ -2725,16 +2762,18 @@ def test_nan_omit(self, test_func, expected): stats.circstd]) def test_nan_omit_all(self, test_func): x = [np.nan, np.nan, np.nan, np.nan, np.nan] - assert_(np.isnan(test_func(x, nan_policy='omit'))) + with pytest.warns(SmallSampleWarning, match=too_small_1d_omit): + assert_(np.isnan(test_func(x, nan_policy='omit'))) @pytest.mark.parametrize("test_func", [stats.circmean, stats.circvar, stats.circstd]) def test_nan_omit_all_axis(self, test_func): - x = np.array([[np.nan, np.nan, np.nan, np.nan, np.nan], - [np.nan, np.nan, np.nan, np.nan, np.nan]]) - out = test_func(x, nan_policy='omit', axis=1) - assert_(np.isnan(out).all()) - assert_(len(out) == 2) + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + x = np.array([[np.nan, np.nan, np.nan, np.nan, np.nan], + [np.nan, np.nan, np.nan, np.nan, np.nan]]) + out = test_func(x, nan_policy='omit', axis=1) + assert_(np.isnan(out).all()) + assert_(len(out) == 2) @pytest.mark.parametrize("x", [[355, 5, 2, 359, 10, 350, np.nan], diff --git a/scipy/stats/tests/test_mstats_basic.py b/scipy/stats/tests/test_mstats_basic.py index c31c965e0e30..68d0a8547140 100644 --- a/scipy/stats/tests/test_mstats_basic.py +++ b/scipy/stats/tests/test_mstats_basic.py @@ -19,8 +19,9 @@ assert_array_almost_equal_nulp, assert_, assert_allclose, assert_array_equal) from numpy.testing import suppress_warnings -from scipy.stats import _mstats_basic +from scipy.stats import _mstats_basic, _stats_py from scipy.conftest import skip_xp_invalid_arg +from scipy.stats._axis_nan_policy import SmallSampleWarning, too_small_1d_not_omit class TestMquantiles: def test_mquantiles_limit_keyword(self): @@ -474,6 +475,10 @@ def test_kendall_p_exact_large(self): res = _mstats_basic._kendall_p_exact(nc[0], nc[1]) assert_almost_equal(res, expected) + @skip_xp_invalid_arg + # mstats.pointbiserialr returns a NumPy float for the statistic, but converts + # it to a masked array with no masked elements before calling `special.betainc`, + # which won't accept masked arrays when `SCIPY_ARRAY_API=1`. def test_pointbiserial(self): x = [1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, -1] @@ -916,7 +921,7 @@ def test_regress_simple(): result = mstats.linregress(x, y) # Result is of a correct class and with correct fields - lr = stats._stats_mstats_common.LinregressResult + lr = _stats_py.LinregressResult assert_(isinstance(result, lr)) attributes = ('slope', 'intercept', 'rvalue', 'pvalue', 'stderr') check_named_results(result, attributes, ma=True) @@ -1101,7 +1106,10 @@ def test_vs_nonmasked(self): mfuncs = [mstats.normaltest, mstats.skewtest, mstats.kurtosistest] x = [1, 2, 3, 4] for func, mfunc in zip(funcs, mfuncs): - assert_raises(ValueError, func, x) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = func(x) + assert np.isnan(res.statistic) + assert np.isnan(res.pvalue) assert_raises(ValueError, mfunc, x) def test_axis_None(self): diff --git a/scipy/stats/tests/test_resampling.py b/scipy/stats/tests/test_resampling.py index 2d296f4d0dc7..42436c13b948 100644 --- a/scipy/stats/tests/test_resampling.py +++ b/scipy/stats/tests/test_resampling.py @@ -732,6 +732,27 @@ def statistic_extradim(*args, axis): ref.confidence_interval.high, atol=1e-15) +def test_gh_20850(): + rng = np.random.default_rng(2085020850) + x = rng.random((10, 2)) + y = rng.random((11, 2)) + def statistic(x, y, axis): + return stats.ttest_ind(x, y, axis=axis).statistic + + # The shapes do *not* need to be the same along axis + stats.bootstrap((x, y), statistic) + stats.bootstrap((x.T, y.T), statistic, axis=1) + # But even when the shapes *are* the same along axis, the lengths + # along other dimensions have to be the same (or `bootstrap` warns). + message = "Ignoring the dimension specified by `axis`..." + with pytest.warns(FutureWarning, match=message): + stats.bootstrap((x, y[:10, 0]), statistic) # this won't work after 1.16 + with pytest.warns(FutureWarning, match=message): + stats.bootstrap((x, y[:10, 0:1]), statistic) # this will + with pytest.warns(FutureWarning, match=message): + stats.bootstrap((x.T, y.T[0:1, :10]), statistic, axis=1) # this will + + # --- Test Monte Carlo Hypothesis Test --- # class TestMonteCarloHypothesisTest: @@ -1097,6 +1118,7 @@ def statistic(*args, axis): assert_allclose(res.statistic, ref.statistic) assert_allclose(res.pvalue, ref.pvalue, atol=1e-2) + @pytest.mark.fail_slow(2) @pytest.mark.xfail_on_32bit("Statistic may not depend on sample order on 32-bit") def test_finite_precision_statistic(self): # Some statistics return numerically distinct values when the values @@ -1924,6 +1946,7 @@ def test_batch_generator(self, iterable, batch, expected): got = list(_resampling._batch_generator(iterable, batch)) assert got == expected + @pytest.mark.fail_slow(2) def test_finite_precision_statistic(self): # Some statistics return numerically distinct values when the values # should be equal in theory. Test that `permutation_test` accounts diff --git a/scipy/stats/tests/test_sampling.py b/scipy/stats/tests/test_sampling.py index 82fe9c36ea19..99921adb0d9b 100644 --- a/scipy/stats/tests/test_sampling.py +++ b/scipy/stats/tests/test_sampling.py @@ -1377,9 +1377,6 @@ def test_bad_args(self): class TestRatioUniforms: - """ Tests for rvs_ratio_uniforms. - """ - def test_rv_generation(self): # use KS test to check distribution of rvs # normal distribution diff --git a/scipy/stats/tests/test_stats.py b/scipy/stats/tests/test_stats.py index 356a33915c7b..a5232339e853 100644 --- a/scipy/stats/tests/test_stats.py +++ b/scipy/stats/tests/test_stats.py @@ -34,15 +34,17 @@ from scipy.special import binom from scipy import optimize from .common_tests import check_named_results -from scipy.stats._axis_nan_policy import _broadcast_concatenate -from scipy.stats._stats_py import _permutation_distribution_t, _chk_asarray, _moment +from scipy.stats._axis_nan_policy import (_broadcast_concatenate, SmallSampleWarning, + too_small_nd_omit, too_small_nd_not_omit, + too_small_1d_omit, too_small_1d_not_omit) +from scipy.stats._stats_py import (_permutation_distribution_t, _chk_asarray, _moment, + LinregressResult) from scipy._lib._util import AxisError from scipy.conftest import array_api_compatible, skip_xp_invalid_arg from scipy._lib._array_api import (xp_assert_close, xp_assert_equal, array_namespace, copy, is_numpy, is_torch, SCIPY_ARRAY_API, size as xp_size) - skip_xp_backends = pytest.mark.skip_xp_backends @@ -586,7 +588,7 @@ def test_input_validation(self): with pytest.raises(ValueError, match=message): res.confidence_interval(method="exact") - @pytest.mark.fail_slow(2) + @pytest.mark.fail_slow(5) @pytest.mark.skip_xp_backends(np_only=True) @pytest.mark.xfail_on_32bit("Monte Carlo method needs > a few kB of memory") @pytest.mark.parametrize('alternative', ('less', 'greater', 'two-sided')) @@ -2000,6 +2002,13 @@ def test_empty_result(self): class TestRegression: + def test_one_arg_deprecation(self): + x = np.arange(20).reshape((2, 10)) + message = "Inference of the two sets..." + with pytest.deprecated_call(match=message): + stats.linregress(x) + stats.linregress(x[0], x[1]) + def test_linregressBIGX(self): # W.II.F. Regress BIG on X. result = stats.linregress(X, BIG) @@ -2047,7 +2056,7 @@ def test_regress_simple(self): y += np.sin(np.linspace(0, 20, 100)) result = stats.linregress(x, y) - lr = stats._stats_mstats_common.LinregressResult + lr = LinregressResult assert_(isinstance(result, lr)) assert_almost_equal(result.stderr, 2.3957814497838803e-3) @@ -2091,6 +2100,9 @@ def test_regress_against_R(self): assert_allclose(res.stderr, 0.0519051424731) assert_allclose(res.intercept_stderr, 8.0490133029927) + # TODO: remove this test once single-arg support is dropped; + # deprecation warning tested in `test_one_arg_deprecation` + @pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_regress_simple_onearg_rows(self): # Regress a line w sinusoidal noise, # with a single input of shape (2, N) @@ -2103,6 +2115,9 @@ def test_regress_simple_onearg_rows(self): assert_almost_equal(result.stderr, 2.3957814497838803e-3) assert_almost_equal(result.intercept_stderr, 1.3866936078570702e-1) + # TODO: remove this test once single-arg support is dropped; + # deprecation warning tested in `test_one_arg_deprecation` + @pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_regress_simple_onearg_cols(self): x = np.linspace(0, 100, 100) y = 0.2 * np.linspace(0, 100, 100) + 10 @@ -2113,6 +2128,9 @@ def test_regress_simple_onearg_cols(self): assert_almost_equal(result.stderr, 2.3957814497838803e-3) assert_almost_equal(result.intercept_stderr, 1.3866936078570702e-1) + # TODO: remove this test once single-arg support is dropped; + # deprecation warning tested in `test_one_arg_deprecation` + @pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_regress_shape_error(self): # Check that a single input argument to linregress with wrong shape # results in a ValueError. @@ -2165,7 +2183,7 @@ def test_linregress_result_attributes(self): result = stats.linregress(x, y) # Result is of a correct class - lr = stats._stats_mstats_common.LinregressResult + lr = LinregressResult assert_(isinstance(result, lr)) # LinregressResult elements have correct names @@ -2243,7 +2261,7 @@ def test_nan_input(self): result = stats.linregress(x, x) # Make sure the result still comes back as `LinregressResult` - lr = stats._stats_mstats_common.LinregressResult + lr = LinregressResult assert_(isinstance(result, lr)) assert_array_equal(result, (np.nan,)*5) assert_equal(result.intercept_stderr, np.nan) @@ -2438,11 +2456,11 @@ def test_empty(self): assert_equal(stats.scoreatpercentile([], [50, 99]), [np.nan, np.nan]) -@pytest.mark.filterwarnings('ignore::FutureWarning') class TestMode: def test_empty(self): - vals, counts = stats.mode([]) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + vals, counts = stats.mode([]) assert_equal(vals, np.array([])) assert_equal(counts, np.array([])) @@ -2491,7 +2509,8 @@ def test_mode_result_attributes(self): actual = stats.mode(data1) attributes = ('mode', 'count') check_named_results(actual, attributes) - actual2 = stats.mode(data2) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + actual2 = stats.mode(data2) check_named_results(actual2, attributes) def test_mode_nan(self): @@ -2617,15 +2636,18 @@ def test_gh9955(self): # The behavior of mode with empty slices (whether the input was empty # or all elements were omitted) was inconsistent. Test that this is # resolved: the mode of an empty slice is NaN and the count is zero. - res = stats.mode([]) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.mode([]) ref = (np.nan, 0) assert_equal(res, ref) - res = stats.mode([np.nan], nan_policy='omit') + with pytest.warns(SmallSampleWarning, match=too_small_1d_omit): + res = stats.mode([np.nan], nan_policy='omit') assert_equal(res, ref) a = [[10., 20., 20.], [np.nan, np.nan, np.nan]] - res = stats.mode(a, axis=1, nan_policy='omit') + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + res = stats.mode(a, axis=1, nan_policy='omit') ref = ([20, np.nan], [2, 0]) assert_equal(res, ref) @@ -2634,14 +2656,19 @@ def test_gh9955(self): assert_equal(res, ref) z = np.array([[], []]) - res = stats.mode(z, axis=1) + with pytest.warns(SmallSampleWarning, match=too_small_nd_not_omit): + res = stats.mode(z, axis=1) ref = ([np.nan, np.nan], [0, 0]) assert_equal(res, ref) @pytest.mark.filterwarnings('ignore::RuntimeWarning') # np.mean warns @pytest.mark.parametrize('z', [np.empty((0, 1, 2)), np.empty((1, 1, 2))]) def test_gh17214(self, z): - res = stats.mode(z, axis=None, keepdims=True) + if z.size == 0: + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.mode(z, axis=None, keepdims=True) + else: + res = stats.mode(z, axis=None, keepdims=True) ref = np.mean(z, axis=None, keepdims=True) assert res[0].shape == res[1].shape == ref.shape == (1, 1, 1) @@ -2668,21 +2695,25 @@ class TestSEM: testcase = [1., 2., 3., 4.] scalar_testcase = 4. - def test_sem(self, xp): + def test_sem_scalar(self, xp): # This is not in R, so used: # sqrt(var(testcase)*3/4)/sqrt(3) # y = stats.sem(self.shoes[0]) # assert_approx_equal(y,0.775177399) scalar_testcase = xp.asarray(self.scalar_testcase)[()] - with suppress_warnings() as sup, np.errstate(invalid="ignore"): - # numpy - sup.filter(RuntimeWarning, "Degrees of freedom <= 0 for slice") - # torch - sup.filter(UserWarning, "std*") - y = stats.sem(scalar_testcase) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + y = stats.sem(scalar_testcase) + else: + # Other array types can emit a variety of warnings. + with np.testing.suppress_warnings() as sup: + sup.filter(UserWarning) + sup.filter(RuntimeWarning) + y = stats.sem(scalar_testcase) assert xp.isnan(y) + def test_sem(self, xp): testcase = xp.asarray(self.testcase) y = stats.sem(testcase) xp_assert_close(y, xp.asarray(0.6454972244)) @@ -3042,9 +3073,10 @@ def test_api(self): stats.iqr(d, None, (50, 50), 'normal', 'raise', 'linear') stats.iqr(d, None, (25, 75), -0.4, 'omit', 'lower', True) - def test_empty(self): - assert_equal(stats.iqr([]), np.nan) - assert_equal(stats.iqr(np.arange(0)), np.nan) + @pytest.mark.parametrize('x', [[], np.arange(0)]) + def test_empty(self, x): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + assert_equal(stats.iqr(x), np.nan) def test_constant(self): # Constant array always gives 0 @@ -3335,10 +3367,7 @@ def test_moment(self, xp): y = stats.moment(testcase, [1.0, 2, 3, 4.0]) xp_assert_close(y, xp.asarray([0, 1.25, 0, 2.5625])) - # test empty input - message = r"Mean of empty slice\.|invalid value encountered.*" - with pytest.warns(RuntimeWarning, match=message): - np.mean([]) # lazy way of ignoring warnings + def test_cases(): y = stats.moment(xp.asarray([])) xp_assert_equal(y, xp.asarray(xp.nan)) y = stats.moment(xp.asarray([], dtype=xp.float32)) @@ -3350,6 +3379,16 @@ def test_moment(self, xp): y = stats.moment(xp.asarray([[]]), order=[0, 1], axis=0) xp_assert_equal(y, xp.empty((2, 0))) + # test empty input + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match="See documentation for..."): + test_cases() + else: + with np.testing.suppress_warnings() as sup: # needed by array_api_strict + sup.filter(RuntimeWarning, "Mean of empty slice.") + sup.filter(RuntimeWarning, "invalid value") + test_cases() + def test_nan_policy(self): x = np.arange(10.) x[9] = np.nan @@ -3435,13 +3474,20 @@ class SkewKurtosisTest: class TestSkew(SkewKurtosisTest): - def test_empty_1d(self): - # This is not essential behavior to maintain w/ array API - message = r"Mean of empty slice\.|invalid value encountered.*" - with pytest.warns(RuntimeWarning, match=message): - stats.skew([]) - with pytest.warns(RuntimeWarning, match=message): - stats.kurtosis([]) + @array_api_compatible + @pytest.mark.parametrize('stat_fun', [stats.skew, stats.kurtosis]) + def test_empty_1d(self, stat_fun, xp): + x = xp.asarray([]) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stat_fun(x) + else: + with np.testing.suppress_warnings() as sup: + # array_api_strict produces these + sup.filter(RuntimeWarning, "Mean of empty slice") + sup.filter(RuntimeWarning, "invalid value encountered") + res = stat_fun(x) + xp_assert_equal(res, xp.asarray(xp.nan)) @skip_xp_backends('jax.numpy', reasons=["JAX arrays do not support item assignment"]) @@ -4760,11 +4806,13 @@ def test_some_code_paths(self): assert_raises(FloatingPointError, _count_paths_outside_method, 2000, 1000, 1, 1) - def test_argument_checking(self): - # Check that an empty array causes a ValueError - assert_raises(ValueError, stats.ks_2samp, [], [1]) - assert_raises(ValueError, stats.ks_2samp, [1], []) - assert_raises(ValueError, stats.ks_2samp, [], []) + @pytest.mark.parametrize('case', (([], [1]), ([1], []), ([], []))) + def test_argument_checking(self, case): + # Check that an empty array warns + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.ks_2samp(*case) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) @pytest.mark.xslow def test_gh12218(self): @@ -4909,16 +4957,19 @@ def convert(t, p, alt): rvs1_2D[:, 20:30] = np.nan rvs2_2D[:, 15:25] = np.nan - tr, pr = stats.ttest_rel(rvs1_2D, rvs2_2D, 0, nan_policy='omit') + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + tr, pr = stats.ttest_rel(rvs1_2D, rvs2_2D, 0, nan_policy='omit') - t, p = stats.ttest_rel(rvs1_2D, rvs2_2D, 0, nan_policy='omit', - alternative='less') + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + t, p = stats.ttest_rel(rvs1_2D, rvs2_2D, 0, + nan_policy='omit', alternative='less') assert_allclose(t, tr, rtol=1e-14) with np.errstate(invalid='ignore'): assert_allclose(p, converter(tr, pr, 'less'), rtol=1e-14) - t, p = stats.ttest_rel(rvs1_2D, rvs2_2D, 0, nan_policy='omit', - alternative='greater') + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + t, p = stats.ttest_rel(rvs1_2D, rvs2_2D, 0, + nan_policy='omit', alternative='greater') assert_allclose(t, tr, rtol=1e-14) with np.errstate(invalid='ignore'): assert_allclose(p, converter(tr, pr, 'greater'), rtol=1e-14) @@ -4948,7 +4999,8 @@ def test_ttest_rel_nan_2nd_arg(): def test_ttest_rel_empty_1d_returns_nan(): # Two empty inputs should return a TtestResult containing nan # for both values. - result = stats.ttest_rel([], []) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + result = stats.ttest_rel([], []) assert isinstance(result, stats._stats_py.TtestResult) assert_equal(result, (np.nan, np.nan)) @@ -4961,7 +5013,10 @@ def test_ttest_rel_axis_size_zero(b, expected_shape): # The results should be arrays containing nan with shape # given by the broadcast nonaxis dimensions. a = np.empty((3, 1, 0)) - result = stats.ttest_rel(a, b, axis=-1) + with np.testing.suppress_warnings() as sup: + # first case should warn, second shouldn't? + sup.filter(SmallSampleWarning, too_small_nd_not_omit) + result = stats.ttest_rel(a, b, axis=-1) assert isinstance(result, stats._stats_py.TtestResult) expected_value = np.full(expected_shape, fill_value=np.nan) assert_equal(result.statistic, expected_value) @@ -5016,90 +5071,114 @@ def test_ttest_ci_iv(test_fun, args): res.confidence_interval(confidence_level=10) -def _desc_stats(x1, x2, axis=0): +def _desc_stats(x1, x2, axis=0, *, xp=None): + xp = array_namespace(x1, x2) if xp is None else xp + def _stats(x, axis=0): - x = np.asarray(x) - mu = np.mean(x, axis=axis) - std = np.std(x, axis=axis, ddof=1) + x = xp.asarray(x) + mu = xp.mean(x, axis=axis) + std = xp.std(x, axis=axis, correction=1) nobs = x.shape[axis] return mu, std, nobs + return _stats(x1, axis) + _stats(x2, axis) -def test_ttest_ind(): +@array_api_compatible +@pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) +@pytest.mark.usefixtures("skip_xp_backends") +def test_ttest_ind(xp): # regression test - tr = 1.0912746897927283 - pr = 0.27647818616351882 - tpr = ([tr,-tr],[pr,pr]) + tr = xp.asarray(1.0912746897927283) + pr = xp.asarray(0.27647818616351882) + tr_2D = xp.asarray([tr, -tr]) + pr_2D = xp.asarray([pr, pr]) + + rvs1 = xp.linspace(5, 105, 100) + rvs2 = xp.linspace(1, 100, 100) + rvs1_2D = xp.stack([rvs1, rvs2]) + rvs2_2D = xp.stack([rvs2, rvs1]) + + res = stats.ttest_ind(rvs1, rvs2, axis=0) + t, p = res # check that result object can be unpacked + xp_assert_close(t, tr) + xp_assert_close(p, pr) - rvs2 = np.linspace(1,100,100) - rvs1 = np.linspace(5,105,100) - rvs1_2D = np.array([rvs1, rvs2]) - rvs2_2D = np.array([rvs2, rvs1]) + res = stats.ttest_ind_from_stats(*_desc_stats(rvs1, rvs2)) + t, p = res # check that result object can be unpacked + xp_assert_close(t, tr) + xp_assert_close(p, pr) - t,p = stats.ttest_ind(rvs1, rvs2, axis=0) - assert_array_almost_equal([t,p],(tr,pr)) - # test from_stats API - assert_array_almost_equal(stats.ttest_ind_from_stats(*_desc_stats(rvs1, - rvs2)), - [t, p]) - t,p = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0) - assert_array_almost_equal([t,p],tpr) - args = _desc_stats(rvs1_2D.T, rvs2_2D.T) - assert_array_almost_equal(stats.ttest_ind_from_stats(*args), - [t, p]) - t,p = stats.ttest_ind(rvs1_2D, rvs2_2D, axis=1) - assert_array_almost_equal([t,p],tpr) - args = _desc_stats(rvs1_2D, rvs2_2D, axis=1) - assert_array_almost_equal(stats.ttest_ind_from_stats(*args), - [t, p]) + res = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) - # test scalars - with suppress_warnings() as sup, np.errstate(invalid="ignore"): - sup.filter(RuntimeWarning, "Degrees of freedom <= 0 for slice") - t, p = stats.ttest_ind(4., 3.) - assert_(np.isnan(t)) - assert_(np.isnan(p)) + res = stats.ttest_ind_from_stats(*_desc_stats(rvs1_2D.T, rvs2_2D.T)) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) - # test on 3 dimensions - rvs1_3D = np.dstack([rvs1_2D,rvs1_2D,rvs1_2D]) - rvs2_3D = np.dstack([rvs2_2D,rvs2_2D,rvs2_2D]) - t,p = stats.ttest_ind(rvs1_3D, rvs2_3D, axis=1) - assert_almost_equal(np.abs(t), np.abs(tr)) - assert_array_almost_equal(np.abs(p), pr) - assert_equal(t.shape, (2, 3)) + res = stats.ttest_ind(rvs1_2D, rvs2_2D, axis=1) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) - t, p = stats.ttest_ind(np.moveaxis(rvs1_3D, 2, 0), - np.moveaxis(rvs2_3D, 2, 0), - axis=2) - assert_array_almost_equal(np.abs(t), np.abs(tr)) - assert_array_almost_equal(np.abs(p), pr) - assert_equal(t.shape, (3, 2)) + res = stats.ttest_ind_from_stats(*_desc_stats(rvs1_2D, rvs2_2D, axis=1)) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) + + # test on 3 dimensions removed because generic tests in + # test_axis_nan_policy are much stronger # test alternative parameter - assert_raises(ValueError, stats.ttest_ind, rvs1, rvs2, alternative="error") - assert_raises(ValueError, stats.ttest_ind_from_stats, - *_desc_stats(rvs1_2D.T, rvs2_2D.T), alternative="error") + message = "`alternative` must be 'less', 'greater', or 'two-sided'." + with pytest.raises(ValueError, match=message): + stats.ttest_ind(rvs1, rvs2, alternative = "error") + + args = _desc_stats(rvs1_2D.T, rvs2_2D.T) + with pytest.raises(ValueError, match=message): + stats.ttest_ind_from_stats(*args, alternative = "error") t, p = stats.ttest_ind(rvs1, rvs2, alternative="less") - assert_allclose(p, 1 - (pr/2)) - assert_allclose(t, tr) + xp_assert_close(p, 1 - (pr/2)) + xp_assert_close(t, tr) t, p = stats.ttest_ind(rvs1, rvs2, alternative="greater") - assert_allclose(p, pr/2) - assert_allclose(t, tr) + xp_assert_close(p, pr/2) + xp_assert_close(t, tr) - # Below makes sure ttest_ind_from_stats p-val functions identically to - # ttest_ind - t, p = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0, alternative="less") + # Check that ttest_ind_from_stats agrees with ttest_ind + res1 = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0, alternative="less") args = _desc_stats(rvs1_2D.T, rvs2_2D.T) - assert_allclose( - stats.ttest_ind_from_stats(*args, alternative="less"), [t, p]) + res2 = stats.ttest_ind_from_stats(*args, alternative="less") + xp_assert_close(res1.statistic, res2.statistic) + xp_assert_close(res1.pvalue, res2.pvalue) - t, p = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0, alternative="greater") + res1 = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0, alternative="less") args = _desc_stats(rvs1_2D.T, rvs2_2D.T) - assert_allclose( - stats.ttest_ind_from_stats(*args, alternative="greater"), [t, p]) + res2 = stats.ttest_ind_from_stats(*args, alternative="less") + xp_assert_close(res1.statistic, res2.statistic) + xp_assert_close(res1.pvalue, res2.pvalue) + + # test NaNs + NaN = xp.asarray(xp.nan) + rvs1 = xp.where(xp.arange(rvs1.shape[0]) == 0, NaN, rvs1) + + res = stats.ttest_ind(rvs1, rvs2, axis=0) + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) + + res = stats.ttest_ind_from_stats(*_desc_stats(rvs1, rvs2)) + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) + + +def test_ttest_ind_nan_policy(): + rvs1 = np.linspace(5, 105, 100) + rvs2 = np.linspace(1, 100, 100) + rvs1_2D = np.array([rvs1, rvs2]) + rvs2_2D = np.array([rvs2, rvs1]) + rvs1_3D = np.dstack([rvs1_2D, rvs1_2D, rvs1_2D]) + rvs2_3D = np.dstack([rvs2_2D, rvs2_2D, rvs2_2D]) # check nan policy rng = np.random.RandomState(12345678) @@ -5151,6 +5230,15 @@ def convert(t, p, alt): assert_allclose(p, converter(tr, pr, 'greater'), rtol=1e-14) +def test_ttest_ind_scalar(): + # test scalars + with suppress_warnings() as sup, np.errstate(invalid="ignore"): + sup.filter(RuntimeWarning, "Degrees of freedom <= 0 for slice") + t, p = stats.ttest_ind(4., 3.) + assert np.isnan(t) + assert np.isnan(p) + + class Test_ttest_ind_permutations: N = 20 @@ -5397,6 +5485,19 @@ def test_ttest_ind_permutation_check_p_values(self): print(0.0 not in p_values) assert 0.0 not in p_values + @array_api_compatible + @pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) + @pytest.mark.usefixtures("skip_xp_backends") + def test_permutation_not_implement_for_xp(self, xp): + message = "Use of `permutations` is compatible only with NumPy arrays." + a2, b2 = xp.asarray(self.a2), xp.asarray(self.b2) + if is_numpy(xp): # no error + stats.ttest_ind(a2, b2, permutations=10) + else: # NotImplementedError + with pytest.raises(NotImplementedError, match=message): + stats.ttest_ind(a2, b2, permutations=10) + class Test_ttest_ind_common: # for tests that are performed on variations of the t-test such as @@ -5589,12 +5690,24 @@ def test_alternatives(self, alt, pr, tr): assert_allclose(statistic, tr, atol=1e-10) def test_errors_unsupported(self): - # confirm that attempting to trim with NaNs or permutations raises an - # error - match = "Permutations are currently not supported with trimming." - with assert_raises(ValueError, match=match): + # confirm that attempting to trim with permutations raises an error + match = "Use of `permutations` is incompatible with with use of `trim`." + with assert_raises(NotImplementedError, match=match): stats.ttest_ind([1, 2], [2, 3], trim=.2, permutations=2) + @array_api_compatible + @pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) + @pytest.mark.usefixtures("skip_xp_backends") + def test_permutation_not_implement_for_xp(self, xp): + message = "Use of `trim` is compatible only with NumPy arrays." + a, b = xp.arange(10), xp.arange(10)+1 + if is_numpy(xp): # no error + stats.ttest_ind(a, b, trim=0.1) + else: # NotImplementedError + with pytest.raises(NotImplementedError, match=message): + stats.ttest_ind(a, b, trim=0.1) + @pytest.mark.parametrize("trim", [-.2, .5, 1]) def test_trim_bounds_error(self, trim): match = "Trimming percentage should be 0 <= `trim` < .5." @@ -5602,6 +5715,10 @@ def test_trim_bounds_error(self, trim): stats.ttest_ind([1, 2], [2, 1], trim=trim) +@array_api_compatible +@pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) +@pytest.mark.usefixtures("skip_xp_backends") class Test_ttest_CI: # indices in order [alternative={two-sided, less, greater}, # equal_var={False, True}, trim={0, 0.2}] @@ -5648,13 +5765,16 @@ class Test_ttest_CI: @pytest.mark.parametrize('alternative', ['two-sided', 'less', 'greater']) @pytest.mark.parametrize('equal_var', [False, True]) @pytest.mark.parametrize('trim', [0, 0.2]) - def test_confidence_interval(self, alternative, equal_var, trim): + def test_confidence_interval(self, alternative, equal_var, trim, xp): if equal_var and trim: pytest.xfail('Discrepancy in `main`; needs further investigation.') + if trim and not is_numpy(xp): + pytest.skip('`trim` is only compatible with NumPy input') + rng = np.random.default_rng(3810954496107292580) - x = rng.random(11) - y = rng.random(13) + x = xp.asarray(rng.random(11)) + y = xp.asarray(rng.random(13)) res = stats.ttest_ind(x, y, alternative=alternative, equal_var=equal_var, trim=trim) @@ -5662,13 +5782,16 @@ def test_confidence_interval(self, alternative, equal_var, trim): alternatives = {'two-sided': 0, 'less': 1, 'greater': 2} ref = self.r[alternatives[alternative], int(equal_var), int(np.ceil(trim))] statistic, df, pvalue, low, high = ref - assert_allclose(res.statistic, statistic) - assert_allclose(res.df, df) - assert_allclose(res.pvalue, pvalue) + + rtol = 1e-7 # only 7 digits in reference + xp_assert_close(res.statistic, xp.asarray(statistic), rtol=rtol) + xp_assert_close(res.df, xp.asarray(df), rtol=rtol) + xp_assert_close(res.pvalue, xp.asarray(pvalue), rtol=rtol) + if not equal_var: # CI not available when `equal_var is True` ci = res.confidence_interval(0.9) - assert_allclose(ci.low, low) - assert_allclose(ci.high, high) + xp_assert_close(ci.low, xp.asarray(low), rtol=rtol) + xp_assert_close(ci.high, xp.asarray(high), rtol=rtol) def test__broadcast_concatenate(): @@ -5689,113 +5812,114 @@ def test__broadcast_concatenate(): assert b[i, j, k, l - a.shape[-3], m, n] == c[i, j, k, l, m, n] -def test_ttest_ind_with_uneq_var(): - # check vs. R - a = (1, 2, 3) - b = (1.1, 2.9, 4.2) - pr = 0.53619490753126731 - tr = -0.68649512735572582 +@array_api_compatible +@pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) +@pytest.mark.usefixtures("skip_xp_backends") +def test_ttest_ind_with_uneq_var(xp): + # check vs. R `t.test`, e.g. + # options(digits=20) + # a = c(1., 2., 3.) + # b = c(1.1, 2.9, 4.2) + # t.test(a, b, equal.var=FALSE) + + a = xp.asarray([1., 2., 3.]) + b = xp.asarray([1.1, 2.9, 4.2]) + pr = xp.asarray(0.53619490753126686) + tr = xp.asarray(-0.686495127355726265) + t, p = stats.ttest_ind(a, b, equal_var=False) - assert_array_almost_equal([t,p], [tr, pr]) - # test from desc stats API - assert_array_almost_equal(stats.ttest_ind_from_stats(*_desc_stats(a, b), - equal_var=False), - [t, p]) - - a = (1, 2, 3, 4) - pr = 0.84354139131608286 - tr = -0.2108663315950719 + xp_assert_close(t, tr) + xp_assert_close(p, pr) + + t, p = stats.ttest_ind_from_stats(*_desc_stats(a, b), equal_var=False) + xp_assert_close(t, tr) + xp_assert_close(p, pr) + + a = xp.asarray([1., 2., 3., 4.]) + pr = xp.asarray(0.84354139131608252) + tr = xp.asarray(-0.210866331595072315) + t, p = stats.ttest_ind(a, b, equal_var=False) - assert_array_almost_equal([t,p], [tr, pr]) - assert_array_almost_equal(stats.ttest_ind_from_stats(*_desc_stats(a, b), - equal_var=False), - [t, p]) + xp_assert_close(t, tr) + xp_assert_close(p, pr) + + t, p = stats.ttest_ind_from_stats(*_desc_stats(a, b), equal_var=False) + xp_assert_close(t, tr) + xp_assert_close(p, pr) # regression test - tr = 1.0912746897927283 - tr_uneq_n = 0.66745638708050492 - pr = 0.27647831993021388 - pr_uneq_n = 0.50873585065616544 - tpr = ([tr,-tr],[pr,pr]) + tr = xp.asarray(1.0912746897927283) + tr_uneq_n = xp.asarray(0.66745638708050492) + pr = xp.asarray(0.27647831993021388) + pr_uneq_n = xp.asarray(0.50873585065616544) + tr_2D = xp.asarray([tr, -tr]) + pr_2D = xp.asarray([pr, pr]) + + rvs3 = xp.linspace(1, 100, 25) + rvs2 = xp.linspace(1, 100, 100) + rvs1 = xp.linspace(5, 105, 100) + rvs1_2D = xp.stack([rvs1, rvs2]) + rvs2_2D = xp.stack([rvs2, rvs1]) + + t, p = stats.ttest_ind(rvs1, rvs2, axis=0, equal_var=False) + xp_assert_close(t, tr) + xp_assert_close(p, pr) - rvs3 = np.linspace(1,100, 25) - rvs2 = np.linspace(1,100,100) - rvs1 = np.linspace(5,105,100) - rvs1_2D = np.array([rvs1, rvs2]) + t, p = stats.ttest_ind_from_stats(*_desc_stats(rvs1, rvs2), equal_var=False) + xp_assert_close(t, tr) + xp_assert_close(p, pr) - rvs2_2D = np.array([rvs2, rvs1]) + t, p = stats.ttest_ind(rvs1, rvs3, axis=0, equal_var=False) + xp_assert_close(t, tr_uneq_n) + xp_assert_close(p, pr_uneq_n) - t,p = stats.ttest_ind(rvs1, rvs2, axis=0, equal_var=False) - assert_array_almost_equal([t,p],(tr,pr)) - assert_array_almost_equal(stats.ttest_ind_from_stats(*_desc_stats(rvs1, - rvs2), - equal_var=False), - (t, p)) - - t,p = stats.ttest_ind(rvs1, rvs3, axis=0, equal_var=False) - assert_array_almost_equal([t,p], (tr_uneq_n, pr_uneq_n)) - assert_array_almost_equal(stats.ttest_ind_from_stats(*_desc_stats(rvs1, - rvs3), - equal_var=False), - (t, p)) - - t,p = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0, equal_var=False) - assert_array_almost_equal([t,p],tpr) - args = _desc_stats(rvs1_2D.T, rvs2_2D.T) - assert_array_almost_equal(stats.ttest_ind_from_stats(*args, - equal_var=False), - (t, p)) + t, p = stats.ttest_ind_from_stats(*_desc_stats(rvs1, rvs3), equal_var=False) + xp_assert_close(t, tr_uneq_n) + xp_assert_close(p, pr_uneq_n) - t,p = stats.ttest_ind(rvs1_2D, rvs2_2D, axis=1, equal_var=False) - assert_array_almost_equal([t,p],tpr) - args = _desc_stats(rvs1_2D, rvs2_2D, axis=1) - assert_array_almost_equal(stats.ttest_ind_from_stats(*args, - equal_var=False), - (t, p)) + res = stats.ttest_ind(rvs1_2D.T, rvs2_2D.T, axis=0, equal_var=False) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) - # test for namedtuple attribute results - attributes = ('statistic', 'pvalue') - res = stats.ttest_ind(rvs1, rvs2, axis=0, equal_var=False) - check_named_results(res, attributes) + args = _desc_stats(rvs1_2D.T, rvs2_2D.T) + res = stats.ttest_ind_from_stats(*args, equal_var=False) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) - # test on 3 dimensions - rvs1_3D = np.dstack([rvs1_2D,rvs1_2D,rvs1_2D]) - rvs2_3D = np.dstack([rvs2_2D,rvs2_2D,rvs2_2D]) - t,p = stats.ttest_ind(rvs1_3D, rvs2_3D, axis=1, equal_var=False) - assert_almost_equal(np.abs(t), np.abs(tr)) - assert_array_almost_equal(np.abs(p), pr) - assert_equal(t.shape, (2, 3)) - args = _desc_stats(rvs1_3D, rvs2_3D, axis=1) - t, p = stats.ttest_ind_from_stats(*args, equal_var=False) - assert_almost_equal(np.abs(t), np.abs(tr)) - assert_array_almost_equal(np.abs(p), pr) - assert_equal(t.shape, (2, 3)) + res = stats.ttest_ind(rvs1_2D, rvs2_2D, axis=1, equal_var=False) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) + + args = _desc_stats(rvs1_2D, rvs2_2D, axis=1) + res = stats.ttest_ind_from_stats(*args, equal_var=False) + xp_assert_close(res.statistic, tr_2D) + xp_assert_close(res.pvalue, pr_2D) - t, p = stats.ttest_ind(np.moveaxis(rvs1_3D, 2, 0), - np.moveaxis(rvs2_3D, 2, 0), - axis=2, equal_var=False) - assert_array_almost_equal(np.abs(t), np.abs(tr)) - assert_array_almost_equal(np.abs(p), pr) - assert_equal(t.shape, (3, 2)) - args = _desc_stats(np.moveaxis(rvs1_3D, 2, 0), - np.moveaxis(rvs2_3D, 2, 0), axis=2) - t, p = stats.ttest_ind_from_stats(*args, equal_var=False) - assert_array_almost_equal(np.abs(t), np.abs(tr)) - assert_array_almost_equal(np.abs(p), pr) - assert_equal(t.shape, (3, 2)) +@array_api_compatible +@pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) +@pytest.mark.usefixtures("skip_xp_backends") +def test_ttest_ind_zero_division(xp): # test zero division problem + x = xp.zeros(3) + y = xp.ones(3) with pytest.warns(RuntimeWarning, match="Precision loss occurred"): - t, p = stats.ttest_ind([0, 0, 0], [1, 1, 1], equal_var=False) - assert_equal((np.abs(t), p), (np.inf, 0)) + t, p = stats.ttest_ind(x, y, equal_var=False) + xp_assert_equal(t, xp.asarray(-xp.inf)) + xp_assert_equal(p, xp.asarray(0.)) + with np.errstate(all='ignore'): - assert_equal(stats.ttest_ind([0, 0, 0], [0, 0, 0], equal_var=False), - (np.nan, np.nan)) + t, p = stats.ttest_ind(x, x, equal_var=False) + xp_assert_equal(t, xp.asarray(xp.nan)) + xp_assert_equal(p, xp.asarray(xp.nan)) # check that nan in input array result in nan output - anan = np.array([[1, np.nan], [-1, 1]]) - assert_equal(stats.ttest_ind(anan, np.zeros((2, 2)), equal_var=False), - ([0, np.nan], [1, np.nan])) + anan = xp.asarray([[1, xp.nan], [-1, 1]]) + t, p = stats.ttest_ind(anan, xp.zeros((2, 2)), equal_var=False) + xp_assert_equal(t, xp.asarray([0., np.nan])) + xp_assert_equal(p, xp.asarray([1., np.nan])) def test_ttest_ind_nan_2nd_arg(): @@ -5820,90 +5944,126 @@ def test_ttest_ind_nan_2nd_arg(): atol=1e-15) -def test_ttest_ind_empty_1d_returns_nan(): +@array_api_compatible +def test_ttest_ind_empty_1d_returns_nan(xp): # Two empty inputs should return a TtestResult containing nan # for both values. - result = stats.ttest_ind([], []) - assert isinstance(result, stats._stats_py.TtestResult) - assert_equal(result, (np.nan, np.nan)) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.ttest_ind(xp.asarray([]), xp.asarray([])) + else: + res = stats.ttest_ind(xp.asarray([]), xp.asarray([])) + assert isinstance(res, stats._stats_py.TtestResult) + NaN = xp.asarray(xp.nan)[()] + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) +@array_api_compatible @pytest.mark.parametrize('b, expected_shape', [(np.empty((1, 5, 0)), (3, 5)), (np.empty((1, 0, 0)), (3, 0))]) -def test_ttest_ind_axis_size_zero(b, expected_shape): +def test_ttest_ind_axis_size_zero(b, expected_shape, xp): # In this test, the length of the axis dimension is zero. # The results should be arrays containing nan with shape # given by the broadcast nonaxis dimensions. - a = np.empty((3, 1, 0)) - result = stats.ttest_ind(a, b, axis=-1) - assert isinstance(result, stats._stats_py.TtestResult) - expected_value = np.full(expected_shape, fill_value=np.nan) - assert_equal(result.statistic, expected_value) - assert_equal(result.pvalue, expected_value) + a = xp.empty((3, 1, 0)) + b = xp.asarray(b) + with np.testing.suppress_warnings() as sup: + # first case should warn, second shouldn't? + sup.filter(SmallSampleWarning, too_small_nd_not_omit) + res = stats.ttest_ind(a, b, axis=-1) + assert isinstance(res, stats._stats_py.TtestResult) + expected_value = xp.full(expected_shape, fill_value=xp.nan) + xp_assert_equal(res.statistic, expected_value) + xp_assert_equal(res.pvalue, expected_value) -def test_ttest_ind_nonaxis_size_zero(): +@array_api_compatible +def test_ttest_ind_nonaxis_size_zero(xp): # In this test, the length of the axis dimension is nonzero, # but one of the nonaxis dimensions has length 0. Check that # we still get the correctly broadcast shape, which is (5, 0) # in this case. - a = np.empty((1, 8, 0)) - b = np.empty((5, 8, 1)) - result = stats.ttest_ind(a, b, axis=1) - assert isinstance(result, stats._stats_py.TtestResult) - assert_equal(result.statistic.shape, (5, 0)) - assert_equal(result.pvalue.shape, (5, 0)) + a = xp.empty((1, 8, 0)) + b = xp.empty((5, 8, 1)) + res = stats.ttest_ind(a, b, axis=1) + assert isinstance(res, stats._stats_py.TtestResult) + assert res.statistic.shape ==(5, 0) + assert res.pvalue.shape == (5, 0) -def test_ttest_ind_nonaxis_size_zero_different_lengths(): +@array_api_compatible +def test_ttest_ind_nonaxis_size_zero_different_lengths(xp): # In this test, the length of the axis dimension is nonzero, # and that size is different in the two inputs, # and one of the nonaxis dimensions has length 0. Check that # we still get the correctly broadcast shape, which is (5, 0) # in this case. - a = np.empty((1, 7, 0)) - b = np.empty((5, 8, 1)) - result = stats.ttest_ind(a, b, axis=1) - assert isinstance(result, stats._stats_py.TtestResult) - assert_equal(result.statistic.shape, (5, 0)) - assert_equal(result.pvalue.shape, (5, 0)) + a = xp.empty((1, 7, 0)) + b = xp.empty((5, 8, 1)) + res = stats.ttest_ind(a, b, axis=1) + assert isinstance(res, stats._stats_py.TtestResult) + assert res.statistic.shape ==(5, 0) + assert res.pvalue.shape == (5, 0) -def test_gh5686(): - mean1, mean2 = np.array([1, 2]), np.array([3, 4]) - std1, std2 = np.array([5, 3]), np.array([4, 5]) - nobs1, nobs2 = np.array([130, 140]), np.array([100, 150]) +@array_api_compatible +@pytest.mark.skip_xp_backends(np_only=True, + reasons=["Other backends don't like integers"]) +@pytest.mark.usefixtures("skip_xp_backends") +def test_gh5686(xp): + mean1, mean2 = xp.asarray([1, 2]), xp.asarray([3, 4]) + std1, std2 = xp.asarray([5, 3]), xp.asarray([4, 5]) + nobs1, nobs2 = xp.asarray([130, 140]), xp.asarray([100, 150]) # This will raise a TypeError unless gh-5686 is fixed. stats.ttest_ind_from_stats(mean1, std1, nobs1, mean2, std2, nobs2) -def test_ttest_ind_from_stats_inputs_zero(): +@array_api_compatible +@pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) +@pytest.mark.usefixtures("skip_xp_backends") +def test_ttest_ind_from_stats_inputs_zero(xp): # Regression test for gh-6409. - result = stats.ttest_ind_from_stats(0, 0, 6, 0, 0, 6, equal_var=False) - assert_equal(result, [np.nan, np.nan]) + zero = xp.asarray(0.) + six = xp.asarray(6.) + NaN = xp.asarray(xp.nan) + res = stats.ttest_ind_from_stats(zero, zero, six, zero, zero, six, equal_var=False) + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) -def test_ttest_single_observation(): +@array_api_compatible +@pytest.mark.skip_xp_backends(cpu_only=True, + reasons=['Uses NumPy for pvalue, CI']) +@pytest.mark.usefixtures("skip_xp_backends") +def test_ttest_uniform_pvalues(xp): # test that p-values are uniformly distributed under the null hypothesis rng = np.random.default_rng(246834602926842) - x = rng.normal(size=(10000, 2)) - y = rng.normal(size=(10000, 1)) + x = xp.asarray(rng.normal(size=(10000, 2))) + y = xp.asarray(rng.normal(size=(10000, 1))) q = rng.uniform(size=100) res = stats.ttest_ind(x, y, equal_var=True, axis=-1) - assert stats.ks_1samp(res.pvalue, stats.uniform().cdf).pvalue > 0.1 - assert_allclose(np.percentile(res.pvalue, q*100), q, atol=1e-2) + pvalue = np.asarray(res.pvalue) + assert stats.ks_1samp(pvalue, stats.uniform().cdf).pvalue > 0.1 + assert_allclose(np.quantile(pvalue, q), q, atol=1e-2) res = stats.ttest_ind(y, x, equal_var=True, axis=-1) - assert stats.ks_1samp(res.pvalue, stats.uniform().cdf).pvalue > 0.1 - assert_allclose(np.percentile(res.pvalue, q*100), q, atol=1e-2) + pvalue = np.asarray(res.pvalue) + assert stats.ks_1samp(pvalue, stats.uniform().cdf).pvalue > 0.1 + assert_allclose(np.quantile(pvalue, q), q, atol=1e-2) # reference values from R: # options(digits=16) # t.test(c(2, 3, 5), c(1.5), var.equal=TRUE) - res = stats.ttest_ind([2, 3, 5], [1.5], equal_var=True) - assert_allclose(res, (1.0394023007754, 0.407779907736), rtol=1e-10) + x, y = xp.asarray([2, 3, 5]), xp.asarray([1.5]) + + res = stats.ttest_ind(x, y, equal_var=True) + rtol = 1e-6 if is_torch(xp) else 1e-10 + xp_assert_close(res.statistic, xp.asarray(1.0394023007754), rtol=rtol) + xp_assert_close(res.pvalue, xp.asarray(0.407779907736), rtol=rtol) def _convert_pvalue_alternative(t, p, alt, xp): @@ -6189,11 +6349,19 @@ def test_describe_empty(self, xp): @array_api_compatible class NormalityTests: def test_too_small(self, xp): - # 1D sample has too few observations -> error + # 1D sample has too few observations -> warning/error test_fun = getattr(stats, self.test_name) - message = "...requires at least..." - with pytest.raises(ValueError, match=message): - test_fun(xp.asarray(4.)) + x = xp.asarray(4.) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = test_fun(x) + NaN = xp.asarray(xp.nan) + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) + else: + message = "...requires at least..." + with pytest.raises(ValueError, match=message): + test_fun(x) @pytest.mark.parametrize("alternative", ['two-sided', 'less', 'greater']) def test_against_R(self, alternative, xp): @@ -6251,9 +6419,19 @@ def test_skewtest_too_few_observations(self, xp): # Regression test for ticket #1492. # skewtest requires at least 8 observations; 7 should raise a ValueError. stats.skewtest(xp.arange(8.0)) - message = '`skewtest` requires at least 8 observations...' - with pytest.raises(ValueError, match=message): - stats.skewtest(xp.arange(7.0)) + + x = xp.arange(7.0) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.skewtest(x) + NaN = xp.asarray(xp.nan) + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) + else: + message = "`skewtest` requires at least 8 observations" + with pytest.raises(ValueError, match=message): + stats.skewtest(x) + class TestKurtosisTest(NormalityTests): test_name = 'kurtosistest' @@ -6286,9 +6464,17 @@ def test_kurtosistest_too_few_observations(self, xp): with pytest.warns(UserWarning, match=message): stats.kurtosistest(xp.arange(19.0)) - message = '`kurtosistest` requires at least 5 observations...' - with pytest.raises(ValueError, match=message): - stats.kurtosistest(xp.arange(4.0)) + x = xp.arange(4.0) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.skewtest(x) + NaN = xp.asarray(xp.nan) + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) + else: + message = "`kurtosistest` requires at least 5 observations" + with pytest.raises(ValueError, match=message): + stats.kurtosistest(x) class TestNormalTest(NormalityTests): @@ -6350,9 +6536,16 @@ def test_jarque_bera_array_like(self): def test_jarque_bera_size(self, xp): x = xp.asarray([]) - message = "At least one observation is required." - with pytest.raises(ValueError, match=message): - stats.jarque_bera(x) + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.jarque_bera(x) + NaN = xp.asarray(xp.nan) + xp_assert_equal(res.statistic, NaN) + xp_assert_equal(res.pvalue, NaN) + else: + message = "At least one observation is required." + with pytest.raises(ValueError, match=message): + res = stats.jarque_bera(x) def test_axis(self, xp): rng = np.random.RandomState(seed=122398129) @@ -7265,19 +7458,15 @@ def test_compare_dtypes(self): assert (res_int16.statistic == res_int32.statistic == res_unit8.statistic == res_float64.statistic) + @pytest.mark.parametrize('case',[([1, 2], []), ([1, 2], 2), ([1, 2], [2])]) + def test_too_small_inputs(self, case): + # input array is of size zero or too small + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + res = stats.alexandergovern(*case) + assert_equal(res.statistic, np.nan) + assert_equal(res.pvalue, np.nan) + def test_bad_inputs(self): - # input array is of size zero - with assert_raises(ValueError, match="Input sample size must be" - " greater than one."): - stats.alexandergovern([1, 2], []) - # input is a singular non list element - with assert_raises(ValueError, match="Input sample size must be" - " greater than one."): - stats.alexandergovern([1, 2], 2) - # input list is of size 1 - with assert_raises(ValueError, match="Input sample size must be" - " greater than one."): - stats.alexandergovern([1, 2], [2]) # inputs are not finite (infinity) with assert_raises(ValueError, match="Input samples must be finite."): stats.alexandergovern([1, 2], [np.inf, np.inf]) @@ -7628,14 +7817,12 @@ def test_3d_inputs(self): def test_length0_1d_error(self): # Require at least one value in each group. - msg = 'at least one input has length 0' - with pytest.warns(stats.DegenerateDataWarning, match=msg): + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): result = stats.f_oneway([1, 2, 3], [], [4, 5, 6, 7]) assert_equal(result, (np.nan, np.nan)) def test_length0_2d_error(self): - msg = 'at least one input has length 0' - with pytest.warns(stats.DegenerateDataWarning, match=msg): + with pytest.warns(SmallSampleWarning, match=too_small_nd_not_omit): ncols = 3 a = np.ones((4, ncols)) b = np.ones((0, ncols)) @@ -7646,14 +7833,14 @@ def test_length0_2d_error(self): assert_equal(p, nans) def test_all_length_one(self): - msg = 'all input arrays have length 1.' - with pytest.warns(stats.DegenerateDataWarning, match=msg): + with pytest.warns(SmallSampleWarning): result = stats.f_oneway([10], [11], [12], [13]) assert_equal(result, (np.nan, np.nan)) @pytest.mark.parametrize('args', [(), ([1, 2, 3],)]) def test_too_few_inputs(self, args): - with assert_raises(TypeError): + message = "At least two samples are required..." + with assert_raises(TypeError, match=message): stats.f_oneway(*args) def test_axis_error(self): @@ -7727,7 +7914,8 @@ def test_empty(self): x = [1, 1, 1] y = [2, 2, 2] z = [] - assert_equal(stats.kruskal(x, y, z), (np.nan, np.nan)) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + assert_equal(stats.kruskal(x, y, z), (np.nan, np.nan)) def test_kruskal_result_attributes(self): x = [1, 3, 5, 7, 9] @@ -7759,54 +7947,59 @@ def test_no_args_gh20661(self): stats.kruskal() +@array_api_compatible class TestCombinePvalues: - def test_fisher(self): + def test_fisher(self, xp): # Example taken from https://en.wikipedia.org/wiki/Fisher%27s_exact_test#Example - xsq, p = stats.combine_pvalues([.01, .2, .3], method='fisher') - assert_approx_equal(p, 0.02156, significant=4) - - def test_stouffer(self): - Z, p = stats.combine_pvalues([.01, .2, .3], method='stouffer') - assert_approx_equal(p, 0.01651, significant=4) - - def test_stouffer2(self): - Z, p = stats.combine_pvalues([.5, .5, .5], method='stouffer') - assert_approx_equal(p, 0.5, significant=4) - - def test_weighted_stouffer(self): - Z, p = stats.combine_pvalues([.01, .2, .3], method='stouffer', - weights=np.ones(3)) - assert_approx_equal(p, 0.01651, significant=4) - - def test_weighted_stouffer2(self): - Z, p = stats.combine_pvalues([.01, .2, .3], method='stouffer', - weights=np.array((1, 4, 9))) - assert_approx_equal(p, 0.1464, significant=4) - - def test_pearson(self): - Z, p = stats.combine_pvalues([.01, .2, .3], method='pearson') - assert_approx_equal(p, 0.02213, significant=4) - - def test_tippett(self): - Z, p = stats.combine_pvalues([.01, .2, .3], method='tippett') - assert_approx_equal(p, 0.0297, significant=4) - - def test_mudholkar_george(self): - Z, p = stats.combine_pvalues([.1, .1, .1], method='mudholkar_george') - assert_approx_equal(p, 0.019462, significant=4) - - def test_mudholkar_george_equal_fisher_pearson_average(self): - Z, p = stats.combine_pvalues([.01, .2, .3], method='mudholkar_george') - Z_f, p_f = stats.combine_pvalues([.01, .2, .3], method='fisher') - Z_p, p_p = stats.combine_pvalues([.01, .2, .3], method='pearson') - assert_approx_equal(0.5 * (Z_f+Z_p), Z, significant=4) + xsq, p = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method='fisher') + xp_assert_close(p, xp.asarray(0.02156), rtol=1e-4) + + def test_stouffer(self, xp): + Z, p = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method='stouffer') + xp_assert_close(p, xp.asarray(0.01651), rtol=1e-3) + + def test_stouffer2(self, xp): + Z, p = stats.combine_pvalues(xp.asarray([.5, .5, .5]), method='stouffer') + xp_assert_close(p, xp.asarray(0.5), rtol=1e-4) + + def test_weighted_stouffer(self, xp): + pvalues = xp.asarray([.01, .2, .3]) + Z, p = stats.combine_pvalues(pvalues, method='stouffer', + weights=xp.ones(3, dtype=pvalues.dtype)) + xp_assert_close(p, xp.asarray(0.01651), rtol=1e-3) + + def test_weighted_stouffer2(self, xp): + Z, p = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method='stouffer', + weights=xp.asarray([1., 4., 9.])) + xp_assert_close(p, xp.asarray(0.1464), rtol=1e-3) + + def test_pearson(self, xp): + Z, p = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method='pearson') + xp_assert_close(p, xp.asarray(0.02213), rtol=1e-3) + + def test_tippett(self, xp): + Z, p = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method='tippett') + xp_assert_close(p, xp.asarray(0.0297), rtol=1e-4) + + def test_mudholkar_george(self, xp): + Z, p = stats.combine_pvalues(xp.asarray([.1, .1, .1]), + method='mudholkar_george') + xp_assert_close(p, xp.asarray(0.019462), rtol=1e-4) + + def test_mudholkar_george_equal_fisher_pearson_average(self, xp): + Z, p = stats.combine_pvalues(xp.asarray([.01, .2, .3]), + method='mudholkar_george') + Z_f, p_f = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method='fisher') + Z_p, p_p = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method='pearson') + xp_assert_close(0.5 * (Z_f+Z_p), Z, rtol=1e-4) methods = ["fisher", "pearson", "tippett", "stouffer", "mudholkar_george"] @pytest.mark.parametrize("variant", ["single", "all", "random"]) @pytest.mark.parametrize("method", methods) - def test_monotonicity(self, variant, method): + def test_monotonicity(self, variant, method, xp): + xp_test = array_namespace(xp.asarray(1)) # Test that result increases monotonically with respect to input. m, n = 10, 7 rng = np.random.default_rng(278448169958891062669391462690811630763) @@ -7816,23 +8009,25 @@ def test_monotonicity(self, variant, method): # monotonically down one column (single), simultaneously down each # column (all), or independently down each column (random). if variant == "single": - pvaluess = np.full((m, n), rng.random(n)) - pvaluess[:, 0] = np.linspace(0.1, 0.9, m) + pvaluess = xp.broadcast_to(xp.asarray(rng.random(n)), (m, n)) + pvaluess = xp_test.concat([xp.reshape(xp.linspace(0.1, 0.9, m), (-1, 1)), + pvaluess[:, 1:]], axis=1) elif variant == "all": - pvaluess = np.full((n, m), np.linspace(0.1, 0.9, m)).T + pvaluess = xp.broadcast_to(xp.linspace(0.1, 0.9, m), (n, m)).T elif variant == "random": - pvaluess = np.sort(rng.uniform(0, 1, size=(m, n)), axis=0) + pvaluess = xp_test.sort(xp.asarray(rng.uniform(0, 1, size=(m, n))), axis=0) - combined_pvalues = [ - stats.combine_pvalues(pvalues, method=method)[1] - for pvalues in pvaluess - ] - assert np.all(np.diff(combined_pvalues) >= 0) + combined_pvalues = xp.asarray([ + stats.combine_pvalues(pvaluess[i, :], method=method)[1] + for i in range(pvaluess.shape[0]) + ]) + assert xp.all(combined_pvalues[1:] - combined_pvalues[:-1] >= 0) @pytest.mark.parametrize("method", methods) - def test_result(self, method): - res = stats.combine_pvalues([.01, .2, .3], method=method) - assert_equal((res.statistic, res.pvalue), res) + def test_result(self, method, xp): + res = stats.combine_pvalues(xp.asarray([.01, .2, .3]), method=method) + xp_assert_equal(res.statistic, res[0]) + xp_assert_equal(res.pvalue, res[1]) class TestCdfDistanceValidation: @@ -8301,17 +8496,15 @@ def test_brunnermunzel_distribution_error(self): distribution, nan_policy) - def test_brunnermunzel_empty_imput(self): - u1, p1 = stats.brunnermunzel(self.X, []) - u2, p2 = stats.brunnermunzel([], self.Y) - u3, p3 = stats.brunnermunzel([], []) - - assert_equal(u1, np.nan) - assert_equal(p1, np.nan) - assert_equal(u2, np.nan) - assert_equal(p2, np.nan) - assert_equal(u3, np.nan) - assert_equal(p3, np.nan) + @pytest.mark.parametrize("kwarg_update", [{'y': []}, {'x': []}, + {'x': [], 'y': []}]) + def test_brunnermunzel_empty_imput(self, kwarg_update): + kwargs = {'x': self.X, 'y': self.Y} + kwargs.update(kwarg_update) + with pytest.warns(SmallSampleWarning, match=too_small_1d_not_omit): + statistic, pvalue = stats.brunnermunzel(**kwargs) + assert_equal(statistic, np.nan) + assert_equal(pvalue, np.nan) def test_brunnermunzel_nan_input_propagate(self): X = [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 1, 1, np.nan] @@ -8383,24 +8576,6 @@ def test_brunnermunzel_normal_dist(self): assert_equal(p, 0) -class TestRatioUniforms: - """ Tests for rvs_ratio_uniforms are in test_sampling.py, - as rvs_ratio_uniforms is deprecated and moved to stats.sampling """ - def test_consistency(self): - f = stats.norm.pdf - v = np.sqrt(f(np.sqrt(2))) * np.sqrt(2) - umax = np.sqrt(f(0)) - gen = stats.sampling.RatioUniforms(f, umax=umax, vmin=-v, vmax=v, - random_state=12345) - r1 = gen.rvs(10) - deprecation_msg = ("Please use `RatioUniforms` from the " - "`scipy.stats.sampling` namespace.") - with pytest.warns(DeprecationWarning, match=deprecation_msg): - r2 = stats.rvs_ratio_uniforms(f, umax, -v, v, size=10, - random_state=12345) - assert_equal(r1, r2) - - class TestQuantileTest: r""" Test the non-parametric quantile test, including the computation of confidence intervals diff --git a/scipy/stats/tests/test_variation.py b/scipy/stats/tests/test_variation.py index e13287fca916..229237090916 100644 --- a/scipy/stats/tests/test_variation.py +++ b/scipy/stats/tests/test_variation.py @@ -7,7 +7,9 @@ from scipy.stats import variation from scipy._lib._util import AxisError from scipy.conftest import array_api_compatible -from scipy._lib._array_api import xp_assert_equal, xp_assert_close +from scipy._lib._array_api import xp_assert_equal, xp_assert_close, is_numpy +from scipy.stats._axis_nan_policy import (too_small_nd_omit, too_small_nd_not_omit, + SmallSampleWarning) pytestmark = [array_api_compatible, pytest.mark.usefixtures("skip_xp_backends")] skip_xp_backends = pytest.mark.skip_xp_backends @@ -72,7 +74,11 @@ def test_keepdims(self, xp): (1, np.full((5, 1), fill_value=np.nan))]) def test_keepdims_size0(self, axis, expected, xp): x = xp.zeros((5, 0)) - y = variation(x, axis=axis, keepdims=True) + if axis == 1: + with pytest.warns(SmallSampleWarning, match=too_small_nd_not_omit): + y = variation(x, axis=axis, keepdims=True) + else: + y = variation(x, axis=axis, keepdims=True) xp_assert_equal(y, expected) @skip_xp_backends(np_only=True, @@ -117,14 +123,11 @@ def test_mean_zero(self, xp): y2 = variation(x2, axis=1) xp_assert_equal(y2, xp.asarray([xp.inf, xp.inf])) - @pytest.mark.parametrize('x', [[0.]*5, [], [1, 2, np.inf, 9]]) + @pytest.mark.parametrize('x', [[0.]*5, [1, 2, np.inf, 9]]) def test_return_nan(self, x, xp): x = xp.asarray(x) # Test some cases where `variation` returns nan. - with suppress_warnings() as sup: - # torch - sup.filter(UserWarning, "std*") - y = variation(x) + y = variation(x) xp_assert_equal(y, xp.asarray(xp.nan, dtype=x.dtype)) @pytest.mark.parametrize('axis, expected', @@ -134,7 +137,14 @@ def test_2d_size_zero_with_axis(self, axis, expected, xp): with suppress_warnings() as sup: # torch sup.filter(UserWarning, "std*") - y = variation(x, axis=axis) + if axis != 0: + if is_numpy(xp): + with pytest.warns(SmallSampleWarning, match="See documentation..."): + y = variation(x, axis=axis) + else: + y = variation(x, axis=axis) + else: + y = variation(x, axis=axis) xp_assert_equal(y, xp.asarray(expected)) def test_neg_inf(self, xp): @@ -158,7 +168,11 @@ def test_combined_edge_cases(self, nan_policy, xp): x = xp.array([[0, 10, xp.nan, 1], [0, -5, xp.nan, 2], [0, -5, xp.nan, 3]]) - y = variation(x, axis=0, nan_policy=nan_policy) + if nan_policy == 'omit': + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + y = variation(x, axis=0, nan_policy=nan_policy) + else: + y = variation(x, axis=0, nan_policy=nan_policy) xp_assert_close(y, [xp.nan, xp.inf, xp.nan, math.sqrt(2/3)/2]) @skip_xp_backends(np_only=True, @@ -173,7 +187,7 @@ def test_more_nan_policy_omit_tests(self, ddof, expected, xp): # The slightly strange formatting in the follow array is my attempt to # maintain a clean tabular arrangement of the data while satisfying # the demands of pycodestyle. Currently, E201 and E241 are not - # disabled by the `# noqa` annotation. + # disabled by the `noqa` annotation. nan = xp.nan x = xp.asarray([[1.0, 2.0, nan, 3.0], [0.0, 4.0, 3.0, 1.0], @@ -182,7 +196,8 @@ def test_more_nan_policy_omit_tests(self, ddof, expected, xp): [nan, nan, nan, nan], [3.0, 3.0, 3.0, 3.0], [0.0, 0.0, 0.0, 0.0]]) - v = variation(x, axis=1, ddof=ddof, nan_policy='omit') + with pytest.warns(SmallSampleWarning, match=too_small_nd_omit): + v = variation(x, axis=1, ddof=ddof, nan_policy='omit') xp_assert_close(v, expected) @skip_xp_backends(np_only=True, diff --git a/tools/openblas_support.py b/tools/openblas_support.py deleted file mode 100644 index 37b60dd69e55..000000000000 --- a/tools/openblas_support.py +++ /dev/null @@ -1,410 +0,0 @@ -import glob -import os -import platform -import sysconfig -import sys -import shutil -import tarfile -import textwrap -import time -import zipfile - -from tempfile import mkstemp, gettempdir -from urllib.request import urlopen, Request -from urllib.error import HTTPError - -OPENBLAS_V = '0.3.27' -OPENBLAS_LONG = 'v0.3.27' -BASE_LOC = 'https://anaconda.org/multibuild-wheels-staging/openblas-libs' -NIGHTLY_BASE_LOC = ( - 'https://anaconda.org/scientific-python-nightly-wheels/openblas-libs' -) - -SUPPORTED_PLATFORMS = [ - 'linux-aarch64', - 'linux-x86_64', - 'musllinux-x86_64', - 'linux-i686', - 'linux-ppc64le', - 'linux-s390x', - 'win-amd64', - 'win-32', - 'macosx-x86_64', - 'macosx-arm64', -] -IS_32BIT = sys.maxsize < 2**32 - - -def get_plat(): - plat = sysconfig.get_platform() - plat_split = plat.split("-") - - arch = plat_split[-1] - if arch == "win32": - plat = "win-32" - elif arch in ["universal2", "intel"]: - plat = f"macosx-{platform.uname().machine}" - elif len(plat_split) > 2: - plat = f"{plat_split[0]}-{arch}" - assert plat in SUPPORTED_PLATFORMS, f'invalid platform {plat}' - return plat - - -def get_ilp64(): - if os.environ.get("NPY_USE_BLAS_ILP64", "0") == "0": - return None - if IS_32BIT: - raise RuntimeError("NPY_USE_BLAS_ILP64 set on 32-bit arch") - return "64_" - - -def get_manylinux(arch): - default = '2014' - ml_ver = os.environ.get("MB_ML_VER", default) - # XXX For PEP 600 this can be a glibc version - assert ml_ver in ('2010', '2014', '_2_24'), f'invalid MB_ML_VER {ml_ver}' - suffix = f'manylinux{ml_ver}_{arch}.tar.gz' - return suffix - - -def get_musllinux(arch): - musl_ver = "1_1" - suffix = f'musllinux_{musl_ver}_{arch}.tar.gz' - return suffix - - -def get_linux(arch): - # best way of figuring out whether manylinux or musllinux is to look - # at the packaging tags. If packaging isn't installed (it's not by default) - # fallback to sysconfig (which may be flakier) - try: - from packaging.tags import sys_tags - tags = list(sys_tags()) - plat = tags[0].platform - except ImportError: - # fallback to sysconfig for figuring out if you're using musl - plat = 'manylinux' - # value could be None - v = sysconfig.get_config_var('HOST_GNU_TYPE') or '' - if 'musl' in v: - plat = 'musllinux' - - if 'manylinux' in plat: - return get_manylinux(arch) - elif 'musllinux' in plat: - return get_musllinux(arch) - - -def download_openblas( - target, plat, ilp64, *, openblas_version=OPENBLAS_LONG, base_loc=BASE_LOC -): - osname, arch = plat.split("-") - fnsuffix = {None: "", "64_": "64_"}[ilp64] - filename = '' - headers = {'User-Agent': - ('Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 ; ' - '(KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.3')} - suffix = None - if osname == "linux": - suffix = get_linux(arch) - typ = 'tar.gz' - elif plat == 'macosx-x86_64': - suffix = 'macosx_10_9_x86_64-gf_c469a42.tar.gz' - typ = 'tar.gz' - elif plat == 'macosx-arm64': - suffix = 'macosx_11_0_arm64-gf_5272328.tar.gz' - typ = 'tar.gz' - elif osname == 'win': - if plat == "win-32": - suffix = 'win32-gcc_8_3_0.zip' - else: - suffix = 'win_amd64-gcc_10_3_0.zip' - typ = 'zip' - - if not suffix: - return None - BASEURL = f'{base_loc}/{openblas_version}/download' - filename = f'{BASEURL}/openblas{fnsuffix}-{openblas_version}-{suffix}' - req = Request(url=filename, headers=headers) - - for _ in range(3): - try: - time.sleep(1) - response = urlopen(req) - break - except HTTPError: - print(f'Could not download "{filename}"', file=sys.stderr) - raise - - length = response.getheader('content-length') - if response.status != 200: - print(f'Could not download "{filename}"', file=sys.stderr) - return None - print(f"Downloading {length} from {filename}", file=sys.stderr) - data = response.read() - print("Saving to file", file=sys.stderr) - with open(target, 'wb') as fid: - fid.write(data) - return typ - - -def setup_openblas(plat=get_plat(), ilp64=get_ilp64(), nightly=False): - ''' - Download and setup an openblas library for building. If successful, - the configuration script will find it automatically. - - Returns - ------- - msg : str - path to extracted files on success, otherwise indicates what went wrong - To determine success, do ``os.path.exists(msg)`` - ''' - fd, tmp = mkstemp() - os.close(fd) - if not plat: - raise ValueError('unknown platform') - openblas_version = "HEAD" if nightly else OPENBLAS_LONG - base_loc = NIGHTLY_BASE_LOC if nightly else BASE_LOC - typ = download_openblas( - tmp, plat, ilp64, openblas_version=openblas_version, base_loc=base_loc - ) - if not typ: - return '' - osname, arch = plat.split("-") - if osname == 'win': - if not typ == 'zip': - return f'expecting to download zipfile on windows, not {typ}' - return unpack_windows_zip(tmp) - else: - if not typ == 'tar.gz': - return 'expecting to download tar.gz, not %s' % str(typ) - return unpack_targz(tmp) - - -def unpack_windows_zip(fname): - with zipfile.ZipFile(fname, 'r') as zf: - # Get the openblas.a file, but not openblas.dll.a nor openblas.dev.a - lib = [x for x in zf.namelist() if OPENBLAS_LONG in x and - x.endswith('a') and not x.endswith('dll.a') and - not x.endswith('dev.a')] - if not lib: - return 'could not find libopenblas_%s*.a ' \ - 'in downloaded zipfile' % OPENBLAS_LONG - if get_ilp64() is None: - target = os.path.join(gettempdir(), 'openblas.a') - else: - target = os.path.join(gettempdir(), 'openblas64_.a') - with open(target, 'wb') as fid: - fid.write(zf.read(lib[0])) - return target - - -def unpack_targz(fname): - target = os.path.join(gettempdir(), 'openblas') - if not os.path.exists(target): - os.mkdir(target) - with tarfile.open(fname, 'r') as zf: - # Strip common prefix from paths when unpacking - prefix = os.path.commonpath(zf.getnames()) - extract_tarfile_to(zf, target, prefix) - return target - - -def extract_tarfile_to(tarfileobj, target_path, archive_path): - """Extract TarFile contents under archive_path/ to target_path/""" - - target_path = os.path.abspath(target_path) - - def get_members(): - for member in tarfileobj.getmembers(): - if archive_path: - norm_path = os.path.normpath(member.name) - if norm_path.startswith(archive_path + os.path.sep): - member.name = norm_path[len(archive_path)+1:] - else: - continue - - dst_path = os.path.abspath(os.path.join(target_path, member.name)) - if os.path.commonpath([target_path, dst_path]) != target_path: - # Path not under target_path, probably contains ../ - continue - - yield member - - tarfileobj.extractall(target_path, members=get_members()) - reformat_pkg_file(target_path=target_path) - - -def reformat_pkg_file(target_path): - # attempt to deal with: - # https://github.com/scipy/scipy/pull/20362#issuecomment-2028517797 - for root, dirs, files in os.walk(target_path): - for name in files: - if name.endswith(".pc") and "openblas" in name: - pkg_path = os.path.join(root, name) - new_pkg_lines = [] - with open(pkg_path) as pkg_orig: - for line in pkg_orig: - if line.startswith("Libs:"): - new_line = line.replace("$(libprefix}", "${libprefix}") - new_pkg_lines.append(new_line) - else: - new_pkg_lines.append(line) - with open(pkg_path, "w") as new_pkg: - new_pkg.writelines(new_pkg_lines) - - -def make_init(dirname): - ''' - Create a _distributor_init.py file for OpenBlas - ''' - with open(os.path.join(dirname, '_distributor_init.py'), 'w') as fid: - fid.write(textwrap.dedent(""" - ''' - Helper to preload windows dlls to prevent dll not found errors. - Once a DLL is preloaded, its namespace is made available to any - subsequent DLL. This file originated in the numpy-wheels repo, - and is created as part of the scripts that build the wheel. - ''' - import os - import glob - if os.name == 'nt': - # convention for storing / loading the DLL from - # numpy/.libs/, if present - try: - from ctypes import WinDLL - basedir = os.path.dirname(__file__) - except: - pass - else: - libs_dir = os.path.abspath(os.path.join(basedir, '.libs')) - DLL_filenames = [] - if os.path.isdir(libs_dir): - for filename in glob.glob(os.path.join(libs_dir, - '*openblas*dll')): - # NOTE: would it change behavior to load ALL - # DLLs at this path vs. the name restriction? - WinDLL(os.path.abspath(filename)) - DLL_filenames.append(filename) - if len(DLL_filenames) > 1: - import warnings - warnings.warn("loaded more than 1 DLL from .libs:" - "\\n%s" % "\\n".join(DLL_filenames), - stacklevel=1) - """)) - - -def test_setup(plats): - ''' - Make sure all the downloadable files exist and can be opened - ''' - def items(): - """ yields all combinations of arch, ilp64 - """ - for plat in plats: - yield plat, None - osname, arch = plat.split("-") - if arch not in ('i686', 'arm64', '32'): - yield plat, '64_' - if osname == "linux" and arch in ('i686', 'x86_64'): - oldval = os.environ.get('MB_ML_VER', None) - os.environ['MB_ML_VER'] = '1' - yield plat, None - # Once we create x86_64 and i686 manylinux2014 wheels... - # os.environ['MB_ML_VER'] = '2014' - # yield arch, None, False - if oldval: - os.environ['MB_ML_VER'] = oldval - else: - os.environ.pop('MB_ML_VER') - - errs = [] - for plat, ilp64 in items(): - osname, _ = plat.split("-") - if plat not in plats: - continue - target = None - try: - try: - target = setup_openblas(plat, ilp64) - except Exception as e: - print(f'Could not setup {plat} with ilp64 {ilp64}, ') - print(e) - errs.append(e) - continue - if not target: - raise RuntimeError(f'Could not setup {plat}') - print(target) - if osname == 'win': - if not target.endswith('.a'): - raise RuntimeError("Not .a extracted!") - else: - files = glob.glob(os.path.join(target, "lib", "*.a")) - if not files: - raise RuntimeError("No lib/*.a unpacked!") - finally: - if target is not None: - if os.path.isfile(target): - os.unlink(target) - else: - shutil.rmtree(target) - if errs: - raise errs[0] - - -def test_version(expected_version, ilp64=get_ilp64()): - """ - Assert that expected OpenBLAS version is - actually available via SciPy - """ - import scipy - import scipy.linalg - import ctypes - - dll = ctypes.CDLL(scipy.linalg.cython_blas.__file__) - if ilp64 == "64_": - get_config = dll.openblas_get_config64_ - else: - get_config = dll.openblas_get_config - get_config.restype = ctypes.c_char_p - res = get_config() - print('OpenBLAS get_config returned', str(res)) - if not expected_version: - expected_version = OPENBLAS_V - check_str = b'OpenBLAS %s' % expected_version.encode() - print(check_str) - assert check_str in res, f'{expected_version} not found in {res}' - if ilp64: - assert b"USE64BITINT" in res - else: - assert b"USE64BITINT" not in res - - -if __name__ == '__main__': - import argparse - parser = argparse.ArgumentParser( - description='Download and expand an OpenBLAS archive for this ' - 'architecture') - parser.add_argument('--test', nargs='*', default=None, - help='Test different architectures. "all", or any of ' - f'{SUPPORTED_PLATFORMS}') - parser.add_argument('--write-init', nargs=1, - metavar='OUT_SCIPY_DIR', - help='Write distribution init to named dir') - parser.add_argument('--check_version', nargs='?', default='', - help='Check provided OpenBLAS version string ' - 'against available OpenBLAS') - parser.add_argument('--nightly', action='store_true', - help='If set, use nightly OpenBLAS build.') - args = parser.parse_args() - if args.check_version != '': - test_version(args.check_version) - elif args.write_init: - make_init(args.write_init[0]) - elif args.test is None: - print(setup_openblas(nightly=args.nightly)) - else: - if len(args.test) == 0 or 'all' in args.test: - test_setup(SUPPORTED_PLATFORMS) - else: - test_setup(args.test) diff --git a/tools/version_utils.py b/tools/version_utils.py index cea0e0af4b63..fbf490bcdf37 100644 --- a/tools/version_utils.py +++ b/tools/version_utils.py @@ -5,7 +5,7 @@ MAJOR = 1 -MINOR = 14 +MINOR = 15 MICRO = 0 ISRELEASED = False IS_RELEASE_BRANCH = False diff --git a/tools/wheels/cibw_before_build_linux.sh b/tools/wheels/cibw_before_build_linux.sh index 2bae0f5e75b1..66e8c2265142 100755 --- a/tools/wheels/cibw_before_build_linux.sh +++ b/tools/wheels/cibw_before_build_linux.sh @@ -13,13 +13,22 @@ else exit 1 fi -PLATFORM=$(PYTHONPATH=tools python -c "import openblas_support; print(openblas_support.get_plat())") - printenv # Update license cat $PROJECT_DIR/tools/wheels/LICENSE_linux.txt >> $PROJECT_DIR/LICENSE.txt +# TODO: delete along with enabling build isolation by unsetting +# CIBW_BUILD_FRONTEND when scipy is buildable under free-threaded +# python with a released version of cython +FREE_THREADED_BUILD="$(python -c"import sysconfig; print(bool(sysconfig.get_config_var('Py_GIL_DISABLED')))")" +if [[ $FREE_THREADED_BUILD == "True" ]]; then + python -m pip install -U --pre pip + python -m pip install git+https://github.com/cython/cython + python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy + # python -m pip install git+https://github.com/serge-sans-paille/pythran + python -m pip install ninja meson-python pybind11 pythran +fi + # Install Openblas -basedir=$(python tools/openblas_support.py $NIGHTLY_FLAG) -cp -r $basedir/lib/* /usr/local/lib -cp $basedir/include/* /usr/local/include +python -m pip install -r requirements/openblas.txt +python -c "import scipy_openblas32; print(scipy_openblas32.get_pkg_config())" > $PROJECT_DIR/scipy-openblas.pc diff --git a/tools/wheels/cibw_before_build_macos.sh b/tools/wheels/cibw_before_build_macos.sh index 1a664a309083..945ff19ba935 100644 --- a/tools/wheels/cibw_before_build_macos.sh +++ b/tools/wheels/cibw_before_build_macos.sh @@ -1,7 +1,7 @@ set -xe PROJECT_DIR="$1" -PLATFORM=$(PYTHONPATH=tools python -c "import openblas_support; print(openblas_support.get_plat())") +PLATFORM=$(uname -m) echo $PLATFORM # Update license @@ -10,17 +10,11 @@ cat $PROJECT_DIR/tools/wheels/LICENSE_osx.txt >> $PROJECT_DIR/LICENSE.txt ######################################################################################### # Install GFortran + OpenBLAS -if [[ $PLATFORM == "macosx-x86_64" ]]; then - # Openblas - basedir=$(python tools/openblas_support.py) - - # copy over the OpenBLAS library stuff first - cp -r $basedir/lib/* /usr/local/lib - cp $basedir/include/* /usr/local/include - +if [[ $PLATFORM == "x86_64" ]]; then #GFORTRAN=$(type -p gfortran-9) #sudo ln -s $GFORTRAN /usr/local/bin/gfortran - # same version of gfortran as the openblas-libs and scipy-wheel builds + # same version of gfortran as the openblas-libs + # https://github.com/MacPython/gfortran-install.git curl -L https://github.com/isuruf/gcc/releases/download/gcc-11.3.0-2/gfortran-darwin-x86_64-native.tar.gz -o gfortran.tar.gz GFORTRAN_SHA256=$(shasum -a 256 gfortran.tar.gz) @@ -49,21 +43,8 @@ if [[ $PLATFORM == "macosx-x86_64" ]]; then export SDKROOT=${SDKROOT:-$(xcrun --show-sdk-path)} fi -if [[ $PLATFORM == "macosx-arm64" ]]; then - # OpenBLAS - # need a version of OpenBLAS that is suited for gcc >= 11 - basedir=$(python tools/openblas_support.py) - - # use /opt/arm64-builds as a prefix, because that's what the multibuild - # OpenBLAS pkgconfig files state - sudo mkdir -p /opt/arm64-builds/lib - sudo mkdir -p /opt/arm64-builds/include - sudo cp -r $basedir/lib/* /opt/arm64-builds/lib - sudo cp $basedir/include/* /opt/arm64-builds/include - - # we want to force a dynamic linking - sudo rm /opt/arm64-builds/lib/*.a +if [[ $PLATFORM == "arm64" ]]; then curl -L https://github.com/fxcoudert/gfortran-for-macOS/releases/download/12.1-monterey/gfortran-ARM-12.1-Monterey.dmg -o gfortran.dmg GFORTRAN_SHA256=$(shasum -a 256 gfortran.dmg) KNOWN_SHA256="e2e32f491303a00092921baebac7ffb7ae98de4ca82ebbe9e6a866dd8501acdf gfortran.dmg" @@ -77,3 +58,18 @@ if [[ $PLATFORM == "macosx-arm64" ]]; then sudo installer -pkg /Volumes/gfortran/gfortran.pkg -target / type -p gfortran fi + + +# Install Openblas +python -m pip install -r requirements/openblas.txt +python -c "import scipy_openblas32; print(scipy_openblas32.get_pkg_config())" > $PROJECT_DIR/scipy-openblas.pc + +lib_loc=$(python -c"import scipy_openblas32; print(scipy_openblas32.get_lib_dir())") +# Use the libgfortran from gfortran rather than the one in the wheel +# since delocate gets confused if there is more than one +# https://github.com/scipy/scipy/issues/20852 +install_name_tool -change @loader_path/../.dylibs/libgfortran.5.dylib @rpath/libgfortran.5.dylib $lib_loc/libsci* +install_name_tool -change @loader_path/../.dylibs/libgcc_s.1.1.dylib @rpath/libgcc_s.1.1.dylib $lib_loc/libsci* +install_name_tool -change @loader_path/../.dylibs/libquadmath.0.dylib @rpath/libquadmath.0.dylib $lib_loc/libsci* + +codesign -s - -f $lib_loc/libsci* diff --git a/tools/wheels/cibw_before_build_win.sh b/tools/wheels/cibw_before_build_win.sh index 63c186f0a989..69b4e7984906 100644 --- a/tools/wheels/cibw_before_build_win.sh +++ b/tools/wheels/cibw_before_build_win.sh @@ -1,39 +1,14 @@ set -xe PROJECT_DIR="$1" -PLATFORM=$(PYTHONPATH=tools python -c "import openblas_support; print(openblas_support.get_plat())") printenv # Update license cat $PROJECT_DIR/tools/wheels/LICENSE_win32.txt >> $PROJECT_DIR/LICENSE.txt # Install Openblas -PYTHONPATH=tools python -c "import openblas_support; openblas_support.make_init('scipy')" -mkdir -p /c/opt/32/lib/pkgconfig -mkdir -p /c/opt/64/lib/pkgconfig +python -m pip install -r requirements/openblas.txt +python -c "import scipy_openblas32; print(scipy_openblas32.get_pkg_config())" > $PROJECT_DIR/scipy-openblas.pc # delvewheel is the equivalent of delocate/auditwheel for windows. python -m pip install delvewheel - -# make the DLL available for tools/wheels/repair_windows.sh. If you change -# this location you need to alter that script. -mkdir -p /c/opt/openblas/openblas_dll -which strip - -target=$(python -c "import tools.openblas_support as obs; plat=obs.get_plat(); ilp64=obs.get_ilp64(); target=f'openblas_{plat}.zip'; obs.download_openblas(target, plat, ilp64);print(target)") - -# The 32/64 bit Fortran wheels are currently coming from different locations. -if [[ $PLATFORM == 'win-32' ]]; then - # 32-bit openBLAS - # Download 32 bit openBLAS and put it into c/opt/32/lib - unzip $target -d /c/opt/ - cp /c/opt/32/bin/*.dll /c/opt/openblas/openblas_dll - # rm /c/opt/openblas/if_32/32/lib/*.dll.a -else - # 64-bit openBLAS - unzip $target -d /c/opt/ - cp /c/opt/64/bin/*.dll /c/opt/openblas/openblas_dll -fi -# attempt to deal with: -# https://github.com/scipy/scipy/pull/20362#issuecomment-2028517797 -python -c "import tools.openblas_support as obs; obs.reformat_pkg_file('C:/opt/')" diff --git a/tools/wheels/cibw_before_test.sh b/tools/wheels/cibw_before_test.sh new file mode 100644 index 000000000000..d18f920bd2c4 --- /dev/null +++ b/tools/wheels/cibw_before_test.sh @@ -0,0 +1,10 @@ +set -ex + +FREE_THREADED_BUILD="$(python -c"import sysconfig; print(bool(sysconfig.get_config_var('Py_GIL_DISABLED')))")" +if [[ $FREE_THREADED_BUILD == "True" ]]; then + # TODO: delete when numpy is buildable under free-threaded python + # with a released version of cython + python -m pip install -U --pre pip + python -m pip install git+https://github.com/cython/cython + python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy +fi diff --git a/tools/wheels/cibw_test_command.sh b/tools/wheels/cibw_test_command.sh index 2eb214b3582b..165be7f3624e 100644 --- a/tools/wheels/cibw_test_command.sh +++ b/tools/wheels/cibw_test_command.sh @@ -1,12 +1,11 @@ set -xe -PROJECT_DIR="$1" - -# python $PROJECT_DIR/tools/wheels/check_license.py -if [[ $(uname) == "Linux" ]] ; then - python $PROJECT_DIR/tools/openblas_support.py --check_version +FREE_THREADED_BUILD="$(python -c"import sysconfig; print(bool(sysconfig.get_config_var('Py_GIL_DISABLED')))")" +if [[ $FREE_THREADED_BUILD == "True" ]]; then + # TODO: delete when importing numpy no longer enables the GIL + # setting to zero ensures the GIL is disabled while running the + # tests under free-threaded python + export PYTHON_GIL=0 fi -echo $? python -c "import sys; import scipy; sys.exit(not scipy.test())" -echo $? diff --git a/tools/wheels/repair_windows.sh b/tools/wheels/repair_windows.sh index 9e966ec1f8c4..ac20eae6ac47 100644 --- a/tools/wheels/repair_windows.sh +++ b/tools/wheels/repair_windows.sh @@ -2,6 +2,7 @@ set -xe WHEEL="$1" DEST_DIR="$2" +OPENBLAS_DIR=$(python -c"import scipy_openblas32 as sop; print(sop.get_lib_dir())") # create a temporary directory in the destination folder and unpack the wheel # into there @@ -19,7 +20,6 @@ pushd scipy* for f in $(find ./scipy* -name '*.pyd'); do strip $f; done - # now repack the wheel and overwrite the original wheel pack . mv -fv *.whl $WHEEL @@ -27,6 +27,4 @@ mv -fv *.whl $WHEEL cd $DEST_DIR rm -rf tmp -# the libopenblas.dll is placed into this directory in the cibw_before_build -# script. -delvewheel repair --add-path /c/opt/openblas/openblas_dll --no-dll libsf_error_state.dll -w $DEST_DIR $WHEEL +delvewheel repair --add-path $OPENBLAS_DIR --no-dll libsf_error_state.dll -w $DEST_DIR $WHEEL