diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml new file mode 100644 index 00000000..1477f618 --- /dev/null +++ b/.github/workflows/build-check.yml @@ -0,0 +1,105 @@ +name: Build Check + +on: + workflow_dispatch: + push: + branches: + - "build-check" + - "release/*" + +jobs: + build-check: + name: Build Check + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + os: [ubuntu-latest, macos-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install non-python dependencies on mac + if: runner.os == 'macOS' + run: | + brew install suite-sparse + + - name: Install non-python dependencies on linux + if: runner.os == 'Linux' + run: | + sudo apt-get install libsuitesparse-dev + + - name: Install build tools + run: | + pip install --upgrade pip + pip install build + + - name: Build sdist and wheel + run: | + python -m build + + - name: List sdist contents + run: | + echo "---- Source distribution contents ----" + tar -tzf dist/*.tar.gz + + - name: List wheel contents + run: | + echo "---- Wheel contents ----" + unzip -l dist/*.whl + + - name: Check wheel for .c/.pyx (should be excluded) + run: | + if unzip -l dist/*.whl | grep -E '\.c$|\.pyx$'; then + echo "::error::Wheel includes Cython source or C files" + exit 1 + fi + + - name: Setup Micromamba + uses: mamba-org/setup-micromamba@v2 + with: + micromamba-version: "latest" + init-shell: bash + + - name: Create new conda env and install wheel + shell: bash -l {0} # use bash as a login shell for conda activation + run: | + micromamba create -n test-wheel python=${{ matrix.python-version }} -y + micromamba activate test-wheel + micromamba install -c conda-forge suitesparse -y + pip install dist/*.whl + cd .. # cd out of repo to avoid import issues + pip show scikit-sparse + pip show scikit-sparse-dev + python -c "import sksparse; print(f'Wheel loaded from: {sksparse.__file__}')" + + - name: Create new conda env and install sdist + shell: bash -l {0} # use bash as a login shell for conda activation + run: | + micromamba create -n test-sdist python=${{ matrix.python-version }} -y + micromamba activate test-sdist + micromamba install -c conda-forge suitesparse pytest -y + pip install dist/scikit_sparse*.tar.gz + SRC_DIR=$(pwd) + cd .. # cd out of repo to avoid import issues + pip show scikit-sparse + pip show scikit-sparse-dev + python -c "import sksparse; print(f'sdist loaded from: {sksparse.__file__}')" + # Run tests on the installed package + cp -R ${SRC_DIR}/tests ./tests + pytest tests + + - name: Upload built artifact + uses: actions/upload-artifact@v4 + # Upload only one artifact to avoid redundancy + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' + with: + name: dist + path: dist/ diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml new file mode 100644 index 00000000..9200d43d --- /dev/null +++ b/.github/workflows/ci-dev.yml @@ -0,0 +1,66 @@ +name: CI Dev Code + +on: + workflow_dispatch: + pull_request: + push: + branches: + - "dev" + - "bugfix/*" + +jobs: + tests: + name: Test Dev Code + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + os: [ubuntu-latest, macos-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install non-python dependencies on mac + if: runner.os == 'macOS' + run: | + brew install suite-sparse + + - name: Install non-python dependencies on linux + if: runner.os == 'Linux' + run: | + sudo apt-get install libsuitesparse-dev + + - name: Install dependencies and package + run: | + pip install --upgrade pip + pip install -v -e .[dev] + + - name: Display installed packages + run: | + python -m pip list + + - name: Run lint + run: ruff check + + - name: Test with pytest + run: pytest -v -s + + - name: Build Docs + run: | + cd doc + make SPHINXOPTS=-nW html + + - name: Upload docs as artifact + uses: actions/upload-artifact@v4 + with: + name: scikit-sparse-doc-${{ matrix.os }}-${{ matrix.python-version }} + path: doc/_build/html diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml deleted file mode 100644 index 7a7b758b..00000000 --- a/.github/workflows/ci_test.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: CI targets - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - release: - types: - - published - - -jobs: - tests: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - os: [ubuntu-latest, macos-latest] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - env: - PIP_DISABLE_PIP_VERSION_CHECK: 1 - with: - python-version: ${{ matrix.python-version }} - - name: Install non-python dependencies on mac - if: runner.os == 'macOS' - run: | - brew install suite-sparse - - name: Install non-python dependencies on linux - if: runner.os == 'Linux' - run: | - sudo apt-get install libsuitesparse-dev - - name: Install dependencies and package - run: | - pip install --upgrade pip setuptools wheel - pip install pytest black - pip install -e . - - name: Display installed packages - run: | - python -m pip list - - name: Run lint - run: black . - - name: Test with pytest - run: pytest -v tests - - build: - needs: [tests] - name: Build source distribution - runs-on: ubuntu-latest - if: github.event_name == 'release' - - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.8' - - name: Install non-python dependencies on linux - run: | - sudo apt-get install libsuitesparse-dev - - name: Build - run: | - pip install --upgrade pip setuptools wheel - pip install numpy cython - python setup.py sdist - - name: Test the sdist - run: | - mkdir tmp - cd tmp - python -m venv venv-sdist - venv-sdist/bin/python -m pip install --upgrade pip setuptools wheel - venv-sdist/bin/python -m pip install pytest - venv-sdist/bin/python -m pip install ../dist/scikit_sparse*.tar.gz - venv-sdist/bin/python -c "import sksparse;print(sksparse.__version__)" - venv-sdist/bin/python -m pytest -v ../tests - - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/* - - deploy: - needs: [tests, build] - runs-on: ubuntu-latest - environment: deploy - permissions: - # IMPORTANT: this permission is mandatory for trusted publishing - id-token: write - if: github.event_name == 'release' - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.8' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Download wheel/dist from build - uses: actions/download-artifact@v4 - with: - name: dist - path: dist - - name: Build and publish - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..96f3a06d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,41 @@ +name: Deploy + +on: + workflow_dispatch: + release: + types: + - published + +jobs: + deploy: + runs-on: ubuntu-latest + environment: deploy + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" # minimum supported version + + - name: Install non-python dependencies on linux + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install libsuitesparse-dev + + - name: Install build tools + run: | + pip install --upgrade pip + pip install build + + - name: Build sdist and wheel + run: | + python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index cf8a3691..e3f2ad48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,16 @@ # Project specific files # ########################## -scikits/sparse/cholmod.c .coverage htmlcov/ .tox .cache +_dev_scripts/ + +# Cython-generated files +*.c +*.cpp +*.html # Cribbed from numpy's .gitignore: @@ -63,6 +68,8 @@ _build dist doc/build doc/cdoc/build +# auto-generated Sphinx files +doc/**/generated # Eggs .eggs # Egg metadata @@ -89,4 +96,4 @@ venv # vscode .devcontainer/ -.vscode/ \ No newline at end of file +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..97a96760 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.7 + hooks: + # Run the linter, but do not make any changes + - id: ruff-check diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..733d99cd --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# .readthedocs.yaml +# The version of the configuration file. +version: 2 + +build: + os: ubuntu-24.04 + tools: + # Use a recent Python version + python: "mambaforge-22.9" + +conda: + environment: doc/environment.yaml + +sphinx: + configuration: doc/conf.py + +formats: all diff --git a/LICENSE.txt b/LICENSE.txt index c2926919..e9eb434b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,5 @@ -Copyright (c) 2009-2018, Joscha Reimer, Alex Grigorievskiy, Antony Lee, Yuri, Leon Barrett, Dag Sverre Seljebotn, Nathaniel Smith, David Cournapeau +Copyright (C) 2009-2025, The scikit-sparse developers. All rights reserved. +See pyproject.toml for complete list of contributors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/MANIFEST.in b/MANIFEST.in index 9a523154..1aabd15e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ +# MANIFEST.in +# Files to be included in the source distribution (not in the wheel) include LICENSE.txt -include README.md -include sksparse/cholmod_backward_compatible.h -include sksparse/cholmod.pyx -include sksparse/test_data/* -exclude sksparse/cholmod.c +include README.rst +recursive-include src/sksparse *.pyx *.pxd *.c +recursive-exclude tests * +recursive-exclude *.egg-info * diff --git a/README.md b/README.md deleted file mode 100644 index c461d1d2..00000000 --- a/README.md +++ /dev/null @@ -1,119 +0,0 @@ -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/scikit-sparse/scikit-sparse)](https://github.com/scikit-sparse/scikit-sparse/releases/latest) -[![PyPI](https://img.shields.io/pypi/v/scikit-sparse)](https://pypi.org/project/scikit-sparse/) -[![Conda Version](https://img.shields.io/conda/vn/conda-forge/scikit-sparse.svg)](https://anaconda.org/conda-forge/scikit-sparse) -[![GitHub Workflow Status (event)](https://img.shields.io/github/workflow/status/scikit-sparse/scikit-sparse/CI%20targets?label=CI%20Tests)](https://github.com/scikit-sparse/scikit-sparse/actions/workflows/ci_test.yml) -[![Python Versions](https://img.shields.io/badge/python-3.6%2C%203.7%2C%203.8%2C%203.9%2C%203.10%2C%203.11%2C%203.12-blue.svg)]() -[![GitHub license](https://img.shields.io/github/license/scikit-sparse/scikit-sparse)](https://github.com/scikit-sparse/scikit-sparse/blob/master/LICENSE.txt) - -# scikit-sparse - -This `scikit-sparse` a companion to the scipy.sparse library for -sparse matrix manipulation in Python. It provides routines that are -not suitable for inclusion in scipy.sparse proper, usually because -they are GPL'ed. - -For more details on usage see the [docs](https://scikit-sparse.readthedocs.org). - -## Installation - -### With `pip` - -For pip installs of `scikit-sparse` depend on the suite-sparse library which can be installed via: -```bash -# mac -brew install suite-sparse - -# debian -sudo apt-get install libsuitesparse-dev -``` - -Then, `scikit-sparse` can be installed via pip: -```bash -pip install scikit-sparse -``` - -If you suite-sparse library is installed in a non-standard place and you get errors when installing with `pip` you can use the environment -variables: -* `SUITESPARSE_INCLUDE_DIR` -* `SUITESPARSE_LIBRARY_DIR` - -at runtime so the compiler can find them. For example, lets say your suite-sparse installation is in `/opt/local` then you can run -```bash -SUITESPARSE_INCLUDE_DIR=/opt/local/include SUITESPARSE_LIBRARY_DIR=/opt/local/lib pip install scikit-sparse -``` - -### With `conda` -The `conda` package comes with `suite-sparse` packaged as a dependency so all you need to do is: - -```bash -conda install -c conda-forge scikit-sparse -``` - -### Windows installation -This was tested with a Anaconda 3 installation and Python 3.8 - -0. Install requirements - - `conda install -c conda-forge cython` - tested with v0.29.32 - - `conda install -c conda-forge suitesparse` - tested with v5.4.0 - - optional (included in the build dependencies of `scikit-sparse`): - - `conda install -c conda-forge numpy` - tested with v1.23.2 - - `conda install -c conda-forge scipy` - tested with v1.9.1 - -1. Download Microsoft Build Tools for C++ from https://visualstudio.microsoft.com/de/visual-cpp-build-tools/ (tested with 2022, should work with 2015 or newer) - -2. Install Visual Studio Build Tools - 1. Choose Workloads - 2. Check "Desktop development with C++" - 3. Keep standard settings - -3. Run in a Powershell - - `$env:SUITESPARSE_INCLUDE_DIR='C:/Anaconda3/envs//Library/include/suitesparse'` - - `$env:SUITESPARSE_LIBRARY_DIR='C:/Anaconda3/envs//Library/lib'` - - `pip install scikit-sparse` - -4. Test `from sksparse.cholmod import cholesky` - - -## License - -The wrapper code contained in this package is released under a -2-clause BSD license, as per below. However, this applies only to the -original code contained in this package, and NOT to the libraries -(e.g., CHOLMOD) which it uses. These libraries are generally -licensed under less permissive licenses, such as the GNU GPL or LGPL, -and users of this package are responsible for determining what -requirements these licenses impose on their usage. (The intent here is -that if you, for example, buy a license to use CHOLMOD in a commercial -product, then you can also go ahead and use our wrapper code with your -commercial license.) - -Copyright (c) 2009-2017, the [scikit-sparse developers](https://scikit-sparse.readthedocs.io/en/latest/overview.html#developers) - - scikits-sparse - Copyright (c) 2009-2017, the scikit-sparse developers - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS - BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - SUCH DAMAGE. diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..d90a8940 --- /dev/null +++ b/README.rst @@ -0,0 +1,125 @@ +.. start-badges + +.. image:: https://img.shields.io/github/v/release/broesler/scikit-sparse + :target: https://github.com/broesler/scikit-sparse/releases/latest + :alt: Latest GitHub release + +.. image:: https://img.shields.io/pypi/v/scikit-sparse-dev + :target: https://pypi.org/project/scikit-sparse-dev/ + :alt: Latest PyPI release + +.. image:: https://img.shields.io/conda/vn/conda-forge/scikit-sparse-dev + :target: https://anaconda.org/conda-forge/scikit-sparse-dev + :alt: Latest conda-forge release + +.. image:: https://github.com/broesler/scikit-sparse/actions/workflows/ci-dev.yml/badge.svg?branch=dev + :target: https://github.com/broesler/scikit-sparse/actions/workflows/ci-dev.yml + :alt: CI Status + +.. image:: https://readthedocs.org/projects/scikit-sparse-dev/badge/?version=latest + :target: https://scikit-sparse-dev.readthedocs.io/en/latest/ + +.. end-badges + +======================== +Scikit-Sparse (sksparse) +======================== + +**NOTE**: + + This is the README for the development version of scikit-sparse. + For the stable version, see `the GitHub repository `_, and + `the stable docs `_. + + +The ``scikit-sparse`` package is a companion to the `scipy.sparse +`_ package for sparse matrix manipulation in Python. It provides +routines that are not suitable for inclusion in `scipy.sparse `_ +proper, typically because they depend on external libraries with +GPL licenses, such as `SuiteSparse `_. + +For more details on usage see `the docs `_. + +.. _upstream_repo: https://github.com/scikit-sparse/scikit-sparse +.. _upstream_docs: https://scikit-sparse.readthedocs.io +.. _scipy_sparse: https://docs.scipy.org/doc/scipy/reference/sparse.html +.. _suitesparse_website: https://people.engr.tamu.edu/davis/suitesparse.html +.. _sksparse_docs: https://scikit-sparse-dev.readthedocs.org + +.. start-installation + +Requirements +------------ + +Installing ``scikit-sparse`` requires: + +* `Python `_ >= 3.10 +* `NumPy `_ >= 2.0 +* `SciPy `_ >= 1.14 +* `Cython `_ >= 3.0 +* `SuiteSparse `_ >= 7.4.0 + +Older versions may work but are untested. + + +Installation +------------ + +Installing SuiteSparse +++++++++++++++++++++++ + +To install ``scikit-sparse``, you need to have the `SuiteSparse +`_ library installed on your system. + +It is recommended that you install SuiteSparse and the scikit-sparse +dependencies in a virtual environment, to avoid conflicts with other packages. +We recommend using Anaconda:: + + $ conda create -n scikit-sparse python>=3.10 suitesparse + $ conda activate scikit-sparse + +If you are not using Anaconda, you can install SuiteSparse using your preferred +package manager. + +On MacOS, you can use `Homebrew `_:: + + $ brew install suite-sparse + +On Debian/Ubuntu systems, use the following command:: + + $ sudo apt-get install python-scipy libsuitesparse-dev + +On Arch Linux, run:: + + $ sudo pacman -S suitesparse + + +Installing Scikit-Sparse +++++++++++++++++++++++++ + +Once you have SuiteSparse installed, you can install ``scikit-sparse`` with:: + + $ conda install -c conda-forge scikit-sparse-dev + +or if you prefer to use pip, you can install it with:: + + $ pip install scikit-sparse-dev + +Check if the installation was successful by running the following command:: + + $ python -c "import sksparse; print(sksparse.__version__)" + + +.. end-installation + +See `Troubleshooting `_ for more information on determining +which SuiteSparse library is being used. + +.. _docs_trouble: https://scikit-sparse-dev.readthedocs.io/en/latest/overview.html#troubleshooting + + +---- + +Copyright © 2009–2025, the `scikit-sparse developers `_. + +.. _docs_dev: https://scikit-sparse-dev.readthedocs.io/en/latest/overview.html#developers diff --git a/doc/_templates/autosummary/class.rst b/doc/_templates/autosummary/class.rst new file mode 100644 index 00000000..40f63116 --- /dev/null +++ b/doc/_templates/autosummary/class.rst @@ -0,0 +1,23 @@ +{{ name }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :no-members: + :no-inherited-members: + :no-special-members: + +{% block methods %} +{% if methods %} +.. rubric:: Methods + +.. autosummary:: + :toctree: + :nosignatures: + + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} +{% endif %} +{% endblock %} diff --git a/doc/_templates/autosummary/exception.rst b/doc/_templates/autosummary/exception.rst new file mode 100644 index 00000000..9e4f9771 --- /dev/null +++ b/doc/_templates/autosummary/exception.rst @@ -0,0 +1,7 @@ +{{ name }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autoexception:: {{ objname }} + :show-inheritance: diff --git a/doc/_templates/autosummary/function.rst b/doc/_templates/autosummary/function.rst new file mode 100644 index 00000000..4b251636 --- /dev/null +++ b/doc/_templates/autosummary/function.rst @@ -0,0 +1,6 @@ +{{ name }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autofunction:: {{ objname }} diff --git a/doc/_templates/autosummary/method.rst b/doc/_templates/autosummary/method.rst new file mode 100644 index 00000000..920b2fbf --- /dev/null +++ b/doc/_templates/autosummary/method.rst @@ -0,0 +1,6 @@ +{{ name }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. automethod:: {{ objname }} diff --git a/doc/changes.rst b/doc/changes.rst index d7b47a3a..212076cf 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -1,70 +1,179 @@ Changes ======= -.. module:: sksparse.cholmod +v0.5.0 +------ +* Major API updates to the :mod:`sksparse.cholmod` module. The module has been + updated to resemble the existing :func:`scipy.linalg.cholesky` interface, as + well as provide additional functions present in the SuiteSparse CHOLMOD + MATLAB interface, and MATLAB built-in Cholesky functions. The underlying + Cython code has been refactored to provide greater type safety and + performance. + + - The :code:`cholmod.Factor` class has been renamed to + :obj:`~sksparse.cholmod.CholeskyFactor`. + + - The :code:`cholmod.Common` class has been removed. Its attributes have been + subsumed into the :code:`CholeskyFactor` class. + + - The :func:`~sksparse.cholmod.cholesky` function now returns + a :obj:`~scipy.sparse.csc_array` instead of a :code:`Factor` object, and an + optional :obj:`~numpy.ndarray` containing the permutation vector. + + - The :func:`~sksparse.cholmod.ldl` function has been added. It returns + a tuple (:obj:`~scipy.sparse.csc_array`, :obj:`~scipy.sparse.csc_array`), + and an optional :obj:`~numpy.ndarray` containing the permutation vector. + + - A :func:`~sksparse.cholmod.cho_factor` function has been added to perform + the numeric Cholesky factorization and return + a :obj:`~sksparse.cholmod.CholeskyFactor` object. + + - Similarly, a :func:`~sksparse.cholmod.ldl_factor` function has been added + to perform the numeric LDL factorization and return + a :obj:`~sksparse.cholmod.CholeskyFactor` object. + + - The :code:`cholmod.analyze` function has been removed. The analysis step is + now performed when calling the constructor of + :obj:`~sksparse.cholmod.CholeskyFactor`. + + - The :code:`use_long` parameter has been removed from the + :func:`~sksparse.cholmod.cholesky` and :func:`~sksparse.cholmod.ldl` + functions. The type of indices is now inferred from the input matrix. + + - The :code:`mode` parameter has been renamed to :code:`supernodal_mode`. + + - The :code:`symmetric` parameter has been removed. It has been replaced by + two new parameters: :code:`lower`, and :code:`sym_kind`. + + * Parameter :code:`lower` controls whether to use the lower or upper + triangular part of the input matrix, or whether to return a lower or + upper triangular factor. + + * Parameter :code:`sym_kind` has been added. It accepts a string argument + in :code:`{"sym", "row", "col"}`, which controls the symmetry structure + of the matrix to analyze. + + - The functions :code:`cholmod.analyze_AAt` and :code:`cholmod.cholesky_AAt` + have been removed. Use :func:`~sksparse.cholmod.cho_factor` or + :func:`~sksparse.cholmod.cholesky` with :code:`sym_kind="row"` instead. + + - The :code:`ordering_method` parameter has been renamed to :code:`order`. + + - The :code:`Factor` methods :code:`L`, :code:`D`, :code:`LD`, :code:`L_D`, + and :code:`P`, have been removed in factor of the methods + :meth:`~sksparse.cholmod.CholeskyFactor.get_factor` and + :meth:`~sksparse.cholmod.CholeskyFactor.get_perm`. + + - The property :code:`perm` and + method :attr:`~sksparse.cholmod.CholeskyFactor.factor` have been added + to return read-only views of the permutation vector and factor matrix, + respectively. + + - The :code:`Factor` methods :code:`solve_LDLt`, :code:`solve_LD`, + :code:`solve_DLt`, :code:`solve_L`, :code:`solve_Lt`, and :code:`solve_D` + have been removed in favor of the single + :meth:`~sksparse.cholmod.CholeskyFactor.solve` method. + The :obj:`~sksparse.cholmod.CholeskyFactor` is not callable. + + - Add multiple properties to the :obj:`~sksparse.cholmod.CholeskyFactor` + class for convenient access to :code:`cholmod_factor` attributes. See the + full documentation for details. + + - Fix a bug in the previous version where sparse inputs with inconsistent + ``has_sorted_indices`` or ``has_canonical_format`` flags would silently + lead to incorrect results. The input matrix is now modified into + a canonical CSC format, regardless of the input format. + +* Create the :mod:`~sksparse.amd` module, which provides the AMD ordering method. +* Create the :mod:`~sksparse.btf` module, which provides the BTF ordering method. +* Create the :mod:`~sksparse.camd` module, which provides the constrained AMD + ordering method. +* Create the :mod:`~sksparse.colamd` module, which provides the COLAMD + ordering method. +* Create the :mod:`~sksparse.ccolamd` module, which provides the constrained + COLAMD ordering method. +* Create the :mod:`~sksparse.klu` submodule, which provides an interface to + the KLU sparse LU solver. +* Create the :mod:`~sksparse.spqr` submodule, which provides an interface to + the SPQR sparse QR solver. +* Create the :mod:`~sksparse.umfpack` submodule, which provides an interface to + the UMFPACK sparse LU solver. +* Remove support for the following versions: + + - Python < 3.10 + - NumPy < 2.0 + - SciPy < 1.14 + - SuiteSparse < 7.4.0 + + Python 3.9 will reach its end of life in October 2025, so remove support for + it now. Numpy will end support for all 1.x versions by September 2025. SciPy + v1.14 (released June 2024) will be supported until the end of 2026. v0.4.4 ------ - * Bug in solve with dense array, where base of result is not set correctly, fixed. - * Travis tests are using conda now. - * Supported versions updated to: - - Python: 3.7, 3.6 - - NumPy: 1.15, 1.14, 1.13 - - SciPy: 1.1, 1.0, 0.19 - - SuiteSparse: 5.2 +* Bug in solve with dense array, where base of result is not set correctly, fixed. +* Travis tests are using conda now. +* Supported versions updated to: + + - Python: 3.7, 3.6 + - NumPy: 1.15, 1.14, 1.13 + - SciPy: 1.1, 1.0, 0.19 + - SuiteSparse: 5.2 v0.4.3 ------ - * The method `solve_L` can now also use the `L` matrix of the LL' decomposition. - * Supported versions updated to: - - Python: 3.6, 3.5 - - NumPy: 1.14, 1.13 - - SciPy: 1.0, 0.19 +* The method :code:`Factor.solve_L` can now also use the `L` matrix of the LL' decomposition. +* Supported versions updated to: + + - Python: 3.6, 3.5 + - NumPy: 1.14, 1.13 + - SciPy: 1.0, 0.19 v0.4.2 ------ - * Bug where the ordering method is not taken into account is fixed. - * The Factor class has now a (public) copy method. +* Bug where the ordering method is not taken into account is fixed. +* The Factor class has now a (public) copy method. v0.4.1 ------ - * Bug with relaxed stride checking in NumPy 1.12 fixed. - * Supported versions updated to: - - Python: 3.6, 3.5, 3.4, 2.7 - - NumPy: 1.8 to 1.12 +* Bug with relaxed stride checking in NumPy 1.12 fixed. +* Supported versions updated to: + + - Python: 3.6, 3.5, 3.4, 2.7 + - NumPy: 1.8 to 1.12 v0.4 ------ - * 64-bit indices (type long) are now supported. - * The ordering method for Cholesky decomposition is now choosable. - * Specific exceptions subclasses are now thrown for each error condition. - * Setup does not rely on an installed Cython anymore. +* 64-bit indices (type long) are now supported. +* The ordering method for Cholesky decomposition is now choosable. +* Specific exceptions subclasses are now thrown for each error condition. +* Setup does not rely on an installed Cython anymore. v0.3.1 ------ - * Ensure that arrays returned by the :meth:`Factor.solve_...` methods are - writeable. +* Ensure that arrays returned by the :code:`Factor.solve_...` methods are + writeable. v0.3 ---- - * Dropped deprecated :meth:`Factor.solve_P` and :meth:`Factor.solve_P`. - * Fixed a memory leak upon garbage collection of :class:`Factor`. +* Dropped deprecated :code:`Factor.solve_P` and :code:`Factor.solve_P`. +* Fixed a memory leak upon garbage collection of :code:`Factor`. v0.2 ---- - * :class:`Factor` solve methods now return 1d output for 1d input - (just like ``np.dot`` does). - * :meth:`Factor.solve_P` and :meth:`Factor.solve_P` deprecated; use - :meth:`Factor.apply_P` and :meth:`Factor.apply_Pt` instead. - * New methods for computing determinants of positive-definite - matrices: :meth:`Factor.det`, :meth:`Factor.logdet`, - :meth:`Factor.slogdet`. - * New method for explicitly computing inverse of a positive-definite - matrix: :meth:`Factor.inv`. - * :meth:`Factor.D` has much better implementation. - * Build system improvements. - * Wrapper code re-licensed under BSD terms. +* :code:`Factor` solve methods now return 1d output for 1d input + (just like :func:`numpy.dot` does). +* :code:`Factor.solve_P` and :code:`Factor.solve_P` deprecated; use + :code:`Factor.apply_P` and :code:`Factor.apply_Pt` instead. +* New methods for computing determinants of positive-definite + matrices: :code:`Factor.det`, :code:`Factor.logdet`, + :code:`Factor.slogdet`. +* New method for explicitly computing inverse of a positive-definite + matrix: :code:`Factor.inv`. +* :code:`Factor.D` has much better implementation. +* Build system improvements. +* Wrapper code re-licensed under BSD terms. v0.1 ---- - First public release. +First public release. diff --git a/doc/cholmod.rst b/doc/cholmod.rst deleted file mode 100644 index 7435ab93..00000000 --- a/doc/cholmod.rst +++ /dev/null @@ -1,244 +0,0 @@ -Sparse Cholesky decomposition (:mod:`sksparse.cholmod`) -======================================================= - -.. module:: sksparse.cholmod - :synopsis: Sparse Cholesky decomposition using CHOLMOD - -.. versionadded:: 0.1 - -Overview --------- - -This module provides efficient implementations of all the basic linear -algebra operations for sparse, symmetric, positive-definite matrices -(as, for instance, commonly arise in least squares problems). - -Specifically, it exposes most of the capabilities of the `CHOLMOD -`_ package, -including: - -* Computation of the `Cholesky decomposition - `_ :math:`LL' = - A` or :math:`LDL' = A` (with fill-reducing permutation) for both - real and complex sparse matrices :math:`A`, in any format supported - by :mod:`scipy.sparse`. (However, CSC matrices will be most - efficient.) -* A convenient and efficient interface for using this decomposition to - solve problems of the form :math:`Ax = b`. -* The ability to perform the costly fill-reduction analysis once, and - then re-use it to efficiently decompose many matrices with the same - pattern of non-zero entries. -* In-place 'update' and 'downdate' operations, for computing the - Cholesky decomposition of a rank-k update of :math:`A` and of - product :math:`AA'`. So, the result is the Cholesky decomposition of - :math:`A + CC'` (or :math:`AA' + CC'`). The last case is useful when the - columns of `A` become available incrementally (e.g., due to memory - constraints), or when many matrices with similar but non-identical - columns must be factored. -* Convenience functions for computing the (log) determinant of the - matrix that has been factored. -* A convenience function for explicitly computing the inverse of the - matrix that has been factored (though this is rarely useful). - -Quickstart ----------- - -If :math:`A` is a sparse, symmetric, positive-definite matrix, and -:math:`b` is a matrix or vector (either sparse or dense), then the -following code solves the equation :math:`Ax = b`:: - - from sksparse.cholmod import cholesky - factor = cholesky(A) - x = factor(b) - -If we just want to compute its determinant:: - - factor = cholesky(A) - ld = factor.logdet() - -(This returns the log of the determinant, rather than the determinant -itself, to avoid issues with underflow/overflow. See :meth:`logdet`, -:meth:`log`.) - -If you have a least-squares problem to solve, minimizing :math:`||Mx - -b||^2`, and :math:`M` is a sparse matrix, the `solution -`_ -is :math:`x = (M'M)^{-1} M'b`, which can be efficiently calculated -as:: - - from sksparse.cholmod import cholesky_AAt - # Notice that CHOLMOD computes AA' and we want M'M, so we must set A = M'! - factor = cholesky_AAt(M.T) - x = factor(M.T * b) - -However, you should be aware that for least squares problems, the -Cholesky method is usually faster but somewhat less numerically stable -than QR- or SVD-based techniques. - -Top-level functions -------------------- - -All usage of this module starts by calling one of four functions, all -of which return a :class:`Factor` object, documented below. - -Most users will want one of the ``cholesky`` functions, which perform -a fill-reduction analysis and decomposition together: - -.. autofunction:: cholesky(A, beta=0, mode="auto", ordering_method="default", use_long=None) - -.. autofunction:: cholesky_AAt(A, beta=0, mode="auto", ordering_method="default", use_long=None) - -However, some users may want to break the fill-reduction analysis and -actual decomposition into separate steps, and instead begin with one -of the ``analyze`` functions, which perform only fill-reduction: - -.. autofunction:: analyze(A, mode="auto", ordering_method="default", use_long=None) - -.. autofunction:: analyze_AAt(A, mode="auto", ordering_method="default", use_long=None) - -.. note:: Even if you used :func:`cholesky` or :func:`cholesky_AAt`, - you can still call :meth:`cholesky_inplace() - ` or :meth:`cholesky_AAt_inplace() - ` on the resulting :class:`Factor` to - quickly factor another matrix with the same non-zero pattern as your - original matrix. - -:class:`Factor` objects ------------------------ - -.. class:: Factor - - A :class:`Factor` object represents the Cholesky decomposition of some - matrix :math:`A` (or :math:`AA'`). Each :class:`Factor` fixes: - - * A specific fill-reducing permutation - * A choice of which Cholesky algorithm to use (see :func:`analyze`) - * Whether we are currently working with real numbers or complex - - Given a :class:`Factor` object, you can: - - * Compute new Cholesky decompositions of matrices that have the same - pattern of non-zeros - * Perform 'updates' or 'downdates' - * Access the various Cholesky factors - * Solve equations involving those factors - -Factoring new matrices -++++++++++++++++++++++ - -.. automethod:: Factor.cholesky_inplace(A, beta=0) - -.. automethod:: Factor.cholesky_AAt_inplace(A, beta=0) - -.. automethod:: Factor.cholesky(A, beta=0) - -.. automethod:: Factor.cholesky_AAt(A, beta=0) - -Updating/Downdating -+++++++++++++++++++ - -.. automethod:: Factor.update_inplace(C, subtract=False) - -Accessing Cholesky factors explicitly -+++++++++++++++++++++++++++++++++++++ - -.. note:: When possible, it is generally more efficient to use the - ``solve_...`` functions documented below rather than extracting the - Cholesky factors explicitly. - -.. automethod:: Factor.P - -.. automethod:: Factor.D - -.. automethod:: Factor.L - -.. automethod:: Factor.LD - -.. automethod:: Factor.L_D - -Solving equations -+++++++++++++++++ - -All methods in this section accept both sparse and dense matrices (or -vectors) ``b``, and return either a sparse or dense ``x`` -accordingly. - -All methods in this section act on :math:`LDL'` factorizations by default. -Thus `L` refers by default to the matrix returned by :meth:`L_D`, not that -returned by :meth:`L` (though conversion is not performed unless necessary). - -.. automethod:: Factor.solve_A(b) - -.. automethod:: Factor.__call__(b) - -.. automethod:: Factor.solve_LDLt(b) - -.. automethod:: Factor.solve_LD(b) - -.. automethod:: Factor.solve_DLt(b) - -.. automethod:: Factor.solve_L(b) - -.. automethod:: Factor.solve_Lt(b) - -.. automethod:: Factor.solve_D(b) - -.. automethod:: Factor.apply_P(b) - -.. automethod:: Factor.apply_Pt(b) - -Convenience methods -------------------- - -.. automethod:: Factor.logdet() - -.. automethod:: Factor.det() - -.. automethod:: Factor.slogdet() - -.. automethod:: Factor.inv() - -.. automethod:: Factor.copy() - - -Error handling --------------- - -.. class:: CholmodError - -.. class:: CholmodNotPositiveDefiniteError - -.. class:: CholmodNotInstalledError - -.. class:: CholmodOutOfMemoryError - -.. class:: CholmodTooLargeError - -.. class:: CholmodNotPositiveDefiniteError - -.. class:: CholmodInvalidError - -.. class:: CholmodGpuProblemError - - Errors detected by CHOLMOD or by our wrapper code are converted into - exceptions of type :class:`CholmodError` or an appropriated subclass. - -.. class:: CholmodWarning - - Warnings issued by CHOLMOD are converted into Python warnings of - type :class:`CholmodWarning`. - -.. class:: CholmodTypeConversionWarning - - CHOLMOD itself supports matrices in CSC form with 32-bit integer - indices and 'double' precision floats (64-bits, or 128-bits total - for complex numbers). If you pass some other sort of matrix, then - the wrapper code will convert it for you before passing it to - CHOLMOD, and issue a warning of type - :class:`CholmodTypeConversionWarning` to let you know that your - efficiency is not as high as it might be. - - .. warning:: Not all conversions currently produce warnings. This is - a bug. - - Child of :class:`CholmodWarning`. diff --git a/doc/conf.py b/doc/conf.py index 5d3654b0..286dc8f5 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # scikit-sparse documentation build configuration file, created by # sphinx-quickstart on Wed Feb 10 23:17:16 2016. @@ -12,55 +11,77 @@ # # All configuration values have a default; values that are commented out # serve to show the default. +"""Configuration file for the Sphinx documentation builder.""" -import sys +import inspect import os +import re +import sys +from importlib import import_module +from pathlib import Path +from urllib.parse import quote + +import sksparse # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -if not os.environ.get('READTHEDOCS'): - sys.path.insert(0, os.path.abspath('..')) +if not os.environ.get("READTHEDOCS"): + sys.path.insert(0, Path("..").resolve().as_posix()) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", # page-per-object + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", # numpy style docstrings + "sphinx.ext.viewcode", + "sphinx.ext.linkcode", + "sphinx_copybutton", # add "copy" button to code blocks ] +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy", None), +} + +nitpicky = True # warn about all references where the target cannot be found + +napoleon_use_param = False # ignore text after colon in param description +napoleon_use_rtype = False # don't show seprate section for return type + # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = {".rst": "restructuredtext"} # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'scikit-sparse' -copyright = '2016, Antony Lee' -author = 'Antony Lee' +project = "scikit-sparse" +copyright = "2016–­2025, The scikit-sparse developers" +author = "The scikit-sparse developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -import sksparse # The short X.Y version. version = sksparse.__version__ # The full version, including alpha/beta/rc tags. @@ -71,203 +92,319 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build", "**/_drafts"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +GITHUB_URL = "https://github.com/broesler/scikit-sparse" +GITHUB_BRANCH = "dev" + +PROJECT_ROOT = Path(__file__).parent.parent + + +def get_cython_lineno(src_path: Path, obj_name: str) -> int | None: + """Get the line number of a Cython object in its .pyx source file. + + Parameters + ---------- + src_path + Path to the .pyx source file. + obj_name + Name of the object to find. + + Returns + ------- + int or None + Line number of the object in the source file, or None if not found. + """ + pat = re.compile( + rf"^\s*(cdef|cpdef|def|class)\s+.*?\b{re.escape(obj_name)}\b\s*(\(|:|=)", + ) + + try: + with src_path.open() as fp: + for i, line in enumerate(fp, start=1): + if pat.search(line): + return i + except FileNotFoundError: + pass + + if obj_name == "CholeskyFactor.factorize": + breakpoint() + return None + + +# linkcode setup +def linkcode_resolve(domain, info): + """Determine the URL corresponding to Python object.""" + if domain != "py": + return None + + if not info["module"]: + return None + + modname = info["module"] + fullname = info["fullname"] + + if not modname or not fullname: + return None + + # --- Get Python object --- + try: + module = import_module(modname) + obj = module + for part in fullname.split("."): + obj = getattr(obj, part) + except Exception: + obj = None # Cython object, or cannot be found + + # --- Get source file and line number --- + filename = None + lineno = None + + if obj is not None: + try: + filename = inspect.getsourcefile(obj) + except TypeError: + pass + + if filename is not None and filename != "": + # ---------- Pure Python Objects ---------- + # Get the line number + try: + _, lineno = inspect.getsourcelines(obj) + except Exception: + pass + + src_path = Path(filename) + else: + # ---------- Cython Objects ---------- + potential_pyx_path = Path(*modname.split(".")) + src_path = PROJECT_ROOT / potential_pyx_path.with_suffix(".pyx") + obj_simple_name = fullname.split(".")[-1] + lineno = get_cython_lineno(src_path, obj_simple_name) + + # Construct the path relative to the Git repo root + try: + rel_path = src_path.relative_to(PROJECT_ROOT) + except ValueError: + return None # file not in the scikit-sparse repo + + github_path = quote(rel_path.as_posix()) + + url = f"{GITHUB_URL}/blob/{GITHUB_BRANCH}/{github_path}" + + if lineno: + url += f"#L{lineno}" + + return url + # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "github_user": "scikit-sparse", - "github_repo": "scikit-sparse", - "github_banner": True} + "source_repository": GITHUB_URL, + "source_branch": GITHUB_BRANCH, + "source_directory": "doc/", + "top_of_page_buttons": ["view"], + "footer_icons": [ + { + "name": "GitHub", + "url": GITHUB_URL, + "html": """ + + + + """, # noqa: E501 + "class": "", + }, + ], +} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'scikit-sparsedoc' +htmlhelp_basename = "scikit-sparsedoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'scikit-sparse.tex', 'scikit-sparse Documentation', - 'Antony Lee', 'manual'), + ( + master_doc, + "scikit-sparse.tex", + "scikit-sparse Documentation", + "Antony Lee", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'scikit-sparse', 'scikit-sparse Documentation', - [author], 1) -] +man_pages = [(master_doc, "scikit-sparse", "scikit-sparse Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -276,19 +413,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'scikit-sparse', 'scikit-sparse Documentation', - author, 'scikit-sparse', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "scikit-sparse", + "scikit-sparse Documentation", + author, + "scikit-sparse", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/doc/environment.yaml b/doc/environment.yaml new file mode 100644 index 00000000..4b08b631 --- /dev/null +++ b/doc/environment.yaml @@ -0,0 +1,11 @@ +# This file is used by ReadTheDocs to set up the conda environment +name: rtd313 +channels: + - conda-forge + - defaults +dependencies: + - python=3.13 + - pip + - pip: + - ../.[dev] + - suitesparse diff --git a/doc/index.rst b/doc/index.rst index a0e98d2b..14701794 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -3,24 +3,80 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -scikit-sparse - Sparse matrix extensions for SciPy -================================================== +=================================================== +Scikit-Sparse -- Sparse Matrix Extensions for SciPy +=================================================== -Contents: +Scikit-Sparse is a collection of sparse matrix extensions for SciPy, +with a focus on factorization routines and reordering methods. -.. toctree:: - :maxdepth: 2 +The package is a thin wrapper around the `SuiteSparse `_ +library, to make it compatible with :mod:`scipy.sparse` arrays. + +.. _suitesparse_website: https://people.engr.tamu.edu/davis/suitesparse.html + + +Features +-------- + +- Fill-reducing orderings: AMD and COLAMD +- Cholesky factorization via CHOLMOD +- Integration with SciPy sparse arrays + + +Installation +------------ + +.. code-block:: bash + + conda install -c conda-forge scikit-sparse-dev + # or + pip install scikit-sparse-dev + + +Quick Example +------------- - overview.rst +.. code-block:: python - cholmod.rst + import numpy as np + from scipy.sparse import csc_array + from sksparse.cholmod import cho_factor + + # Create a sparse positive definite matrix + A = csc_array([[4, 1, 0], + [1, 3, 0], + [0, 0, 2]]) + + # Perform Cholesky factorization + f = cho_factor(A) + + # Solve Ax = b + b = np.array([1, 2, 3]) + x = f.solve(b) + + +Learn More +---------- + +* :doc:`Overview ` - Introduction and installation instructions +* :doc:`User Guide ` - Tutorials and examples +* :doc:`API Reference ` - Detailed API documentation +* :doc:`Change Log ` - List of changes by version + +.. _github_repo: https://github.com/broesler/scikit-sparse +.. _github_issues: https://github.com/broesler/scikit-sparse/issues + + +.. toctree:: + :maxdepth: 2 + :caption: Contents + :hidden: - changes.rst + Overview -Indices and tables -================== + User Guide -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + API Reference + Change Log diff --git a/doc/overview.rst b/doc/overview.rst index 530bddb1..4a0eb7b8 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -1,91 +1,108 @@ +======== Overview ======== Introduction ------------ -The :mod:`scikit-sparse` package (previously known as :mod:`scikits.sparse`) -is a companion to the :mod:`scipy.sparse` library for sparse matrix -manipulation in Python. All :mod:`scikit-sparse` routines expect and -return :mod:`scipy.sparse` matrices (usually in CSC format). The intent -of :mod:`scikit-sparse` is to wrap GPL'ed code such as `SuiteSparse -`_, which cannot be -included in SciPy proper. +The ``scikit-sparse`` package (previously known as ``scikits.sparse``) +is a companion to the :mod:`scipy.sparse` library for sparse matrix manipulation +in Python. All :mod:`sksparse` routines expect and return :mod:`scipy.sparse` +matrices (usually in CSC format). The intent of :mod:`sksparse` is to wrap code +with a GPL license, such as `SuiteSparse `_, which cannot +be included in SciPy proper. -Currently our coverage is rather... sparse, with only a wrapper for -the CHOLMOD routines for sparse Cholesky decomposition, but we hope -that this will expand over time. Contributions of new wrappers are -very welcome, especially if you can follow the style of the existing -interfaces. +.. _suitesparse_website: https://people.engr.tamu.edu/davis/suitesparse.html -Download --------- +.. include:: ../README.rst + :start-after: .. start-installation + :end-before: .. end-installation -The current release may be downloaded from the Python Package index at - https://pypi.python.org/pypi/scikit-sparse/ +Troubleshooting ++++++++++++++++ -Or from the `homepage `_ -at +The installation will automatically detect the SuiteSparse library and compile +the necessary Cython code. It will check for the SuiteSparse library in the +following order: - https://github.com/scikit-sparse/scikit-sparse/releases + 1. The environment variables ``SUITESPARSE_INCLUDE_DIR`` and + ``SUITESPARSE_LIB_DIR`` (if set, these will override the default search + paths) + 2. Your active conda environment path + 3. Your homebrew paths (*e.g.* ``/opt/homebrew/include/suitesparse``) + 4. Typical system paths (*e.g.* ``/usr/include/suitesparse`` on Linux, or + ``/usr/local/include/suitesparse`` on macOS) -Or the latest *development version* may be found in our `Git -repository `_:: +The first path that contains the SuiteSparse headers and libraries will be used. - $ git clone git://github.com/scikit-sparse/scikit-sparse.git -Requirements ------------- +To see which SuiteSparse library was found, you can run the following command on +MacOS or Linux:: -Installing :mod:`scikit-sparse` requires: + $ CHECK_SKSPARSE_INSTALL=$(python -c 'import sksparse.cholmod; print(sksparse.cholmod.__file__)') -* `Python `_ -* `NumPy `_ -* `SciPy `_ -* `Cython `_ -* CHOLMOD (included in `SuiteSparse `_) +Then, use one of the following commands depending on your operating system. -Test versions are: -* Python: 3.7, 3.6 -* NumPy: 1.15, 1.14, 1.13 -* SciPy: 1.1, 1.0, 0.19 -* SuiteSparse: 5.2 -(Other versions may work but are untested.) -On Debian/Ubuntu systems, the following command should suffice:: +MacOS +^^^^^ - $ sudo apt-get install python-scipy libsuitesparse-dev +On MacOS, use the following command to check where the SuiteSparse +installation was found:: -On Arch Linux, run:: + $ otool -L $CHECK_SKSPARSE_INSTALL | grep cholmod - $ sudo pacman -S suitesparse +Look for a line that contains ``cholmod.*\.dylib`` or ``cholmod.*\.a``. The +output might be something like:: -Installation ------------- + $ otool -L $CHECK_SKSPARSE_INSTALL | grep cholmod + /Users/username/src/scikit-sparse/sksparse/cholmod.cpython-313-darwin.so: + @rpath/libcholmod.5.dylib (compatibility version 5.0.0, current version 5.3.1) + /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0) + +The ``@rpath/libcholmod.5.dylib`` indicates that the library was found on the +relative path. To resolve this path, run:: + + $ otool -l @rpath/libcholmod.5.dylib | grep -A2 LC_RPATH + cmd LC_RPATH + cmdsize 72 + path /Users/username/anaconda3/envs/scikit-sparse/lib (offset 12) -As usual, :: +which indicates that the library was found on the conda path. - $ pip install --user scikit-sparse -or with conda :: +Linux +^^^^^ + +On Linux, use the following commands instead:: + + $ ldd $CHECK_SKSPARSE_INSTALL | grep cholmod + $ readelf -d $CHECK_SKSPARSE_INSTALL | grep -E '(RPATH|RUNPATH)' + 0x000000000000001d (RUNPATH) Library runpath: [/home/user/anaconda3/envs/scikit-sparse/lib] + +also confirming installation on the conda path. - $ conda install -c conda-forge scikit-sparse Contact ------- -Post your suggestions and questions directly to our `bug tracker -`_. +Post your suggestions and questions directly to our `GitHub Issues page +`_. + +.. _github_issues: https://github.com/broesler/scikit-sparse/issues Developers ---------- * 2008 `David Cournapeau `_ -* 2009-2015 `Nathaniel Smith `_ +* 2009–2015 `Nathaniel Smith `_ * 2010 `Dag Sverre Seljebotn `_ * 2014 `Leon Barrett `_ * 2015 `Yuri `_ -* 2016-2017 `Antony Lee `_ +* 2016–2017 `Antony Lee `_ * 2016 `Alex Grigorievskiy `_ -* 2016-2018 `Joscha Reimer `_ +* 2016–2018 `Joscha Reimer `_ +* 2021- `Justin Ellis `_ +* 2022- `Aaron Johnson `_ +* 2025– `Bernard Roesler `_ diff --git a/doc/reference/amd.rst b/doc/reference/amd.rst new file mode 100644 index 00000000..a09648cd --- /dev/null +++ b/doc/reference/amd.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.amd + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/btf.rst b/doc/reference/btf.rst new file mode 100644 index 00000000..057cf7d3 --- /dev/null +++ b/doc/reference/btf.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.btf + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/camd.rst b/doc/reference/camd.rst new file mode 100644 index 00000000..4ad935c9 --- /dev/null +++ b/doc/reference/camd.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.camd + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/ccolamd.rst b/doc/reference/ccolamd.rst new file mode 100644 index 00000000..ca6a4124 --- /dev/null +++ b/doc/reference/ccolamd.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.ccolamd + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/cholmod.rst b/doc/reference/cholmod.rst new file mode 100644 index 00000000..80666cdf --- /dev/null +++ b/doc/reference/cholmod.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.cholmod + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/colamd.rst b/doc/reference/colamd.rst new file mode 100644 index 00000000..5fa25054 --- /dev/null +++ b/doc/reference/colamd.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.colamd + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/index.rst b/doc/reference/index.rst new file mode 100644 index 00000000..e0f3f936 --- /dev/null +++ b/doc/reference/index.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/doc/reference/klu.rst b/doc/reference/klu.rst new file mode 100644 index 00000000..d6109f30 --- /dev/null +++ b/doc/reference/klu.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.klu + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/spqr.rst b/doc/reference/spqr.rst new file mode 100644 index 00000000..38e6c5b2 --- /dev/null +++ b/doc/reference/spqr.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.spqr + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/reference/umfpack.rst b/doc/reference/umfpack.rst new file mode 100644 index 00000000..d3b5bafc --- /dev/null +++ b/doc/reference/umfpack.rst @@ -0,0 +1,4 @@ +.. automodule:: sksparse.umfpack + :no-inherited-members: + :no-members: + :no-special-members: diff --git a/doc/tutorial/amd.rst b/doc/tutorial/amd.rst new file mode 100644 index 00000000..14858b78 --- /dev/null +++ b/doc/tutorial/amd.rst @@ -0,0 +1,134 @@ +.. Copyright (C) 2025, Bernard Roesler. All rights reserved. + Part of the scikit-sparse project. + See pyproject.toml for full author list and LICENSE.txt for license details. + SPDX-License-Identifier: BSD-2-Clause + +=============================================================== +Approximate Minimum Degree (AMD) Ordering (:mod:`sksparse.amd`) +=============================================================== + +.. currentmodule:: sksparse.amd + + +The :mod:`sksparse.amd` module provides an interface to the `Approximate +Minimum Degree (AMD) `_ ordering algorithm for sparse, square +matrices. + +It exposes the main function of the `AMD package +`_, which +computes a symmetric ordering of a sparse matrix that minimizes the fill-in of +the Cholesky decomposition. The AMD function accepts both real and complex +matrices, in any format supported by :mod:`scipy.sparse` (CSC format is most +efficient). + +.. _amd_paper: https://epubs.siam.org/doi/abs/10.1137/S0895479894278952 +.. _amd_github: https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/AMD + + +Quickstart +---------- + +If :math:`A` is a sparse, square matrix, then the +following code computes the AMD ordering of :math:`A`: + +.. code:: python + + from sksparse.amd import amd + A = ... # some sparse matrix + p = amd(A) + PAPT = A[p][:, p] + +to give the permuted matrix :math:`PAP^T`, where :math:`P` is the permutation +matrix corresponding to the ordering :math:`p`. If :math:`A` is not symmetric, +then this is the same as AMD computes the ordering of the symbolically +symmetric matrix :math:`A + A^T`. + +We can then compute the Cholesky decompositions of the original and permuted +matrix using :func:`~sksparse.cholmod.cholesky`, and compare the number of +non-zeros in each: + +.. code:: python + + from sksparse.cholmod import cholesky + A_factor = cholesky(A) + PAPT_factor = cholesky(PAPT) + L = A_factor.L() + Lp = PAPT_factor.L() + print("Number of non-zeros in L: ", L.nnz) + print("Number of non-zeros in Lp:", Lp.nnz) + +The number of non-zeros in the Cholesky factorization of the permuted matrix +should be less than or equal to the number of non-zeros in the Cholesky +factorization of the original matrix, but this is not guaranteed. + + +Example +------- + +To see the effects of AMD ordering, we can load a sparse matrix from the +`SuiteSparse Matrix Collection `_ and compute its AMD ordering. + +.. _SSMC: https://sparse.tamu.edu + +.. literalinclude:: examples/amd_example.py + :language: python + +The figure shows the effect of AMD ordering that reduces the fill-in of the +Cholesky factorization of a sparse matrix. + +.. figure:: examples/amd_example.svg + :alt: AMD Example + :align: center + :width: 90% + + The number of non-zeros in the Cholesky factorization of the original matrix + (left) and the permuted matrix (right) using AMD ordering. + + +:class:`AMDInfo` Objects +------------------------ + +An :class:`AMDInfo` object is a dataclass returned by the :func:`amd` function +when the ``return_info`` parameter is set to ``True``. It contains information +about the AMD ordering, including the return status, the number of non-zeros in +the Cholesky factorization, and others. + +We can use :class:`AMDInfo` objects to compare the number of non-zeros in the +Cholesky factorization of the original matrix, without computing it directly: + +.. code:: python + + from sksparse.amd import amd + from sksparse.cholmod import cholesky + A = ... # some sparse matrix + N = A.shape[0] + p, info = amd(A, return_info=True) + PAPT_factor = cholesky(A[p][:, p]) + print("nnz in L of A: ", info.Lnz + N) + print("nnz in L of PAPT:", PAPT_factor.L().nnz) + +Typically, the :class:`AMDInfo` object is unnecessary, and you can just use the +permutation vector returned by :func:`amd`. + + +Convenience Methods +------------------- + +The AMD package also provides a convenience function, +:func:`amd_default_control` to get the default control parameters from the AMD +package. Most users will not need to use this function, as the default control +parameters are used automatically by :func:`amd`. + + +Error Handling +-------------- + +Errors raised by the AMD package are converted into Python exceptions. See +the :ref:`amd-exceptions` for details. + + +References +---------- +* Amestoy, P. R., Davis, T. A., & Duff, I. S. (1996). *An approximate minimum + degree ordering algorithm*. SIAM Journal on Matrix Analysis and Applications, + 17(4), 886-905. . diff --git a/doc/tutorial/btf.rst b/doc/tutorial/btf.rst new file mode 100644 index 00000000..bdf08647 --- /dev/null +++ b/doc/tutorial/btf.rst @@ -0,0 +1,78 @@ +.. Copyright (C) 2025, Bernard Roesler. All rights reserved. + Part of the scikit-sparse project. + See pyproject.toml for full author list and LICENSE.txt for license details. + SPDX-License-Identifier: BSD-2-Clause + +================================================= +Block Triangular Form (BTF) (:mod:`sksparse.btf`) +================================================= + +.. currentmodule:: sksparse.btf + + +The :mod:`sksparse.btf` module provides efficient implementations of the +Block Triangular Form (BTF) ordering algorithm for sparse, square matrices. + +It exposes the main functions of the `BTF package +`_, which +permutes a sparse matrix into upper block triangular form with a zero-free +diagonal, or with a maximum number of nonzeros along the diagonal if +a zero-free permutation does not exist. The BTF function accepts both real and +complex matrices, in any format supported by :mod:`scipy.sparse` (CSC format is +most efficient). + +.. _btf_github: https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/BTF + + +Quickstart +---------- + +If :math:`A` is a sparse, square matrix, then the +following code computes the AMD ordering of :math:`A`: + +.. code:: python + + from sksparse.btf import btf + A = ... # some sparse matrix + p, q, r = btf(A) + PAQ = A[p][:, q] + +to give the permuted matrix :math:`PAQ`, where :math:`P` is the permutation +matrix corresponding to the ordering :math:`p`, and similarly for :math:`q`. If +:math:`A` is structurally singular, then the permutation vector `q` will have +negative entries denoting the unmatched indices. To get the actual permutation, +use + +.. code:: python + + import numpy as np + from sksparse.btf import btf_q_permutation + q_idx = np.nonzero(q < 0)[0] # store the indices of negative entries + q = btf_q_permutation(q) # get the permutation vector + PAQ = A[p][:, q] # permute the matrix + + +Example +------- + +To see the effects of BTF ordering, we can load a sparse matrix from the +`SuiteSparse Matrix Collection `_ and compute its ordering. + +.. _SSMC: https://sparse.tamu.edu + +.. literalinclude:: examples/btf_example.py + :language: python + +This figure shows the effect of BTF ordering: + +.. figure:: examples/btf_example.svg + :alt: BTF Example + :align: center + :width: 90% + + +Convenience Methods +------------------- + +The BTF package also provides a convenience function, +:func:`btf_q_permutation`, to get the actual ``q`` permutation vector. diff --git a/doc/tutorial/camd.rst b/doc/tutorial/camd.rst new file mode 100644 index 00000000..5e8bf38f --- /dev/null +++ b/doc/tutorial/camd.rst @@ -0,0 +1,143 @@ +.. Copyright (C) 2025, Bernard Roesler. All rights reserved. + Part of the scikit-sparse project. + See pyproject.toml for full author list and LICENSE.txt for license details. + SPDX-License-Identifier: BSD-2-Clause + +============================================================================= +Constrained Approximate Minimum Degree (CAMD) Ordering (:mod:`sksparse.camd`) +============================================================================= + +.. currentmodule:: sksparse.camd + + +The :mod:`sksparse.camd` module provides an interface to the `Approximate +Minimum Degree (AMD) `_ ordering algorithm for sparse, square +matrices. + +It exposes the main function of the `CAMD package +`_, which +computes a symmetric ordering of a sparse matrix that minimizes the fill-in of +the Cholesky decomposition. The CAMD function accepts both real and complex +matrices, in any format supported by :mod:`scipy.sparse` (CSC format is most +efficient). + +This function is identical to that of the `AMD package +`_ +(:mod:`sksparse.amd`), except that it allows the user to specify a set of +constraints on the ordering of the matrix. + +.. _amd_paper: https://epubs.siam.org/doi/abs/10.1137/S0895479894278952 +.. _camd_github: https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/CAMD + + +Quickstart +---------- + +If :math:`A` is a sparse, square matrix, then the following code computes the +CAMD ordering of :math:`A`: + +.. code:: python + + from sksparse.camd import camd + A = ... # some sparse matrix + # Set some constraints, e.g., to fix the first K rows and columns + N = A.shape[0] + K = N // 2 # number of constrained variables + C = np.full(N, K) # none are constrained (all == K) + C[:K] = np.arange(K) # first K variables are constrained + p = camd(A, constraints=C) + PAPT = A[p][:, p] + +to give the permuted matrix :math:`PAP^T`, where :math:`P` is the permutation +matrix corresponding to the ordering :math:`p`. If :math:`A` is not symmetric, +then this is the same as CAMD computes the ordering of the symbolically +symmetric matrix :math:`A + A^T`. + +We can then continue from above to compute the Cholesky decompositions of the +original and permuted matrix using :func:`~sksparse.cholmod.cholesky`, and +compare the number of non-zeros in each: + +.. code:: python + + from sksparse.cholmod import cholesky + A_factor = cholesky(A) + PAPT_factor = cholesky(PAPT) + L = A_factor.L() + Lp = PAPT_factor.L() + print("Number of non-zeros in L: ", L.nnz) + print("Number of non-zeros in Lp:", Lp.nnz) + +The number of non-zeros in the Cholesky factorization of the permuted matrix +should be less than or equal to the number of non-zeros in the Cholesky +factorization of the original matrix, but this is not guaranteed. + + +Example +------- + +To see the effects of CAMD ordering, we can load a sparse matrix from the +`SuiteSparse Matrix Collection `_ and compute its CAMD ordering. + +.. _SSMC: https://sparse.tamu.edu + +.. literalinclude:: examples/camd_example.py + :language: python + +The figure shows the effect of CAMD ordering that reduces the fill-in of the +Cholesky factorization of a sparse matrix, while constraining some of the rows. + +.. figure:: examples/camd_example.svg + :alt: CAMD Example + :align: center + :width: 90% + + The number of non-zeros in the Cholesky factorization of the original matrix + (left) and the permuted matrix (right) using CAMD ordering. + + +:class:`CAMDInfo` Objects +------------------------- + +An :class:`CAMDInfo` object is a dataclass returned by the :func:`camd` +function when the ``return_info`` parameter is set to ``True``. It contains +information about the CAMD ordering, including the return status, the number of +non-zeros in the Cholesky factorization, and others. + +We can use :class:`CAMDInfo` objects to compare the number of non-zeros in the +Cholesky factorization of the original matrix, without computing it directly: + +.. code:: python + + from sksparse.camd import camd + from sksparse.cholmod import cholesky + A = ... # some sparse matrix + N = A.shape[0] + p, info = camd(A, return_info=True) + PAPT_factor = cholesky(A[p][:, p]) + print("nnz in L of A: ", info.Lnz + N) + print("nnz in L of PAPT:", PAPT_factor.L().nnz) + +Typically, the :class:`CAMDInfo` object is unnecessary, and you can just use +the permutation vector returned by :func:`camd`. + + +Convenience Methods +------------------- + +The CAMD package also provides a convenience function, +:func:`camd_default_control` to get the default control parameters from the +CAMD package. Most users will not need to use this function, as the default +control parameters are used automatically by :func:`camd`. + + +Error Handling +-------------- + +Errors raised by the CAMD package are converted into Python exceptions. See +the :ref:`camd-exceptions` for details. + +References +---------- +* Amestoy, P. R., Davis, T. A., & Duff, I. S. (1996). *An approximate minimum + degree ordering algorithm*. SIAM Journal on Matrix Analysis and Applications, + 17(4), 886-905. . diff --git a/doc/tutorial/ccolamd.rst b/doc/tutorial/ccolamd.rst new file mode 100644 index 00000000..18fbab89 --- /dev/null +++ b/doc/tutorial/ccolamd.rst @@ -0,0 +1,106 @@ +.. Copyright (C) 2025, Bernard Roesler. All rights reserved. + Part of the scikit-sparse project. + See pyproject.toml for full author list and LICENSE.txt for license details. + SPDX-License-Identifier: BSD-2-Clause + +========================================================================================== +Constrained Column Approximate Minimum Degree (CCOLAMD) Ordering (:mod:`sksparse.ccolamd`) +========================================================================================== + +.. currentmodule:: sksparse.ccolamd + + +The :mod:`sksparse.ccolamd` module provides efficient an implementation of the +`Column Approximate Minimum Degree (CCOLAMD) `_ ordering +algorithm for sparse matrices. + +It exposes the main functions of the `CCOLAMD package `_, +which computes a column ordering :math:`Q` of a sparse matrix that minimizes +the fill-in of the Cholesky decomposition of :math:`(AQ)^{\top}(AQ)`. The +:func:`.ccolamd` function is appropriate for use with non-symmetric and +non-square matrices, for LU factorization, QR factorization, and other +decompositions that require a column ordering. + +This module also provides a symmetric variant, :func:`.csymamd`, which computes +a permutation `P` of a symmetric matrix `A` such that the Cholesky +factorization of :math:`PAP^{\top}` has less fill-in and requires fewer +floating point operations than `A`. This function assumes that its input is +symmetric. + +The :func:`.ccolamd` and :func:`.csymamd` functions accept both real and +complex matrices, in any format supported by :mod:`scipy.sparse` (CSC format is +most efficient). + +These function are identical to that of the `COLAMD package `_ +(:mod:`sksparse.colamd`), except that they allow the user to specify a set of +constraints on the ordering of the matrix. + +.. _colamd_paper: https://dl.acm.org/doi/abs/10.1145/1024074.1024079 +.. _colamd_github: https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/COLAMD +.. _ccolamd_github: https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/CCOLAMD + + +Quickstart +---------- + +If :math:`A` is a sparse matrix, then the +following code computes the CCOLAMD ordering of :math:`A`: + +.. code:: python + + from sksparse.ccolamd import ccolamd + A = ... # some sparse matrix + # Set some constraints, e.g., to fix the first K rows and columns + N = A.shape[0] + K = N // 2 # number of constrained variables + C = np.full(N, K) # none are constrained (all == K) + C[:K] = np.arange(K) # first K variables are constrained + q = ccolamd(A, constraints=C) + AQ = A[:, q] # permute the columns of A + +to give the permuted matrix :math:`AQ`, where :math:`Q` is the permutation +matrix corresponding to the ordering :math:`q`. + +We can then continue from above to compute the LU decompositions of the +original and permuted matrix, and compare the number of non-zeros in each: + +.. code:: python + + from scipy.sparse.linalg import splu + lu = splu(A) + luq = splu(A[:, q]) + L, U = lu.L, lu.U + Lq, Uq = luq.L, luq.U + print("Number of non-zeros in L + U: ", (L + U).nnz) + print("Number of non-zeros in Lq + Uq:", (Lq + Uq).nnz) + +The number of non-zeros in the LU factorization of the permuted matrix +should be less than or equal to the number of non-zeros in the LU +factorization of the original matrix, but this is not guaranteed. + + +:class:`CCOLAMDStats` Objects +----------------------------- + +An :class:`CCOLAMDStats` object is a dataclass returned by the :func:`ccolamd` +function when the ``return_info`` parameter is set to ``True``. It contains +information about the ordering, including the return status. + +Typically, the :class:`CCOLAMDStats` object is unnecessary, and you can +just use the permutation vector returned by :func:`ccolamd`. + + +Convenience Methods +------------------- + +The CCOLAMD package also provides a convenience function, +:func:`ccolamd_get_defaults` to get the default control parameters from the +CCOLAMD package. Most users will not need to use this function, as the default +control parameters are used automatically by :func:`ccolamd`. + + +Error Handling +-------------- + +Errors raised by the CCOLAMD package are converted into Python exceptions. See +the :ref:`ccolamd-exceptions` for details. diff --git a/doc/tutorial/cholmod.rst b/doc/tutorial/cholmod.rst new file mode 100644 index 00000000..a0b783e6 --- /dev/null +++ b/doc/tutorial/cholmod.rst @@ -0,0 +1,203 @@ +================================================ +Cholesky Decomposition (:mod:`sksparse.cholmod`) +================================================ + +.. currentmodule:: sksparse.cholmod + + +The :mod:`sksparse.cholmod` module provides an interface to the SuiteSparse +`CHOLMOD `_ package, which computes basic linear algebra +operations for sparse, symmetric, positive-definite matrices. + +The main function of this module is to compute the `Cholesky factor +`_ :math:`L` of a sparse, +symmetric (Hermitian if complex), positive-definite matrix :math:`A` with +a fill-reducing permutation :math:`P`, such that: + +.. math:: + + LL^{\top} = PAP^{\top}. + +For matrices that are symmetric but may be numerically close to semi-definite, +the module can compute the LDL factorization: + +.. math:: + + LDL^{\top} = PAP^{\top}. + +Either of these factors can then be used to solve linear systems of the form +:math:`Ax = b`. + +The :mod:`.cholmod` module exposes most of the capabilities of the CHOLMOD +package including: + +* Computation of the Cholesky factor with fill-reducing + permutation for both real and complex sparse matrices :math:`A`, in any + format supported by :mod:`scipy.sparse`. +* An interface for using this decomposition to solve problems of the form + :math:`Ax = b`. +* Functions to compute a rank-:math:`k` "update" or "downdate" of the Cholesky + factor. +* The ability to perform the fill-reduction analysis once, and then + re-use it to efficiently decompose many matrices with the same pattern of + non-zero entries. + +This wrapper handles both 32-bit and 64-bit integer types, depending on the +input matrix format. + +.. _cholmod_github: https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/CHOLMOD +.. _cholesky_wiki: http://en.wikipedia.org/wiki/Cholesky_decomposition + + +Quickstart +---------- + +If :math:`A` is a sparse, symmetric, positive-definite matrix, then the +following code computes the Cholesky decomposition of :math:`A`: + +.. code:: python + + from sksparse.cholmod import cholesky + L = cholesky(A) + +or, with a fill-reducing permutation: + +.. code:: python + + L, p = cholesky(A, order="default") + +See the :ref:`example ` below for a demonstration of the +effect of the fill-reducing permutation. + +Once the factorization has been computed, it can be used to solve a linear +system: + +.. code:: python + + from sksparse.cholmod import ldl_factor + A = ... # a symmetric, positive-definite sparse matrix + b = ... # right-hand side + f = ldl_factor(A) # compute LDL^T factorization + x = f.solve(b) # solve Ax = b + + +Examples +-------- + +.. _cholesky-example: + +Cholesky Example +++++++++++++++++ + +To see how to use the Cholesky factorization, we can load a sparse matrix +from the `SuiteSparse Matrix Collection `_ and compute its ordering. + +.. _SSMC: https://sparse.tamu.edu + +.. literalinclude:: examples/cholmod_example.py + :language: python + +This figure shows the effect of AMD ordering that reduces the fill-in of the +Cholesky factorization of a sparse matrix. + +.. figure:: examples/cholesky_example.svg + :alt: Cholesky Example with AMD Ordering + :align: center + :width: 90% + + The number of non-zeros in the Cholesky factorization of the original matrix + (left) and the permuted matrix (right) using AMD ordering. + + +Nested Dissection Example ++++++++++++++++++++++++++ + +To see the effects of nested dissection ordering, we can load a sparse matrix +and compute its ordering. + +.. literalinclude:: examples/nesdis_example.py + :language: python + +This figure shows the effect of nested dissection ordering that reduces the +fill-in of the LU factorization of a sparse matrix in a case where the AMD +order *does not* help. + +.. figure:: examples/nesdis_example.svg + :alt: LU Example with Nesdis Ordering + :align: center + :width: 90% + + The number of non-zeros in the LU factorization of the original matrix + and the permuted matrix using AMD and nested dissection ordering. + + +Function Interface +------------------ + +For users who want to directly compute the factorization without needing to +manipulate the :class:`CholeskyFactor` object, the :mod:`.cholmod` module +provides the :func:`cholesky` and :func:`ldl` functions that perform both the +symbolic analysis and the numerical factorization in one step, and return the +matrices directly. + + +Object Interface +---------------- + +For more advanced usage, users can instantiate the :class:`CholeskyFactor` +class. This class can be instantiated directly using its constructor, or more +conveniently using the :func:`cho_factor` or :func:`ldl_factor` functions. + +When instantiated directly, the constructor performs a symbolic analysis of the +matrix, but does not compute the numerical factorization. The numerical +factorization is then performed by calling the :meth:`.CholeskyFactor.factorize` method. + +The :func:`cho_factor` and :func:`ldl_factor` functions perform both the +symbolic analysis and the numerical factorization in one step, and return an +instance of the :class:`CholeskyFactor` class. + +The resulting :class:`CholeskyFactor` object can then be used to solve linear +systems using its :meth:`CholeskyFactor.solve` method, or to update the +factorization in-place using the :meth:`.update`, :meth:`.rowadd`, +:meth:`.rowdel`, and :meth:`.resymbol` methods. + +The :meth:`CholeskyFactor.factorize` method can be called again to factor a new +matrix with the same sparsity pattern. + + +Symbolic Analysis +----------------- + +In addition to numerical factorization, :mod:`.cholmod` provides symbolic +operations :func:`symbfact`, and :func:`etree` that can be used to analyze the +structure of the Cholesky factor and to compute fill-reducing permutations. + + +Graph Partitioning +------------------ + +The :mod:`.cholmod` module also includes functions for graph partitioning and +node reordering, which can be used like the :mod:`~sksparse.amd` and +:mod:`~sksparse.colamd` (and their constrained counterparts) modules to reduce +fill-in during factorization. + +These functions provide a direct interface to the corresponding CHOLMOD +functions that are used internally by :func:`cholesky` and :func:`ldl` when the +``order`` argument is specified. The functions :func:`bisect`, :func:`metis`, +and :func:`nesdis` can be used to compute fill-reducing orderings, and the +:class:`SeparatorTree` class represents the resulting separator tree. + + +Exceptions and Warnings +----------------------- + +Warnings issued by CHOLMOD are converted into Python warnings of +type :exc:`CholmodWarning`. The module will also issue +a :exc:`~scipy.sparse.SparseEfficiencyWarning` if the input matrix is not +a :class:`~scipy.sparse.csc_array` (note that the +:class:`~scipy.sparse.csc_matrix` class is not supported, as it will be +deprecated and is not recommended for use in new code). + +Errors detected by CHOLMOD or by our wrapper code are converted into exceptions +of type :exc:`CholmodError` or an appropriate subclass. See the +:ref:`cholmod-exceptions` for details. diff --git a/doc/tutorial/colamd.rst b/doc/tutorial/colamd.rst new file mode 100644 index 00000000..adc49a93 --- /dev/null +++ b/doc/tutorial/colamd.rst @@ -0,0 +1,118 @@ +.. Copyright (C) 2025, Bernard Roesler. All rights reserved. + Part of the scikit-sparse project. + See pyproject.toml for full author list and LICENSE.txt for license details. + SPDX-License-Identifier: BSD-2-Clause + +============================================================================ +Column Approximate Minimum Degree (COLAMD) Ordering (:mod:`sksparse.colamd`) +============================================================================ + +.. currentmodule:: sksparse.colamd + + +The :mod:`sksparse.colamd` module provides efficient an implementation of the +`Column Approximate Minimum Degree (COLAMD) `_ ordering +algorithm for sparse matrices. + +It exposes the main functions of the `COLAMD package `_, which +computes a column ordering :math:`Q` of a sparse matrix that minimizes the +fill-in of the Cholesky decomposition of :math:`(AQ)^{\top}(AQ)`. The +:func:`.colamd` function is appropriate for use with non-symmetric and +non-square matrices, for LU factorization, QR factorization, and other +decompositions that require a column ordering. + +This module also provides a symmetric variant, :func:`.symamd`, which computes a +permutation `P` of a symmetric matrix `A` such that the Cholesky factorization +of :math:`PAP^{\top}` has less fill-in and requires fewer floating point +operations than `A`. This function assumes that its input is symmetric. + +The :func:`.colamd` and :func:`.symamd` functions accept both real and complex +matrices, in any format supported by :mod:`scipy.sparse` (CSC format is most +efficient). + +.. _colamd_paper: https://dl.acm.org/doi/abs/10.1145/1024074.1024079 +.. _colamd_github: https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/COLAMD + + +Quickstart +---------- + +If :math:`A` is a sparse matrix, then the +following code computes the COLAMD ordering of :math:`A`: + +.. code:: python + + from sksparse.colamd import colamd + A = ... # some sparse matrix + q = colamd(A) + AQ = A[:, q] # permute the columns of A + +to give the permuted matrix :math:`AQ`, where :math:`Q` is the permutation +matrix corresponding to the ordering :math:`q`. + +We can then continue from above to compute the LU decompositions of the +original and permuted matrix, and compare the number of non-zeros in each: + +.. code:: python + + from scipy.sparse.linalg import splu + lu = splu(A) + luq = splu(A[:, q]) + L, U = lu.L, lu.U + Lq, Uq = luq.L, luq.U + print("Number of non-zeros in L + U: ", (L + U).nnz) + print("Number of non-zeros in Lq + Uq:", (Lq + Uq).nnz) + +The number of non-zeros in the LU factorization of the permuted matrix +should be less than or equal to the number of non-zeros in the LU +factorization of the original matrix, but this is not guaranteed. + + +Example +------- + +To see the effects of COLAMD ordering, we can load a sparse matrix from the +`SuiteSparse Matrix Collection `_ and compute its ordering. + +.. _SSMC: https://sparse.tamu.edu + +.. literalinclude:: examples/colamd_example.py + :language: python + +This figure shows the effect of COLAMD ordering that reduces the fill-in of the +Cholesky factorization of a sparse matrix. + +.. figure:: examples/colamd_example.svg + :alt: COLAMD Example + :align: center + :width: 90% + + The number of non-zeros in the LU factorization of the original matrix + (left) and the permuted matrix (right) using COLAMD ordering. + + +:class:`COLAMDStats` Objects +---------------------------- + +An :class:`COLAMDStats` object is a dataclass returned by the :func:`colamd` +function when the ``return_info`` parameter is set to ``True``. It contains +information about the ordering, including the return status. + +Typically, the :class:`COLAMDStats` object is unnecessary, and you can +just use the permutation vector returned by :func:`colamd`. + + +Convenience Methods +------------------- + +The COLAMD package also provides a convenience function, +:func:`colamd_get_defaults` to get the default control parameters from the +COLAMD package. Most users will not need to use this function, as the default +control parameters are used automatically by :func:`colamd`. + + +Error Handling +-------------- + +Errors raised by the COLAMD package are converted into Python exceptions. See +the :ref:`colamd-exceptions` for details. diff --git a/doc/tutorial/examples/amd_example.py b/doc/tutorial/examples/amd_example.py new file mode 100644 index 00000000..d3fe0bb9 --- /dev/null +++ b/doc/tutorial/examples/amd_example.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: amd_example.py +# Created: 2025-07-29 12:33 +# ============================================================================= + +"""An example of using the AMD (Approximate Minimum Degree) algorithm to find +a fill-reducing ordering of a sparse matrix. +""" + +from pathlib import Path + +import matplotlib.pyplot as plt +from scipy.io import mmread + +from sksparse.amd import amd +from sksparse.cholmod import cholesky + +# Load the bcsstk06 matrix (downloaded from the SuiteSparse Matrix Collection: +# ) +filepath = Path("data") / "bcsstk06.mtx" +A = mmread(filepath, spmatrix=False).tocsc() # read the matrix + +p = amd(A) # compute the AMD ordering +PAPT = A[p][:, p] # apply the ordering to the matrix + +# Compute the Cholesky factorization of each matrix +L = cholesky(A, lower=True) +Lp = cholesky(PAPT, lower=True) + +# Plot the original and permuted matrices +plt.rcParams.update({"font.size": 10}) + +fig, axs = plt.subplots(num=1, nrows=2, ncols=2, clear=True) +fig.set_size_inches((6, 6), forward=True) +fig.set_constrained_layout(True) +fig.suptitle("AMD Example: Fill-Reducing Ordering of bcsstk06") + +ax = axs[0, 0] +ax.spy(A, markersize=1) +ax.set_title(r"Original Matrix $A$") + +ax = axs[0, 1] +ax.spy(PAPT, markersize=1) +ax.set_title(r"Permuted Matrix $PAP^T$") + +ax = axs[1, 0] +ax.spy(L, markersize=1) +ax.set_title(r"Original Cholesky Factor $L$") +ax.set_xlabel(f"{L.nnz:,} non-zeros") + +ax = axs[1, 1] +ax.spy(Lp, markersize=1) +ax.set_title(r"Permuted Cholesky Factor $L_p$") +ax.set_xlabel(f"{Lp.nnz:,} non-zeros") + +for ax in axs.flat: + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + +plt.show() +fig.savefig("amd_example.svg") diff --git a/doc/tutorial/examples/amd_example.svg b/doc/tutorial/examples/amd_example.svg new file mode 100644 index 00000000..2d14ae9b --- /dev/null +++ b/doc/tutorial/examples/amd_example.svg @@ -0,0 +1,43106 @@ + + + + + + + + 2025-09-09T14:23:16.049168 + image/svg+xml + + + Matplotlib v3.10.5, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorial/examples/btf_example.py b/doc/tutorial/examples/btf_example.py new file mode 100644 index 00000000..97695dfa --- /dev/null +++ b/doc/tutorial/examples/btf_example.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: btf_example.py +# Created: 2025-08-07 14:03 +# ============================================================================= + +"""An example of using the BTF (Block Triangular Form) algorithm.""" + +from pathlib import Path + +import matplotlib.pyplot as plt +from scipy.io import mmread + +from sksparse.btf import btf, btf_q_permutation, maxtrans, strongcomp + + +def plot_btf(A, p, q, r, ax=None): + """Plot the blocks of the BTF matrix. + + Adapted from the SuiteSparse ``drawbtf.m`` [#drawbtf]_. + + Parameters + ---------- + A : (N, N) sparse matrix + A square, sparse matrix in CSC format. + p, q : (N,) ndarray of int + The row and column permutations that put ``A`` into upper block + triangular form. + r : (Nb + 1,) ndarray of int + The indices of the block boundaries in the permuted matrix. The number + of blocks is ``len(r) - 1``. + ax : matplotlib.axes.Axes, optional + The axes to plot on. If None, the current axes will be used. + + Returns + ------- + ax : matplotlib.axes.Axes + The axes with the BTF blocks plotted. + + References + ---------- + .. [#drawbtf] MATLAB BTF plotting function: + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/BTF/MATLAB/drawbtf.m + """ + if ax is None: + ax = plt.gca() + + Nb = len(r) - 1 + q = btf_q_permutation(q) # ensure q is a valid permutation + + for k in range(Nb): + r0 = r[k] + r1 = r[k + 1] + Nk = r1 - r0 + if Nk > 1: + # Plot the block + ax.add_patch( + plt.Rectangle( + (r0 - 0.5, r0 - 0.5), + Nk, + Nk, + fill=False, + edgecolor="C3", + linewidth=1.5, + ) + ) + + return ax + + +# Load the west0479 matrix (downloaded from the SuiteSparse Matrix Collection: +# ) +filepath = Path("data") / "west0479.mtx" +A = mmread(filepath, spmatrix=False).tocsc() # read the matrix + +# Compute the BTF ordering +p, q, r = btf(A) +q = btf_q_permutation(q) +PAQ = A[p][:, q] + +# Compute the maximum transversal +qm = maxtrans(A) +A_mt = A[:, qm] + +# Compute the strongly connected components +pcc, rcc = strongcomp(A) +A_scc = A[pcc][:, pcc] + +# Plot the original and permuted matrices +plt.rcParams.update({"font.size": 10}) + +fig, axs = plt.subplots(num=1, nrows=2, ncols=2, clear=True) +fig.set_size_inches((6, 6), forward=True) +fig.set_constrained_layout(True) +fig.suptitle("BTF Example: Block Triangular Ordering of west0479") + +ax = axs[0, 0] +ax.spy(A, markersize=1) +ax.set_title(r"Original Matrix $A$") + +ax = axs[0, 1] +ax.spy(PAQ, markersize=1) +plot_btf(PAQ, p, q, r, ax=ax) +ax.set_title(r"BTF Matrix $PAQ$") + +ax = axs[1, 0] +ax.spy(A_mt, markersize=1) +ax.set_title(r"Maximum Transveral") + +ax = axs[1, 1] +ax.spy(A_scc, markersize=1) +plot_btf(A_scc, pcc, pcc, rcc, ax=ax) +ax.set_title(r"Strongly Connected Components") + +for ax in axs.flat: + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + +plt.show() +fig.savefig("btf_example.svg") diff --git a/doc/tutorial/examples/btf_example.svg b/doc/tutorial/examples/btf_example.svg new file mode 100644 index 00000000..2702fb92 --- /dev/null +++ b/doc/tutorial/examples/btf_example.svg @@ -0,0 +1,9161 @@ + + + + + + + + 2025-08-08T08:59:11.968329 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorial/examples/camd_example.py b/doc/tutorial/examples/camd_example.py new file mode 100644 index 00000000..6c2803ce --- /dev/null +++ b/doc/tutorial/examples/camd_example.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: camd_example.py +# Created: 2025-09-30 10:16 +# ============================================================================= + +"""An example of using the AMD (Approximate Minimum Degree) algorithm to find +a fill-reducing ordering of a sparse matrix. +""" + +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +from scipy.io import mmread + +from sksparse.camd import camd +from sksparse.cholmod import cholesky + +# Load the bcsstk06 matrix (downloaded from the SuiteSparse Matrix Collection: +# ) +filepath = Path("data") / "bcsstk06.mtx" +A = mmread(filepath, spmatrix=False).tocsc() # read the matrix + +N = A.shape[0] +K = N // 2 # number of constrained variables +C = np.full(N, K) # fully constrained +C[:K] = np.arange(K) # first K variables are constrained +Nc = np.count_nonzero(C < K) # number of constrained variables + +p = camd(A, constraints=C) # compute the AMD ordering +PAPT = A[p][:, p] # apply the ordering to the matrix + +# Compute the Cholesky factorization of each matrix +L = cholesky(A, lower=True) +Lp = cholesky(PAPT, lower=True) + +# Plot the original and permuted matrices +plt.rcParams.update({"font.size": 10}) + +fig, axs = plt.subplots(num=1, nrows=2, ncols=2, clear=True) +fig.set_size_inches((6, 6), forward=True) +fig.set_constrained_layout(True) +fig.suptitle( + f"CAMD Example: Fill-Reducing Ordering of bcsstk06\n{Nc} constrained variables" +) + +ax = axs[0, 0] +ax.spy(A, markersize=1) +ax.set_title(r"Original Matrix $A$") + +ax = axs[0, 1] +ax.spy(PAPT, markersize=1) +ax.set_title(r"Permuted Matrix $PAP^T$") + +ax = axs[1, 0] +ax.spy(L, markersize=1) +ax.set_title(r"Original Cholesky Factor $L$") +ax.set_xlabel(f"{L.nnz:,} non-zeros") + +ax = axs[1, 1] +ax.spy(Lp, markersize=1) +ax.set_title(r"Permuted Cholesky Factor $L_p$") +ax.set_xlabel(f"{Lp.nnz:,} non-zeros") + +for ax in axs.flat: + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + +plt.show() +fig.savefig("camd_example.svg") diff --git a/doc/tutorial/examples/camd_example.svg b/doc/tutorial/examples/camd_example.svg new file mode 100644 index 00000000..f4e04334 --- /dev/null +++ b/doc/tutorial/examples/camd_example.svg @@ -0,0 +1,44428 @@ + + + + + + + + 2025-09-30T10:13:27.490786 + image/svg+xml + + + Matplotlib v3.10.6, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorial/examples/cholesky_example.svg b/doc/tutorial/examples/cholesky_example.svg new file mode 100644 index 00000000..6b4cfe05 --- /dev/null +++ b/doc/tutorial/examples/cholesky_example.svg @@ -0,0 +1,83651 @@ + + + + + + + + 2025-08-14T09:51:33.379307 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorial/examples/cholmod_example.py b/doc/tutorial/examples/cholmod_example.py new file mode 100644 index 00000000..a708b308 --- /dev/null +++ b/doc/tutorial/examples/cholmod_example.py @@ -0,0 +1,71 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: cholmod_example.py +# Created: 2025-08-12 20:12 +# ============================================================================= + +"""An example of using CHOLMOD to compute the Cholesky factorization of a +sparse matrix. +""" + +from pathlib import Path + +import matplotlib.pyplot as plt +from numpy.testing import assert_allclose +from scipy.io import mmread + +from sksparse.cholmod import cholesky + +# Load the west0479 matrix (downloaded from the SuiteSparse Matrix Collection: +# ) +filepath = Path("data") / "west0479.mtx" +A = mmread(filepath, spmatrix=False) # read the matrix +A = (A.T @ A).tocsc() # make it symmetric positive definite + +# compute the Cholesky factorization +R = cholesky(A) +Rp, p = cholesky(A, order="amd") + +PAPT = A[p][:, p] # apply the ordering to the matrix + +# Make sure the factorization is correct +assert_allclose((R.T.conj() @ R).toarray(), A.toarray(), atol=1e-9) +assert_allclose((Rp.T.conj() @ Rp).toarray(), PAPT.toarray(), atol=1e-9) + +# Plot the original and permuted matrices +plt.rcParams.update({"font.size": 10}) + +fig, axs = plt.subplots(num=1, nrows=2, ncols=2, clear=True) +fig.set_size_inches((6, 6), forward=True) +fig.set_constrained_layout(True) +fig.suptitle("CHOLMOD Example: Cholesky Factor of west0479") + +ax = axs[0, 0] +ax.spy(A, markersize=1) +ax.set_title(r"Original Matrix $A$") + +ax = axs[0, 1] +ax.spy(PAPT, markersize=1) +ax.set_title(r"Permuted Matrix $PAP^{\top}$") + +ax = axs[1, 0] +ax.spy(R, markersize=1) +ax.set_title(r"Original Cholesky Factor $R$") +ax.set_xlabel(f"{R.nnz:,} non-zeros") + +ax = axs[1, 1] +ax.spy(Rp, markersize=1) +ax.set_title(r"Permuted R Factor $R_p$") +ax.set_xlabel(f"{Rp.nnz:,} non-zeros") + +for ax in axs.flat: + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + +plt.show() +fig.savefig("cholesky_example.svg") diff --git a/doc/tutorial/examples/cholmod_mini_example.py b/doc/tutorial/examples/cholmod_mini_example.py new file mode 100644 index 00000000..e31682ac --- /dev/null +++ b/doc/tutorial/examples/cholmod_mini_example.py @@ -0,0 +1,75 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: cholmod_mini_example.py +# Created: 2025-08-13 10:13 +# ============================================================================= + +"""An example of using CHOLMOD to compute the Cholesky factorization of a +sparse matrix. +""" + +import matplotlib.pyplot as plt +from numpy.testing import assert_allclose +from scipy import sparse +from scipy.sparse.linalg import LaplacianNd + +from sksparse.cholmod import cholesky + +# Create (negative) Laplacian matrix that is symmetric positive definite +N = 5 +G = LaplacianNd((N, N)) +A = -G.tosparse().tocsc().astype(float) +A.setdiag(A.diagonal() + 1) # make it positive definite + +# Make it complex +A = A + 0.5j * sparse.tril(A) +A = 0.5 * (A + A.conj().T) # ensure symmetry +A = A.tocsc() + +# compute the Cholesky factorization +R = cholesky(A) +Rp, p = cholesky(A, order="amd") + +PAPT = A[p][:, p] # apply the ordering to the matrix + +# Make sure the factorization is correct +assert_allclose((R.T.conj() @ R).toarray(), A.toarray(), atol=1e-15) +assert_allclose((Rp.T.conj() @ Rp).toarray(), PAPT.toarray(), atol=1e-15) + +# Plot the original and permuted matrices +plt.rcParams.update({"font.size": 10}) +MSIZE = 5 # marker size for the spy plots + +fig, axs = plt.subplots(num=1, nrows=2, ncols=2, clear=True) +fig.set_size_inches((6, 6), forward=True) +fig.set_constrained_layout(True) +fig.suptitle("CHOLMOD Example: Cholesky Factor of Laplacian Matrix") + +ax = axs[0, 0] +ax.spy(A, markersize=MSIZE) +ax.set_title(r"Original Matrix $A$") + +ax = axs[0, 1] +ax.spy(PAPT, markersize=MSIZE) +ax.set_title(r"Permuted Matrix $PAP^{\top}$") + +ax = axs[1, 0] +ax.spy(R, markersize=MSIZE) +ax.set_title(r"Original Cholesky Factor $R$") +ax.set_xlabel(f"{R.nnz:,} non-zeros") + +ax = axs[1, 1] +ax.spy(Rp, markersize=MSIZE) +ax.set_title(r"Permuted R Factor $R_p$") +ax.set_xlabel(f"{Rp.nnz:,} non-zeros") + +for ax in axs.flat: + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + +plt.show() diff --git a/doc/tutorial/examples/colamd_example.py b/doc/tutorial/examples/colamd_example.py new file mode 100644 index 00000000..82577137 --- /dev/null +++ b/doc/tutorial/examples/colamd_example.py @@ -0,0 +1,73 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: colamd_example.py +# Created: 2025-07-31 14:18 +# ============================================================================= + +"""An example of using the COLAMD (Column Approximate Minimum Degree) algorithm +to find a fill-reducing ordering of a sparse matrix. +""" + +from pathlib import Path + +import matplotlib.pyplot as plt +from scipy.io import mmread +from scipy.sparse.linalg import splu + +from sksparse.colamd import colamd + +# from numpy.testing import assert_allclose, assert_array_equal + + +# Load the west0479 matrix (downloaded from the SuiteSparse Matrix Collection: +# ) +filepath = Path("data") / "west0479.mtx" +A = mmread(filepath, spmatrix=False).tocsc() # read the matrix + +q = colamd(A) # compute the COLAMD ordering +AQ = A[:, q] # apply the column ordering to the matrix + +# Compute the LU factorization of the original and permuted matrices +lu = splu(A, permc_spec="NATURAL") +L_, U_ = lu.L, lu.U + +luq = splu(AQ, permc_spec="NATURAL") +Lq, Uq = luq.L, luq.U + +# Plot the original and permuted matrices +plt.rcParams.update({"font.size": 10}) + +fig, axs = plt.subplots(num=1, nrows=2, ncols=2, clear=True) +fig.set_size_inches((6, 6), forward=True) +fig.set_constrained_layout(True) +fig.suptitle("COLAMD Example: Fill-Reducing Ordering of west0479") + +ax = axs[0, 0] +ax.spy(A, markersize=1) +ax.set_title(r"Original Matrix $A$") + +ax = axs[0, 1] +ax.spy(AQ, markersize=1) +ax.set_title(r"Permuted Matrix $AQ$") + +ax = axs[1, 0] +ax.spy(L_ + U_, markersize=1) +ax.set_title(r"Original LU Factors $L + U$") +ax.set_xlabel(f"{(L_ + U_).nnz:,} total non-zeros") + +ax = axs[1, 1] +ax.spy(Lq + Uq, markersize=1) +ax.set_title(r"Permuted LU Factors $L_q + U_q$") +ax.set_xlabel(f"{(Lq + Uq).nnz:,} total non-zeros") + +for ax in axs.flat: + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + +plt.show() +fig.savefig("colamd_example.svg") diff --git a/doc/tutorial/examples/colamd_example.svg b/doc/tutorial/examples/colamd_example.svg new file mode 100644 index 00000000..597d8059 --- /dev/null +++ b/doc/tutorial/examples/colamd_example.svg @@ -0,0 +1,29111 @@ + + + + + + + + 2025-07-31T15:02:08.550588 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorial/examples/data/bcsstk06.mtx b/doc/tutorial/examples/data/bcsstk06.mtx new file mode 100644 index 00000000..0aea9b19 --- /dev/null +++ b/doc/tutorial/examples/data/bcsstk06.mtx @@ -0,0 +1,4154 @@ +%%MatrixMarket matrix coordinate real symmetric +%------------------------------------------------------------------------------- +% UF Sparse Matrix Collection, Tim Davis +% http://www.cise.ufl.edu/research/sparse/matrices/HB/bcsstk06 +% name: HB/bcsstk06 +% [SYMMETRIC STIFFNESS MATRIX, MEDIUM TEST PROBLEM, LUMPED MASSES] +% id: 28 +% date: 1982 +% author: J. Lewis +% ed: I. Duff, R. Grimes, J. Lewis +% fields: title A name id date author ed kind +% kind: structural problem +%------------------------------------------------------------------------------- +420 420 4140 +1 1 1064685280.47 +5 1 10097137.073 +7 1 449885280.472 +2 2 427728386.378 +4 2 -4238867.37393 +8 2 208101719.712 +3 3 1.044e6 +9 3 -1.044e6 +4 4 642010.94971 +5 4 -7.5453722119e-9 +6 4 -139397.060211 +7 4 4635264.30435 +8 4 -4238867.37393 +9 4 2914870.77518 +10 4 -87659.3847246 +12 4 139397.060211 +13 4 4635264.30435 +15 4 2914870.77518 +46 4 -497833.333333 +47 4 7.5453722119e-9 +48 4 3.77272463354e-9 +49 4 7.00812794124e-26 +50 4 -4.98230079945e-11 +51 4 1.08892637781e-10 +5 5 4626538.74061 +6 5 -846.8959922 +7 5 10097137.073 +8 5 101627.519064 +9 5 57997.4828172 +11 5 -4491426.93395 +46 5 7.5453722119e-9 +47 5 -483.31235681 +48 5 846.8959922 +49 5 1.98006486354e-9 +50 5 101627.519064 +51 5 57997.4828172 +6 6 8031000.16844 +7 6 -9839944.65487 +8 6 -209828.755322 +9 6 -4736891.82342 +10 6 139397.060211 +12 6 -295918.262146 +13 6 -9839944.65487 +15 6 -4635264.30435 +46 6 3.77272463354e-9 +47 6 846.8959922 +48 6 -1748.57296102 +49 6 -3.95088310528e-9 +50 6 -209828.755323 +51 6 -101627.519064 +7 7 1572032934.94 +8 7 -3.58058582794e-6 +9 7 241034053.726 +10 7 -4635264.30435 +11 7 -4.08492899983e-6 +12 7 9839944.65487 +13 7 147056055.205 +15 7 67231775.3905 +46 7 -7.00812793411e-26 +47 7 -1.98006486354e-9 +48 7 3.95088310528e-9 +49 7 -3335 +50 7 3.13049447079e-7 +51 7 1.56890899053e-7 +8 8 461552214.933 +9 8 16341500.8471 +10 8 7.97257459677e-8 +12 8 -1.26780887705e-7 +13 8 -4.21574832612e-6 +14 8 -89130.8412912 +15 8 -2.65105952638e-6 +46 8 4.98230080059e-11 +47 8 -101627.519064 +48 8 209828.755322 +49 8 3.13049447079e-7 +50 8 16624203.5638 +51 8 8049103.72824 +9 9 157604548.934 +10 9 -2914870.77518 +12 9 4635264.30435 +13 9 67231775.3905 +15 9 46611942.19 +46 9 -1.08892637791e-10 +47 9 -57997.4828172 +48 9 101627.519064 +49 9 1.56890899053e-7 +50 9 8049103.72824 +51 9 4598861.67962 +10 10 1135943.76945 +11 10 -7.28401282128e-9 +12 10 -278794.120422 +13 10 5.36441802978e-7 +14 10 -2.04596680925e-9 +15 10 3.12526194407e-7 +16 10 -87659.3847246 +18 10 139397.060211 +19 10 4635264.30435 +21 10 2914870.77518 +52 10 -960625 +53 10 7.28401282128e-9 +54 10 7.26364484235e-9 +55 10 -1.85245689307e-23 +56 10 -2.04596680923e-9 +57 10 -3.98190663434e-10 +11 11 8984214.83699 +12 11 -1798.78435511 +13 11 2.87377901778e-9 +14 11 215854.122614 +15 11 163316.290872 +17 11 -4491426.93395 +52 11 7.28401282128e-9 +53 11 -1360.9690906 +54 11 1798.78435511 +55 11 2.87377901778e-9 +56 11 215854.122614 +57 11 163316.290872 +12 12 595884.872943 +13 12 -1.07820154245e-6 +14 12 -485801.838229 +15 12 -215854.122614 +16 12 139397.060211 +18 12 -295918.262146 +19 12 -9839944.65487 +21 12 -4635264.30435 +52 12 7.26364484235e-9 +53 12 1798.78435511 +54 12 -4048.34865191 +55 12 -5.31793649045e-9 +56 12 -485801.838229 +57 12 -215854.122614 +13 13 1014705603.94 +14 13 8.58829443215e-7 +15 13 482068107.452 +16 13 -4635264.30435 +18 13 9839944.65487 +19 13 147056055.205 +21 13 67231775.3905 +52 13 1.85245689307e-23 +53 13 -2.87377901778e-9 +54 13 5.31793649045e-9 +55 13 -16965 +56 13 4.17475314493e-7 +57 13 2.2595414902e-7 +14 14 78619455.2073 +15 14 34890926.5662 +20 14 -89130.8412912 +52 14 2.04596680925e-9 +53 14 -215854.122614 +54 14 485801.838229 +55 14 4.17475314493e-7 +56 14 38151247.6502 +57 14 16914062.8611 +15 15 320794261.443 +16 15 -2914870.77518 +18 15 4635264.30435 +19 15 67231775.3905 +21 15 46611942.19 +52 15 3.98190663417e-10 +53 15 -163316.290872 +54 15 215854.122614 +55 15 2.2595414902e-7 +56 15 16914062.8611 +57 15 12881677.8419 +16 16 1135943.76945 +17 16 -7.28401282128e-9 +18 16 -278794.120422 +19 16 -5.96046447754e-8 +20 16 -2.04596680926e-9 +21 16 -1.0470631902e-7 +22 16 -87659.3847246 +24 16 139397.060211 +25 16 4635264.30435 +27 16 2914870.77518 +58 16 -960625 +59 16 7.28401282128e-9 +60 16 7.26364484235e-9 +61 16 -1.85245689307e-23 +62 16 -2.04596680923e-9 +63 16 -3.98190663434e-10 +17 17 8984214.83699 +18 17 -1798.78435511 +19 17 2.87377901778e-9 +20 17 215854.122613 +21 17 163316.290872 +23 17 -4491426.93395 +58 17 7.28401282128e-9 +59 17 -1360.96909059 +60 17 1798.78435511 +61 17 2.87377901778e-9 +62 17 215854.122614 +63 17 163316.290872 +18 18 595884.872943 +19 18 3.52309932162e-7 +20 18 -485801.838229 +21 18 -215854.122613 +22 18 139397.060211 +24 18 -295918.262146 +25 18 -9839944.65487 +27 18 -4635264.30435 +58 18 7.26364484235e-9 +59 18 1798.78435511 +60 18 -4048.34865191 +61 18 -5.31793649045e-9 +62 18 -485801.838229 +63 18 -215854.122613 +19 19 1014705603.94 +20 19 8.58829443215e-7 +21 19 482068107.452 +22 19 -4635264.30435 +24 19 9839944.65487 +25 19 147056055.205 +27 19 67231775.3905 +58 19 1.85245689307e-23 +59 19 -2.87377901778e-9 +60 19 5.31793649045e-9 +61 19 -16965 +62 19 4.17475314493e-7 +63 19 2.2595414902e-7 +20 20 78619455.2073 +21 20 34890926.5662 +26 20 -89130.8412912 +58 20 2.04596680926e-9 +59 20 -215854.122613 +60 20 485801.838229 +61 20 4.17475314492e-7 +62 20 38151247.6502 +63 20 16914062.8611 +21 21 320794261.443 +22 21 -2914870.77518 +24 21 4635264.30435 +25 21 67231775.3905 +27 21 46611942.19 +58 21 3.9819066341e-10 +59 21 -163316.290872 +60 21 215854.122613 +61 21 2.2595414902e-7 +62 21 16914062.8611 +63 21 12881677.8419 +22 22 1135943.76945 +23 22 -5.47323452122e-9 +24 22 -278794.120422 +25 22 5.96046447754e-8 +26 22 -4.29593884434e-9 +27 22 -1.64267928219e-8 +28 22 -87659.3847246 +30 22 139397.060211 +31 22 4635264.30435 +33 22 2914870.77518 +64 22 -960625 +65 22 5.47323452122e-9 +66 22 1.08852423583e-8 +67 22 -4.17640086446e-23 +68 22 -4.2959388443e-9 +69 22 -1.52563162803e-9 +23 23 8984214.83699 +24 23 -1798.78435511 +25 23 3.08367989622e-9 +26 23 215854.122614 +27 23 163316.290872 +29 23 -4491426.93395 +64 23 5.47323452122e-9 +65 23 -1360.9690906 +66 23 1798.78435511 +67 23 3.08367989622e-9 +68 23 215854.122614 +69 23 163316.290872 +24 24 595884.872943 +25 24 5.43892037771e-8 +26 24 -485801.838229 +27 24 -215854.122614 +28 24 139397.060211 +30 24 -295918.262146 +31 24 -9839944.65487 +33 24 -4635264.30435 +64 24 1.08852423583e-8 +65 24 1798.78435511 +66 24 -4048.34865191 +67 24 -5.21544099829e-9 +68 24 -485801.838229 +69 24 -215854.122614 +25 25 1014705603.94 +26 25 8.42454037735e-7 +27 25 482068107.452 +28 25 -4635264.30435 +30 25 9839944.65487 +31 25 147056055.205 +33 25 67231775.3905 +64 25 4.17640086447e-23 +65 25 -3.08367989622e-9 +66 25 5.21544099829e-9 +67 25 -16965 +68 25 4.09251801855e-7 +69 25 2.42785907544e-7 +26 26 78619455.2073 +27 26 34890926.5662 +32 26 -89130.8412912 +64 26 4.29593884434e-9 +65 26 -215854.122614 +66 26 485801.838229 +67 26 4.09251801854e-7 +68 26 38151247.6502 +69 26 16914062.8611 +27 27 320794261.443 +28 27 -2914870.77518 +30 27 4635264.30435 +31 27 67231775.3905 +33 27 46611942.19 +64 27 1.52563162802e-9 +65 27 -163316.290872 +66 27 215854.122614 +67 27 2.42785907544e-7 +68 27 16914062.8611 +69 27 12881677.8419 +28 28 1159106.56644 +29 28 -2.74002556236e-9 +30 28 -312543.835606 +31 28 561051.477096 +32 28 9.77237297708e-8 +33 28 411018.779791 +34 28 -110822.181711 +36 28 173146.775395 +37 28 5196315.78145 +39 28 3325889.55497 +70 28 -960625 +71 28 2.74002556236e-9 +72 28 7.25512408797e-9 +73 28 -1.76023871718e-23 +74 28 -3.06845733461e-9 +75 28 -1.17181202417e-9 +29 29 9469288.94586 +30 29 -1798.78435511 +31 29 -4.52425004332e-6 +32 29 215854.122613 +33 29 163316.290872 +35 29 -4976501.04282 +70 29 2.74002556236e-9 +71 29 -1360.96909059 +72 29 1798.78435511 +73 29 1.8512884924e-9 +74 29 215854.122614 +75 29 163316.290872 +30 30 669469.664388 +31 30 -1249228.35622 +32 30 -485801.838229 +33 30 -776905.59971 +34 30 173146.775395 +36 30 -369503.05359 +37 30 -11089173.0111 +39 30 -5196315.78145 +70 30 7.25512408797e-9 +71 30 1798.78435511 +72 30 -4048.34865191 +73 30 -3.0167167093e-9 +74 30 -485801.838229 +75 30 -215854.122613 +31 31 1039758660.39 +32 31 5.21335934261e-6 +33 31 493267386.08 +34 31 -5196315.78145 +35 31 4.52610133181e-6 +36 31 11089173.0111 +37 31 133197979.113 +39 31 59660407.959 +70 31 1.76023871719e-23 +71 31 -1.8512884924e-9 +72 31 3.0167167093e-9 +73 31 -16965 +74 31 2.36674339589e-7 +75 31 1.45833052365e-7 +32 32 78629081.3382 +33 32 34890926.5662 +34 32 -1.00792187105e-7 +36 32 1.5747607485e-7 +37 32 4.72602167197e-6 +38 32 -98756.9721507 +39 32 3.02487892893e-6 +70 32 3.06845733463e-9 +71 32 -215854.122613 +72 32 485801.838229 +73 32 2.36674339589e-7 +74 32 38151247.6502 +75 32 16914062.8611 +33 33 329115622.176 +34 33 -3325889.55497 +36 33 5196315.78145 +37 33 59660407.959 +39 33 44065471.0872 +70 33 1.17181202416e-9 +71 33 -163316.290872 +72 33 215854.122613 +73 33 1.45833052365e-7 +74 33 16914062.8611 +75 33 12881677.8419 +34 34 665173.746697 +35 34 -6.41872848215e-12 +36 34 -173146.775395 +37 34 -5196315.78145 +38 34 -4238867.37393 +39 34 -3325889.55497 +41 34 -4238867.37393 +76 34 -497833.333333 +77 34 6.41872848215e-12 +78 34 3.75988717657e-9 +79 34 -5.83779954664e-24 +80 34 -1.59031784371e-9 +81 34 -7.70247417859e-10 +35 35 5111612.84948 +36 35 -846.8959922 +37 35 10097137.073 +38 35 101627.519064 +39 35 57997.4828172 +40 35 10097137.073 +76 35 6.41872848215e-12 +77 35 -483.31235681 +78 35 846.8959922 +79 35 4.3957002782e-10 +80 35 101627.519064 +81 35 57997.4828172 +36 36 8104584.95988 +37 36 11089173.0111 +38 36 -209828.755322 +39 36 5094688.26238 +76 36 3.75988717657e-9 +77 36 846.8959922 +78 36 -1748.57296102 +79 36 -7.70247417859e-10 +80 36 -209828.755323 +81 36 -101627.519064 +37 37 1597085991.39 +38 37 1.23854236996e-7 +39 37 252233332.355 +40 37 449885280.472 +76 37 5.83779954664e-24 +77 37 -4.3957002782e-10 +78 37 7.70247417859e-10 +79 37 -3335 +80 37 6.10051432904e-8 +81 37 3.4880612472e-8 +38 38 461561841.064 +39 38 16341500.8471 +41 38 208101719.712 +76 38 1.59031784373e-9 +77 38 -101627.519064 +78 38 209828.755322 +79 38 6.10051432904e-8 +80 38 16624203.5638 +81 38 8049103.72824 +39 39 165925909.667 +42 39 -1.044e6 +76 39 7.70247417859e-10 +77 39 -57997.4828172 +78 39 101627.519064 +79 39 3.4880612472e-8 +80 39 8049103.72824 +81 39 4598861.67962 +40 40 1064685280.47 +41 41 427728386.378 +42 42 1.044e6 +43 43 1064685280.47 +44 43 -6.27457870385e-20 +45 43 6.44917406118e-6 +46 43 -8.61483932116e-22 +47 43 10097137.073 +48 43 2.44887804559e-7 +49 43 449885280.472 +50 43 -3.07557839189e-20 +51 43 2.73411860976e-6 +44 44 427728386.378 +45 44 1.03484583664e-5 +46 44 -4238867.37393 +47 44 7.70371977755e-34 +48 44 -2.57015161208e-8 +49 44 -3.07557839189e-20 +50 44 208101719.712 +51 44 5.07245130603e-6 +45 45 1.044e6 +46 45 -1.02806064483e-7 +47 45 6.12219511396e-8 +48 45 8.61483932115e-22 +49 45 2.73411860976e-6 +50 45 5.07245130603e-6 +51 45 -1.044e6 +46 46 1239611.25567 +47 46 -3.77589547019e-9 +48 46 -308467.450515 +49 46 10257233.2606 +50 46 -4238867.37393 +51 46 6232345.95189 +52 46 -187426.357349 +54 46 308467.450514 +55 46 10257233.2606 +57 46 6232345.95189 +88 46 -497833.333333 +89 46 -3.76947674171e-9 +90 46 -6.41872848215e-12 +91 46 5.83779954664e-24 +92 46 -7.70247417859e-10 +93 46 -4.3957002782e-10 +47 47 7199781.17048 +48 47 -1693.79198458 +49 47 10097137.073 +50 47 -9.31322574615e-10 +51 47 -1.27882231027e-5 +53 47 -7064186.05146 +88 47 -3.76947674171e-9 +89 47 -483.31235681 +90 47 846.8959922 +91 47 -7.70247417859e-10 +92 47 101627.519064 +93 47 57997.4828172 +48 48 8385106.47624 +49 48 -21556628.1215 +50 48 1.43516808748e-6 +51 48 -10257233.2606 +52 48 308467.450514 +54 48 -648275.996988 +55 48 -21556628.1215 +57 48 -10257233.2606 +88 48 -6.41872848215e-12 +89 48 846.8959922 +90 48 -1748.57296102 +91 48 1.59031784371e-9 +92 48 -209828.755323 +93 48 -101627.519064 +49 49 2251035569.32 +50 49 -4.8160669067e-5 +51 49 570093221.007 +52 49 -10257233.2606 +53 49 -6.42483978614e-6 +54 49 21556628.1215 +55 49 247268663.971 +57 49 112058714.222 +88 49 -5.83779954664e-24 +89 49 7.70247417859e-10 +90 49 -1.59031784371e-9 +91 49 -3335 +92 49 -1.26022151894e-7 +93 49 -6.10051432904e-8 +50 50 495403602.535 +51 50 32683001.6942 +52 50 7.31562302806e-7 +54 50 -1.45975668101e-6 +55 50 -4.85401774346e-5 +56 50 -205820.7294 +57 50 -2.43260842335e-5 +88 50 7.70247417859e-10 +89 50 -101627.519064 +90 50 209828.755322 +91 50 -1.26022151894e-7 +92 50 16624203.5638 +93 50 8049103.72824 +51 51 354310766.846 +52 51 -6232345.95189 +53 51 1.28496795723e-5 +54 51 10257233.2606 +55 51 112058714.222 +57 51 79853210.6342 +88 51 4.39570027836e-10 +89 51 -57997.4828172 +90 51 101627.519064 +91 51 -6.10051432904e-8 +92 51 8049103.72824 +93 51 4598861.67962 +52 52 2296102.7147 +53 52 -1.3633207005e-11 +54 52 -616934.901029 +55 52 5.96046447754e-7 +56 52 2.80959493881e-7 +57 52 4.75599364026e-7 +58 52 -187426.357349 +60 52 308467.450515 +61 52 10257233.2606 +63 52 6232345.95189 +94 52 -960625 +95 52 -7.27037961428e-9 +96 52 -1.36332070051e-11 +97 52 1.23993295393e-23 +98 52 -1.63598484061e-9 +99 52 -1.23779417717e-9 +53 53 14131094.0411 +54 53 -3597.56871023 +55 53 -4.50976385839e-9 +56 53 -9.31322574615e-10 +57 53 6.4242631197e-6 +59 53 -7064186.05146 +94 53 -7.27037961428e-9 +95 53 -1360.9690906 +96 53 1798.78435511 +97 53 -1.63598484061e-9 +98 53 215854.122614 +99 53 163316.290872 +54 54 1304648.69128 +55 54 -1.66524810206e-6 +56 54 -5.84870576859e-7 +57 54 -6.03497028351e-7 +58 54 308467.450515 +60 54 -648275.996988 +61 54 -21556628.1215 +63 54 -10257233.2606 +94 54 -1.36332070051e-11 +95 54 1798.78435511 +96 54 -4048.34865191 +97 54 3.68195164984e-9 +98 54 -485801.838229 +99 54 -215854.122614 +55 55 2372721167.7 +56 55 1.98700816717e-5 +57 55 1140186442.01 +58 55 -10257233.2606 +60 55 21556628.1215 +61 55 247268663.971 +63 55 112058714.222 +94 55 -1.23993295393e-23 +95 55 1.63598484061e-9 +96 55 -3.68195164984e-9 +97 55 -16965 +98 55 -2.89281559846e-7 +99 55 -1.28193754647e-7 +56 56 157294028.508 +57 56 69781853.1323 +58 56 -2.80549511912e-7 +60 56 5.89603584547e-7 +61 56 1.96056390646e-5 +62 56 -205820.7294 +63 56 9.32889930534e-6 +94 56 1.63598484061e-9 +95 56 -215854.122614 +96 56 485801.838229 +97 56 -2.89281559846e-7 +98 56 38151247.6502 +99 56 16914062.8611 +57 57 721879860.841 +58 57 -6232345.95189 +59 57 -6.42483978614e-6 +60 57 10257233.2606 +61 57 112058714.222 +63 57 79853210.6342 +94 57 1.23779417719e-9 +95 57 -163316.290872 +96 57 215854.122614 +97 57 -1.28193754647e-7 +98 57 16914062.8611 +99 57 12881677.8419 +58 58 2296102.7147 +59 58 -3.64882301414e-9 +60 58 -616934.901029 +61 58 -1.78813934326e-7 +62 58 -4.49784816505e-7 +63 58 -6.18897088597e-10 +64 58 -187426.357349 +66 58 308467.450515 +67 58 10257233.2606 +69 58 6232345.95189 +100 58 -960625 +101 58 -3.63518980714e-9 +102 58 -6.81660350253e-12 +103 58 3.09983238482e-24 +104 58 -8.17992420304e-10 +105 58 -6.18897088587e-10 +59 59 14131094.0411 +60 59 -3597.56871023 +61 59 6.4211480147e-6 +62 59 9.31322574615e-10 +63 59 -6.42519444227e-6 +65 59 -7064186.05146 +100 59 -3.63518980714e-9 +101 59 -1360.96909059 +102 59 1798.78435511 +103 59 -8.17992420304e-10 +104 59 215854.122614 +105 59 163316.290872 +60 60 1304648.69128 +61 60 -1.17368313726e-7 +62 60 8.69855284691e-7 +63 60 1.19209289551e-7 +64 60 308467.450515 +66 60 -648275.996988 +67 60 -21556628.1215 +69 60 -10257233.2606 +100 60 -6.81660350253e-12 +101 60 1798.78435511 +102 60 -4048.34865191 +103 60 1.84097582492e-9 +104 60 -485801.838229 +105 60 -215854.122614 +61 61 2372721167.7 +62 61 -2.83729023448e-5 +63 61 1140186442.01 +64 61 -10257233.2606 +65 61 -6.42483978614e-6 +66 61 21556628.1215 +67 61 247268663.971 +69 61 112058714.222 +100 61 -3.09983238482e-24 +101 61 8.17992420304e-10 +102 61 -1.84097582492e-9 +103 61 -16965 +104 61 -1.44640779923e-7 +105 61 -6.40968773234e-8 +62 62 157294028.508 +63 62 69781853.1323 +64 62 4.51012790894e-7 +66 62 -8.7015309646e-7 +67 62 -2.89345383699e-5 +68 62 -205820.7294 +69 62 -1.49971849282e-5 +100 62 8.17992420304e-10 +101 62 -215854.122614 +102 62 485801.838229 +103 62 -1.44640779923e-7 +104 62 38151247.6502 +105 62 16914062.8611 +63 63 721879860.841 +64 63 -6232345.95189 +65 63 6.42483978614e-6 +66 63 10257233.2606 +67 63 112058714.222 +69 63 79853210.6342 +100 63 6.18897088597e-10 +101 63 -163316.290872 +102 63 215854.122613 +103 63 -6.40968773234e-8 +104 63 16914062.8611 +105 63 12881677.8419 +64 64 2296102.7147 +65 64 -3.65563961765e-9 +66 64 -616934.901029 +67 64 -5.96046447754e-8 +68 64 4.54899733528e-7 +69 64 5.92951962311e-8 +70 64 -187426.357349 +72 64 308467.450514 +73 64 10257233.2606 +75 64 6232345.95189 +106 64 -960625 +107 64 -1.81759490357e-9 +108 64 -3.40830175126e-12 +109 64 7.74958096205e-25 +110 64 -4.08996210152e-10 +111 64 -3.09448544293e-10 +65 65 14131094.0411 +66 65 -3597.56871023 +67 65 -6.42833246224e-6 +68 65 -9.31322574615e-10 +69 65 6.4242631197e-6 +71 65 -7064186.05146 +106 65 -1.81759490357e-9 +107 65 -1360.9690906 +108 65 1798.78435511 +109 65 -4.08996210152e-10 +110 65 215854.122614 +111 65 163316.290872 +66 66 1304648.69128 +67 66 -1.18288801638e-7 +68 66 -8.66129994392e-7 +69 66 5.21540641785e-8 +70 66 308467.450514 +72 66 -648275.996988 +73 66 -21556628.1215 +75 66 -10257233.2606 +106 66 -3.40830175126e-12 +107 66 1798.78435511 +108 66 -4048.34865191 +109 66 9.2048791246e-10 +110 66 -485801.838229 +111 66 -215854.122614 +67 67 2372721167.7 +68 67 2.96283956987e-5 +69 67 1140186442.01 +70 67 -10257233.2606 +71 67 6.42483978614e-6 +72 67 21556628.1215 +73 67 247268663.971 +75 67 112058714.222 +106 67 -7.74958096205e-25 +107 67 4.08996210152e-10 +108 67 -9.2048791246e-10 +109 67 -16965 +110 67 -7.23203899614e-8 +111 67 -3.20484386617e-8 +68 68 157294028.508 +69 68 69781853.1323 +70 68 -4.51012790894e-7 +72 68 8.7015309646e-7 +73 68 2.89345383699e-5 +74 68 -205820.7294 +75 68 1.49971849282e-5 +106 68 4.08996210152e-10 +107 68 -215854.122614 +108 68 485801.838229 +109 68 -7.23203899614e-8 +110 68 38151247.6502 +111 68 16914062.8611 +69 69 721879860.841 +70 69 -6232345.95189 +71 69 -6.42483978614e-6 +72 69 10257233.2606 +73 69 112058714.222 +75 69 79853210.6342 +106 69 3.09448544299e-10 +107 69 -163316.290872 +108 69 215854.122614 +109 69 -3.20484386617e-8 +110 69 16914062.8611 +111 69 12881677.8419 +70 70 2341151.26585 +71 70 -1.83122811058e-9 +72 70 -684029.893911 +73 70 1013788.37464 +74 70 5.55870709263e-7 +75 70 744469.140943 +76 70 -232474.908504 +78 70 375562.443396 +79 70 11271021.6352 +81 70 6976815.09283 +112 70 -960625 +113 70 -9.08797451785e-10 +114 70 -1.70415087563e-12 +115 70 1.93739524051e-25 +116 70 -2.04498105076e-10 +117 70 -1.54724272147e-10 +71 71 14894026.1346 +72 71 -3597.56871023 +73 71 -7.12077826964e-6 +74 71 9.31322574615e-10 +75 71 7.11809843779e-6 +77 71 -7827118.14501 +112 71 -9.08797451785e-10 +113 71 -1360.96909059 +114 71 1798.78435511 +115 71 -2.04498105076e-10 +116 71 215854.122614 +117 71 163316.290872 +72 72 1449937.00271 +73 72 -2259066.56878 +74 72 -1.06357038021e-6 +75 72 -1013788.37464 +76 72 375562.443396 +78 72 -793564.308415 +79 72 -23815694.6903 +81 72 -11271021.6352 +112 72 -1.70415087563e-12 +113 72 1798.78435511 +114 72 -4048.34865191 +115 72 4.6024395623e-10 +116 72 -485801.838229 +117 72 -215854.122614 +73 73 2421358974.52 +74 73 3.23242219168e-5 +75 73 1162099482.64 +76 73 -11271021.6352 +77 73 7.11872248304e-6 +78 73 23815694.6903 +79 73 194486375.663 +81 73 84504028.1182 +112 73 -1.93739524051e-25 +113 73 2.04498105076e-10 +114 73 -4.6024395623e-10 +115 73 -16965 +116 73 -3.61601949807e-8 +117 73 -1.60242193309e-8 +74 74 157316257.147 +75 74 69781853.1323 +76 74 -5.53006750033e-7 +78 74 1.06331458647e-6 +79 74 3.19111826007e-5 +80 74 -228049.368176 +81 74 1.6596310823e-5 +112 74 2.04498105076e-10 +113 74 -215854.122614 +114 74 485801.838229 +115 74 -3.61601949807e-8 +116 74 38151247.6502 +117 74 16914062.8611 +75 75 737779636.92 +76 75 -6976815.09283 +77 75 -7.11872248304e-6 +78 75 11271021.6352 +79 75 84504028.1182 +81 75 68237558.0284 +112 75 1.54724272149e-10 +113 75 -163316.290872 +114 75 215854.122613 +115 75 -1.60242193309e-8 +116 75 16914062.8611 +117 75 12881677.8419 +76 76 1284659.80682 +77 76 -6.41872848215e-12 +78 76 -375562.443396 +79 76 -11271021.6352 +80 76 -4238867.37393 +81 76 -6976815.09283 +83 76 -4238867.37393 +118 76 -497833.333333 +77 77 7962713.26403 +78 77 -1693.7919844 +79 77 10097137.073 +80 77 -9.31322574615e-10 +81 77 6.14672899246e-8 +82 77 10097137.073 +84 77 6.12219511396e-8 +119 77 -483.31235681 +120 77 846.8959922 +122 77 101627.519064 +123 77 57997.4828172 +78 78 8530394.78767 +79 78 23815694.6903 +80 78 -2.421438694e-8 +81 78 11271021.6352 +83 78 -2.57015161207e-8 +119 78 846.8959922 +120 78 -1748.57296102 +122 78 -209828.755323 +123 78 -101627.519064 +79 79 2299673376.14 +80 79 1.23854236996e-7 +81 79 592006261.636 +82 79 449885280.472 +84 79 2.73411860976e-6 +121 79 -3335 +80 80 495425831.174 +81 80 32683001.6942 +83 80 208101719.712 +119 80 -101627.519064 +120 80 209828.755322 +122 80 16624203.5638 +123 80 8049103.72824 +81 81 370210542.925 +82 81 2.73411860976e-6 +84 81 -1.044e6 +119 81 -57997.4828172 +120 81 101627.519064 +122 81 8049103.72824 +123 81 4598861.67962 +82 82 1064685280.47 +84 82 6.44917406118e-6 +83 83 427728386.378 +84 84 1.044e6 +85 85 1064685280.47 +86 85 -6.27457870385e-20 +87 85 1.28983481224e-5 +88 85 -8.61483932116e-22 +89 85 10097137.073 +90 85 1.22443902279e-7 +91 85 449885280.472 +92 85 -3.07557839189e-20 +93 85 5.46823721952e-6 +86 86 427728386.378 +87 86 5.1742291832e-6 +88 86 -4238867.37393 +89 86 7.70371977755e-34 +90 86 -5.14030322415e-8 +91 86 -3.07557839189e-20 +92 86 208101719.712 +93 86 2.53622565301e-6 +87 87 1.044e6 +88 87 -5.14030322414e-8 +89 87 1.22443902279e-7 +90 87 8.61483932115e-22 +91 87 5.46823721952e-6 +92 87 2.53622565301e-6 +93 87 -1.044e6 +88 88 1239611.25567 +89 88 3.77268610595e-9 +90 88 -308467.450515 +91 88 10257233.2606 +92 88 -4238867.37393 +93 88 6232345.95189 +94 88 -187426.357349 +96 88 308467.450514 +97 88 10257233.2606 +99 88 6232345.95189 +130 88 -497833.333333 +131 88 -3.20936424108e-12 +132 88 -1.87994358829e-9 +133 88 -1.45944988666e-24 +134 88 7.95158921856e-10 +135 88 3.85123708929e-10 +89 89 7199781.17048 +90 89 -1693.79198449 +91 89 10097137.073 +92 89 4.65661287308e-10 +93 89 -2.55759805441e-5 +95 89 -7064186.05146 +130 89 -3.20936424108e-12 +131 89 -483.31235681 +132 89 846.8959922 +133 89 -2.1978501391e-10 +134 89 101627.519064 +135 89 57997.4828172 +90 90 8385106.47624 +91 90 -21556628.1215 +92 90 2.58628278971e-6 +93 90 -10257233.2606 +94 90 308467.450514 +96 90 -648275.996988 +97 90 -21556628.1215 +99 90 -10257233.2606 +130 90 -1.87994358829e-9 +131 90 846.8959922 +132 90 -1748.57296102 +133 90 3.85123708929e-10 +134 90 -209828.755323 +135 90 -101627.519064 +91 91 2251035569.32 +92 91 -8.80690368129e-5 +93 91 570093221.007 +94 91 -10257233.2606 +95 91 -6.42483978614e-6 +96 91 21556628.1215 +97 91 247268663.971 +99 91 112058714.222 +130 91 1.45944988666e-24 +131 91 2.1978501391e-10 +132 91 -3.85123708929e-10 +133 91 -3335 +134 91 -3.05025716452e-8 +135 91 -1.7440306236e-8 +92 92 495403602.535 +93 92 32683001.6942 +94 92 1.29266132663e-6 +96 92 -2.6389638501e-6 +97 92 -8.77514555638e-5 +98 92 -205820.7294 +99 92 -4.29838828442e-5 +130 92 -7.95158921867e-10 +131 92 -101627.519064 +132 92 209828.755323 +133 92 -3.05025716452e-8 +134 92 16624203.5638 +135 92 8049103.72824 +93 93 354310766.846 +94 93 -6232345.95189 +95 93 2.56993591446e-5 +96 93 10257233.2606 +97 93 112058714.222 +99 93 79853210.6342 +130 93 -3.85123708929e-10 +131 93 -57997.4828172 +132 93 101627.519064 +133 93 -1.7440306236e-8 +134 93 8049103.72824 +135 93 4598861.67962 +94 94 2296102.7147 +95 94 7.27037961428e-9 +96 94 -616934.901029 +97 94 4.76837158203e-7 +98 94 5.62735008665e-7 +99 94 4.76837158203e-7 +100 94 -187426.357349 +102 94 308467.450514 +103 94 10257233.2606 +105 94 6232345.95189 +136 94 -960625 +95 95 14131094.0411 +96 95 -3597.56871023 +97 95 1.63598484061e-9 +98 95 3.72529029846e-9 +99 95 1.28541141748e-5 +101 95 -7064186.05146 +137 95 -1360.9690906 +138 95 1798.78435511 +140 95 215854.122614 +141 95 163316.290872 +96 96 1304648.69128 +97 96 -1.31130218506e-6 +98 96 -1.19395554066e-6 +99 96 -4.88013029099e-7 +100 96 308467.450514 +102 96 -648275.996988 +103 96 -21556628.1215 +105 96 -10257233.2606 +137 96 1798.78435511 +138 96 -4048.34865191 +140 96 -485801.838229 +141 96 -215854.122614 +97 97 2372721167.7 +98 97 3.86168912931e-5 +99 97 1140186442.01 +100 97 -10257233.2606 +102 97 21556628.1215 +103 97 247268663.971 +105 97 112058714.222 +139 97 -16965 +98 98 157294028.508 +99 98 69781853.1323 +100 98 -5.61099023825e-7 +102 98 1.17920716909e-6 +103 98 3.92112781292e-5 +104 98 -205820.7294 +105 98 1.86577986107e-5 +137 98 -215854.122614 +138 98 485801.838229 +140 98 38151247.6502 +141 98 16914062.8611 +99 99 721879860.841 +100 99 -6232345.95189 +101 99 -1.28496795723e-5 +102 99 10257233.2606 +103 99 112058714.222 +105 99 79853210.6342 +137 99 -163316.290872 +138 99 215854.122614 +140 99 16914062.8611 +141 99 12881677.8419 +100 100 2296102.7147 +101 100 3.64200641064e-9 +102 100 -616934.901029 +103 100 -1.19209289551e-7 +104 100 -7.28903334561e-7 +105 100 9.02249595834e-8 +106 100 -187426.357349 +108 100 308467.450514 +109 100 10257233.2606 +111 100 6232345.95189 +142 100 -960625 +143 100 -6.81660350253e-12 +144 100 -3.62500581767e-9 +145 100 -3.09983238482e-24 +146 100 1.84097582492e-9 +147 100 8.17992420304e-10 +101 101 14131094.0411 +102 101 -3597.56871023 +103 101 6.42503888147e-6 +104 101 4.65661287308e-9 +105 101 -1.28438696265e-5 +107 101 -7064186.05146 +142 101 -6.81660350253e-12 +143 101 -1360.9690906 +144 101 1798.78435511 +145 101 -6.18897088587e-10 +146 101 215854.122614 +147 101 163316.290872 +102 102 1304648.69128 +103 102 -5.95228455334e-7 +104 102 1.45472586155e-6 +105 102 -6.70552253723e-8 +106 102 308467.450514 +108 102 -648275.996988 +109 102 -21556628.1215 +111 102 -10257233.2606 +142 102 -3.62500581767e-9 +143 102 1798.78435511 +144 102 -4048.34865191 +145 102 8.17992420304e-10 +146 102 -485801.838229 +147 102 -215854.122614 +103 103 2372721167.7 +104 103 -4.89695921562e-5 +105 103 1140186442.01 +106 103 -10257233.2606 +107 103 -6.42483978614e-6 +108 103 21556628.1215 +109 103 247268663.971 +111 103 112058714.222 +142 103 3.09983238482e-24 +143 103 6.18897088587e-10 +144 103 -8.17992420304e-10 +145 103 -16965 +146 103 -6.40968773234e-8 +147 103 -4.88801971864e-8 +104 104 157294028.508 +105 104 69781853.1323 +106 104 7.31562302806e-7 +108 104 -1.45975668101e-6 +109 104 -4.85401774346e-5 +110 104 -205820.7294 +111 104 -2.43260842335e-5 +142 104 -1.84097582493e-9 +143 104 -215854.122614 +144 104 485801.838229 +145 104 -6.40968773234e-8 +146 104 38151247.6502 +147 104 16914062.8611 +105 105 721879860.841 +106 105 -6232345.95189 +107 105 1.28496795723e-5 +108 105 10257233.2606 +109 105 112058714.222 +111 105 79853210.6342 +142 105 -8.17992420304e-10 +143 105 -163316.290872 +144 105 215854.122614 +145 105 -4.88801971864e-8 +146 105 16914062.8611 +147 105 12881677.8419 +106 106 2296102.7147 +107 106 1.83122811058e-9 +108 106 -616934.901029 +109 106 -1.78813934326e-7 +110 106 7.35653250666e-7 +111 106 3.14383072283e-8 +112 106 -187426.357349 +114 106 308467.450514 +115 106 10257233.2606 +117 106 6232345.95189 +148 106 -960625 +149 106 -1.36332070051e-11 +150 106 -7.25001163534e-9 +151 106 -1.23993295393e-23 +152 106 3.68195164984e-9 +153 106 1.63598484061e-9 +107 107 14131094.0411 +108 107 -3597.56871023 +109 107 -6.4256685841e-6 +110 107 -1.86264514923e-9 +111 107 1.28522515297e-5 +113 107 -7064186.05146 +148 107 -1.36332070051e-11 +149 107 -1360.9690906 +150 107 1798.78435511 +151 107 -1.23779417717e-9 +152 107 215854.122614 +153 107 163316.290872 +108 108 1304648.69128 +109 108 -3.55991883812e-7 +110 108 -1.45100057125e-6 +111 108 5.21540641785e-8 +112 108 308467.450514 +114 108 -648275.996988 +115 108 -21556628.1215 +117 108 -10257233.2606 +148 108 -7.25001163534e-9 +149 108 1798.78435511 +150 108 -4048.34865191 +151 108 1.63598484061e-9 +152 108 -485801.838229 +153 108 -215854.122614 +109 109 2372721167.7 +110 109 4.81271381184e-5 +111 109 1140186442.01 +112 109 -10257233.2606 +113 109 6.42483978614e-6 +114 109 21556628.1215 +115 109 247268663.971 +117 109 112058714.222 +148 109 1.23993295393e-23 +149 109 1.23779417717e-9 +150 109 -1.63598484061e-9 +151 109 -16965 +152 109 -1.28193754647e-7 +153 109 -9.77603943728e-8 +110 110 157294028.508 +111 110 69781853.1323 +112 110 -7.31562302806e-7 +114 110 1.45975668101e-6 +115 110 4.85401774346e-5 +116 110 -205820.7294 +117 110 2.43260842335e-5 +148 110 -3.68195164987e-9 +149 110 -215854.122614 +150 110 485801.838229 +151 110 -1.28193754647e-7 +152 110 38151247.6502 +153 110 16914062.8611 +111 111 721879860.841 +112 111 -6232345.95189 +113 111 -1.28496795723e-5 +114 111 10257233.2606 +115 111 112058714.222 +117 111 79853210.6342 +148 111 -1.63598484061e-9 +149 111 -163316.290872 +150 111 215854.122614 +151 111 -9.77603943728e-8 +152 111 16914062.8611 +153 111 12881677.8419 +112 112 2341151.26585 +113 112 9.15614055287e-10 +114 112 -684029.893911 +115 112 1013788.37464 +116 112 8.96624276417e-7 +117 112 744469.140943 +118 112 -232474.908504 +120 112 375562.443396 +121 112 11271021.6352 +123 112 6976815.09283 +154 112 -960625 +155 112 -6.81660350253e-12 +156 112 -3.62500581767e-9 +157 112 -3.09983238482e-24 +158 112 1.84097582492e-9 +159 112 8.17992420304e-10 +113 113 14894026.1346 +114 113 -3597.56871023 +115 113 -7.11913688202e-6 +116 113 4.65661287308e-9 +117 113 1.42436474562e-5 +119 113 -7827118.14501 +154 113 -6.81660350253e-12 +155 113 -1360.9690906 +156 113 1798.78435511 +157 113 -6.18897088587e-10 +158 113 215854.122614 +159 113 163316.290872 +114 114 1449937.00271 +115 114 -2259066.56878 +116 114 -1.79000198841e-6 +117 114 -1013788.37464 +118 114 375562.443396 +120 114 -793564.308415 +121 114 -23815694.6903 +123 114 -11271021.6352 +154 114 -3.62500581767e-9 +155 114 1798.78435511 +156 114 -4048.34865191 +157 114 8.17992420304e-10 +158 114 -485801.838229 +159 114 -215854.122614 +115 115 2421358974.52 +116 115 5.33649110825e-5 +117 115 1162099482.64 +118 115 -11271021.6352 +119 115 7.11872248304e-6 +120 115 23815694.6903 +121 115 194486375.663 +123 115 84504028.1182 +154 115 3.09983238482e-24 +155 115 6.18897088587e-10 +156 115 -8.17992420304e-10 +157 115 -16965 +158 115 -6.40968773234e-8 +159 115 -4.88801971864e-8 +116 116 157316257.147 +117 116 69781853.1323 +118 116 -8.94578802487e-7 +120 116 1.78505712049e-6 +121 116 5.35714307405e-5 +122 116 -228049.368176 +123 116 2.68472452838e-5 +154 116 -1.84097582493e-9 +155 116 -215854.122614 +156 116 485801.838229 +157 116 -6.40968773234e-8 +158 116 38151247.6502 +159 116 16914062.8611 +117 117 737779636.92 +118 117 -6976815.09283 +119 117 -1.42374449661e-5 +120 117 11271021.6352 +121 117 84504028.1182 +123 117 68237558.0284 +154 117 -8.17992420304e-10 +155 117 -163316.290872 +156 117 215854.122614 +157 117 -4.88801971864e-8 +158 117 16914062.8611 +159 117 12881677.8419 +118 118 1284659.80682 +119 118 3.20936424108e-12 +120 118 -375562.443396 +121 118 -11271021.6352 +122 118 -4238867.37393 +123 118 -6976815.09283 +125 118 -4238867.37393 +160 118 -497833.333333 +161 118 -3.20936424108e-12 +162 118 -1.87994358829e-9 +163 118 -1.45944988666e-24 +164 118 7.95158921856e-10 +165 118 3.85123708929e-10 +119 119 7962713.26403 +120 119 -1693.7919844 +121 119 10097137.073 +122 119 4.65661287308e-10 +123 119 1.23167410493e-7 +124 119 10097137.073 +126 119 1.22443902279e-7 +160 119 -3.20936424108e-12 +161 119 -483.31235681 +162 119 846.8959922 +163 119 -2.1978501391e-10 +164 119 101627.519064 +165 119 57997.4828172 +120 120 8530394.78767 +121 120 23815694.6903 +122 120 -5.21540641785e-8 +123 120 11271021.6352 +125 120 -5.14030322414e-8 +160 120 -1.87994358829e-9 +161 120 846.8959922 +162 120 -1748.57296102 +163 120 3.85123708929e-10 +164 120 -209828.755323 +165 120 -101627.519064 +121 121 2299673376.14 +122 121 -6.19271184978e-8 +123 121 592006261.636 +124 121 449885280.472 +126 121 5.46823721952e-6 +160 121 1.45944988666e-24 +161 121 2.1978501391e-10 +162 121 -3.85123708929e-10 +163 121 -3335 +164 121 -3.05025716452e-8 +165 121 -1.7440306236e-8 +122 122 495425831.174 +123 122 32683001.6942 +125 122 208101719.712 +160 122 -7.95158921867e-10 +161 122 -101627.519064 +162 122 209828.755323 +163 122 -3.05025716452e-8 +164 122 16624203.5638 +165 122 8049103.72824 +123 123 370210542.925 +124 123 5.46823721952e-6 +126 123 -1.044e6 +160 123 -3.85123708929e-10 +161 123 -57997.4828172 +162 123 101627.519064 +163 123 -1.7440306236e-8 +164 123 8049103.72824 +165 123 4598861.67962 +124 124 1064685280.47 +126 124 1.28983481224e-5 +125 125 427728386.378 +126 126 1.044e6 +127 127 1064685280.47 +128 127 -1.25491574077e-19 +129 127 2.57966962447e-5 +130 127 -1.72296786423e-21 +131 127 10097137.073 +132 127 1.22443902279e-7 +133 127 449885280.472 +134 127 -6.15115678378e-20 +135 127 1.0936474439e-5 +128 128 427728386.378 +129 128 5.1742291832e-6 +130 128 -4238867.37393 +131 128 3.08148791102e-33 +132 128 -1.02806064483e-7 +133 128 -6.15115678378e-20 +134 128 208101719.712 +135 128 2.53622565301e-6 +129 129 1.044e6 +130 129 -5.14030322414e-8 +131 129 2.44887804558e-7 +132 129 1.72296786423e-21 +133 129 1.0936474439e-5 +134 129 2.53622565301e-6 +135 129 -1.044e6 +130 130 1310730.30329 +131 130 3.20936424331e-12 +132 130 -308467.450515 +133 130 10257233.2606 +134 130 -4238867.37393 +135 130 6232345.95189 +136 130 -187426.357349 +138 130 308467.450514 +139 130 10257233.2606 +141 130 6232345.95189 +172 130 -568952.380952 +131 131 7200015.50281 +132 131 -2103.53829495 +133 131 10097137.073 +134 131 30319.9227152 +135 131 17355.2094833 +137 131 -7064186.05146 +173 131 -717.644689064 +174 131 1256.64230266 +176 131 131947.441779 +177 131 75352.6923517 +132 132 8385952.96859 +133 132 -21556628.1215 +134 132 -62653.102172 +135 132 -10287553.1833 +136 132 308467.450514 +138 132 -648275.996988 +139 132 -21556628.1215 +141 132 -10257233.2606 +173 132 1256.64230266 +174 132 -2595.06530952 +176 132 -272481.8575 +177 132 -131947.441779 +133 133 2251036045.75 +134 133 -.000175564838246 +135 133 570093221.007 +136 133 -10257233.2606 +137 133 -1.28496795723e-5 +138 133 21556628.1215 +139 133 247268663.971 +141 133 112058714.222 +175 133 -3811.42857143 +134 134 500056925.087 +135 134 34934494.8732 +136 134 2.58532265326e-6 +138 134 -5.2779277002e-6 +139 134 -.000175502911128 +140 134 -205820.7294 +141 134 -8.59677656884e-5 +173 134 -131947.441779 +174 134 272481.8575 +176 134 18833169.809 +177 134 9115968.74745 +135 135 355600363.928 +136 135 -6232345.95189 +137 135 5.13987182891e-5 +138 135 10257233.2606 +139 135 112058714.222 +141 135 79853210.6342 +173 135 -75352.6923517 +174 135 131947.441779 +176 135 9115968.74745 +177 135 5213934.11585 +136 136 2433334.85755 +138 136 -616934.901029 +139 136 4.76837158203e-7 +140 136 1.29266132663e-6 +141 136 4.47034835815e-7 +142 136 -187426.357349 +144 136 308467.450514 +145 136 10257233.2606 +147 136 6232345.95189 +178 136 -1097857.14286 +137 137 14131747.9211 +138 137 -4451.70838056 +139 137 -6.42483978614e-6 +140 137 62702.9000587 +141 137 48242.8612429 +143 137 -7064186.05146 +179 137 -2014.84906751 +180 137 2652.92402545 +182 137 278557.022672 +183 137 211559.152089 +138 138 1306578.6539 +139 138 -1.19209289551e-6 +140 138 -141920.845228 +141 138 -62702.9000592 +142 138 308467.450514 +144 138 -648275.996988 +145 138 -21556628.1215 +147 138 -10257233.2606 +179 138 2652.92402545 +180 138 -5978.311271 +182 138 -627722.683454 +183 138 -278557.022672 +139 139 2372723591.27 +140 139 8.77514555638e-5 +141 139 1140186442.01 +142 139 -10257233.2606 +143 139 6.42483978614e-6 +144 139 21556628.1215 +145 139 247268663.971 +147 139 112058714.222 +181 139 -19388.5714286 +140 140 167786542.96 +141 140 74411907.4925 +142 140 -1.29266132663e-6 +144 140 2.6389638501e-6 +145 140 8.77514555638e-5 +146 140 -205820.7294 +147 140 4.29838828442e-5 +179 140 -278557.022672 +180 140 627722.683454 +182 140 42888055.5487 +183 140 18975993.8348 +141 141 725455085.057 +142 141 -6232345.95189 +143 141 -2.56993591446e-5 +144 141 10257233.2606 +145 141 112058714.222 +147 141 79853210.6342 +179 141 -211559.152089 +180 141 278557.022672 +182 141 18975993.8348 +183 141 14537965.7548 +142 142 2433334.85755 +143 142 6.81660350253e-12 +144 142 -616934.901029 +145 142 -1.19209289551e-7 +146 142 -1.84097582492e-9 +147 142 5.96046447754e-8 +148 142 -187426.357349 +150 142 308467.450514 +151 142 10257233.2606 +153 142 6232345.95189 +184 142 -1097857.14286 +143 143 14131747.9211 +144 143 -4451.70838056 +145 143 6.18897088587e-10 +146 143 62702.9000587 +147 143 48242.8612172 +149 143 -7064186.05146 +185 143 -2014.84906751 +186 143 2652.92402545 +188 143 278557.022672 +189 143 211559.152089 +144 144 1306578.6539 +145 144 -5.96046447754e-7 +146 144 -141920.845225 +147 144 -62702.9000588 +148 144 308467.450514 +150 144 -648275.996988 +151 144 -21556628.1215 +153 144 -10257233.2606 +185 144 2652.92402545 +186 144 -5978.311271 +188 144 -627722.683454 +189 144 -278557.022672 +145 145 2372723591.27 +146 145 -1.32221303549e-7 +147 145 1140186442.01 +148 145 -10257233.2606 +150 145 21556628.1215 +151 145 247268663.971 +153 145 112058714.222 +187 145 -19388.5714286 +146 146 167786542.96 +147 146 74411907.4925 +152 146 -205820.7294 +185 146 -278557.022672 +186 146 627722.683454 +188 146 42888055.5487 +189 146 18975993.8348 +147 147 725455085.057 +148 147 -6232345.95189 +150 147 10257233.2606 +151 147 112058714.222 +153 147 79853210.6342 +185 147 -211559.152089 +186 147 278557.022672 +188 147 18975993.8348 +189 147 14537965.7548 +148 148 2433334.85755 +149 148 1.36332070051e-11 +150 148 -616934.901029 +151 148 -1.78813934326e-7 +152 148 -3.68195164984e-9 +154 148 -187426.357349 +156 148 308467.450514 +157 148 10257233.2606 +159 148 6232345.95189 +190 148 -1097857.14286 +149 149 14131747.9211 +150 149 -4451.70838056 +151 149 1.23779417717e-9 +152 149 62702.9000587 +153 149 48242.8612172 +155 149 -7064186.05146 +191 149 -2014.84906751 +192 149 2652.92402545 +194 149 278557.022672 +195 149 211559.152089 +150 150 1306578.6539 +151 150 -3.57627868652e-7 +152 150 -141920.845226 +153 150 -62702.9000587 +154 150 308467.450514 +156 150 -648275.996988 +157 150 -21556628.1215 +159 150 -10257233.2606 +191 150 2652.92402545 +192 150 -5978.311271 +194 150 -627722.683454 +195 150 -278557.022672 +151 151 2372723591.27 +152 151 -2.64442607099e-7 +153 151 1140186442.01 +154 151 -10257233.2606 +156 151 21556628.1215 +157 151 247268663.971 +159 151 112058714.222 +193 151 -19388.5714286 +152 152 167786542.96 +153 152 74411907.4925 +158 152 -205820.7294 +191 152 -278557.022672 +192 152 627722.683454 +194 152 42888055.5487 +195 152 18975993.8348 +153 153 725455085.057 +154 153 -6232345.95189 +156 153 10257233.2606 +157 153 112058714.222 +159 153 79853210.6342 +191 153 -211559.152089 +192 153 278557.022672 +194 153 18975993.8348 +195 153 14537965.7548 +154 154 2478383.40871 +155 154 6.81660350253e-12 +156 154 -684029.893911 +157 154 1013788.37464 +158 154 1.57588193157e-6 +159 154 744469.140943 +160 154 -232474.908504 +162 154 375562.443396 +163 154 11271021.6352 +165 154 6976815.09283 +196 154 -1097857.14286 +155 155 14894680.0146 +156 155 -4451.70838056 +157 155 -7.11810358595e-6 +158 155 62702.9000587 +159 155 48242.8612457 +161 155 -7827118.14501 +197 155 -2014.84906751 +198 155 2652.92402545 +200 155 278557.022672 +201 155 211559.152089 +156 156 1451866.96532 +157 156 -2259066.56878 +158 156 -141920.845229 +159 156 -1076491.2747 +160 156 375562.443396 +162 156 -793564.308415 +163 156 -23815694.6903 +165 156 -11271021.6352 +197 156 2652.92402545 +198 156 -5978.311271 +200 156 -627722.683454 +201 156 -278557.022672 +157 157 2421361398.09 +158 157 9.67597057167e-5 +159 157 1162099482.64 +160 157 -11271021.6352 +161 157 7.11872248304e-6 +162 157 23815694.6903 +163 157 194486375.663 +165 157 84504028.1182 +199 157 -19388.5714286 +158 158 167808771.599 +159 158 74411907.4925 +160 158 -1.57772290739e-6 +162 158 3.22854218853e-6 +163 158 9.68919270203e-5 +164 158 -228049.368176 +165 158 4.73491142053e-5 +197 158 -278557.022672 +198 158 627722.683454 +200 158 42888055.5487 +201 158 18975993.8348 +159 159 741354861.136 +160 159 -6976815.09283 +161 159 -2.84748899322e-5 +162 159 11271021.6352 +163 159 84504028.1182 +165 159 68237558.0284 +197 159 -211559.152089 +198 159 278557.022672 +200 159 18975993.8348 +201 159 14537965.7548 +160 160 1355778.85444 +161 160 3.20936424108e-12 +162 160 -375562.443396 +163 160 -11271021.6352 +164 160 -4238867.37393 +165 160 -6976815.09283 +167 160 -4238867.37393 +202 160 -568952.380952 +161 161 7962947.59637 +162 161 -2103.53829486 +163 161 10097137.073 +164 161 30319.9227152 +165 161 17355.2095347 +166 161 10097137.073 +168 161 2.44887804558e-7 +203 161 -717.644689064 +204 161 1256.64230266 +206 161 131947.441779 +207 161 75352.6923517 +162 162 8531241.28002 +163 162 23815694.6903 +164 162 -62653.1021773 +165 162 11240701.7125 +167 162 -1.02806064483e-7 +203 162 1256.64230266 +204 162 -2595.06530952 +206 162 -272481.8575 +207 162 -131947.441779 +163 163 2299673852.57 +164 163 -6.19271184978e-8 +165 163 592006261.636 +166 163 449885280.472 +168 163 1.0936474439e-5 +205 163 -3811.42857143 +164 164 500079153.726 +165 164 34934494.8733 +167 164 208101719.712 +203 164 -131947.441779 +204 164 272481.8575 +206 164 18833169.809 +207 164 9115968.74745 +165 165 371500140.006 +166 165 1.0936474439e-5 +168 165 -1.044e6 +203 165 -75352.6923517 +204 165 131947.441779 +206 165 9115968.74745 +207 165 5213934.11585 +166 166 1064685280.47 +168 166 2.57966962447e-5 +167 167 427728386.378 +168 168 1.044e6 +169 169 1064685280.47 +170 169 -1.25491574077e-19 +171 169 2.57966962447e-5 +172 169 -1.72296786423e-21 +173 169 10097137.073 +174 169 1.22443902279e-7 +175 169 449885280.472 +176 169 -6.15115678378e-20 +177 169 1.0936474439e-5 +170 170 427728386.378 +171 170 5.1742291832e-6 +172 170 -4238867.37393 +173 170 3.08148791102e-33 +174 170 -1.02806064483e-7 +175 170 -6.15115678378e-20 +176 170 208101719.712 +177 170 2.53622565301e-6 +171 171 1.044e6 +172 171 -5.14030322414e-8 +173 171 2.44887804558e-7 +174 171 1.72296786423e-21 +175 171 1.0936474439e-5 +176 171 2.53622565301e-6 +177 171 -1.044e6 +172 172 1381849.35091 +173 172 2.23484491497e-21 +174 172 -308467.450515 +175 172 10257233.2606 +176 172 -4238867.37393 +177 172 6232345.95189 +178 172 -187426.357349 +180 172 308467.450514 +181 172 10257233.2606 +183 172 6232345.95189 +214 172 -568952.380952 +173 173 7200249.83514 +174 173 -2513.28460541 +175 173 10097137.073 +176 173 1.49011611938e-8 +177 173 -5.11440448463e-5 +179 173 -7064186.05146 +215 173 -717.644689064 +216 173 1256.64230266 +218 173 131947.441779 +219 173 75352.6923517 +174 174 8386799.46094 +175 174 -21556628.1215 +176 174 5.13903796673e-6 +177 174 -10257233.2606 +178 174 308467.450514 +180 174 -648275.996988 +181 174 -21556628.1215 +183 174 -10257233.2606 +215 174 1256.64230266 +216 174 -2595.06530952 +218 174 -272481.8575 +219 174 -131947.441779 +175 175 2251036522.18 +176 175 -.000175502911128 +177 175 570093221.007 +178 175 -10257233.2606 +179 175 -1.28496795723e-5 +180 175 21556628.1215 +181 175 247268663.971 +183 175 112058714.222 +217 175 -3811.42857143 +176 176 504710247.639 +177 176 37185988.0523 +178 176 2.58532265326e-6 +180 176 -5.2779277002e-6 +181 176 -.000175502911128 +182 176 -205820.7294 +183 176 -8.59677656884e-5 +215 176 -131947.441779 +216 176 272481.8575 +218 176 18833169.809 +219 176 9115968.74745 +177 177 356889961.009 +178 177 -6232345.95189 +179 177 5.13987182891e-5 +180 177 10257233.2606 +181 177 112058714.222 +183 177 79853210.6342 +215 177 -75352.6923517 +216 177 131947.441779 +218 177 9115968.74745 +219 177 5213934.11585 +178 178 2570567.00041 +180 178 -616934.901029 +181 178 4.76837158203e-7 +182 178 1.29266132663e-6 +183 178 4.47034835815e-7 +184 178 -187426.357349 +186 178 308467.450514 +187 178 10257233.2606 +189 178 6232345.95189 +220 178 -1097857.14286 +179 179 14132401.8011 +180 179 -5305.8480509 +181 179 -6.42483978614e-6 +182 179 1.11758708954e-8 +183 179 2.57138162851e-5 +185 179 -7064186.05146 +221 179 -2014.84906751 +222 179 2652.92402545 +224 179 278557.022672 +225 179 211559.152089 +180 180 1308508.61652 +181 180 -1.19209289551e-6 +182 180 -2.6673078537e-6 +183 180 -5.28991222382e-7 +184 180 308467.450514 +186 180 -648275.996988 +187 180 -21556628.1215 +189 180 -10257233.2606 +221 180 2652.92402545 +222 180 -5978.311271 +224 180 -627722.683454 +225 180 -278557.022672 +181 181 2372726014.84 +182 181 8.77514555638e-5 +183 181 1140186442.01 +184 181 -10257233.2606 +185 181 6.42483978614e-6 +186 181 21556628.1215 +187 181 247268663.971 +189 181 112058714.222 +223 181 -19388.5714286 +182 182 178279057.412 +183 182 79041961.8527 +184 182 -1.29266132663e-6 +186 182 2.6389638501e-6 +187 182 8.77514555638e-5 +188 182 -205820.7294 +189 182 4.29838828442e-5 +221 182 -278557.022672 +222 182 627722.683454 +224 182 42888055.5487 +225 182 18975993.8348 +183 183 729030309.274 +184 183 -6232345.95189 +185 183 -2.56993591446e-5 +186 183 10257233.2606 +187 183 112058714.222 +189 183 79853210.6342 +221 183 -211559.152089 +222 183 278557.022672 +224 183 18975993.8348 +225 183 14537965.7548 +184 184 2570567.00041 +186 184 -616934.901029 +187 184 -1.19209289551e-7 +188 184 -1.12219804765e-6 +189 184 5.96046447754e-8 +190 184 -187426.357349 +192 184 308467.450514 +193 184 10257233.2606 +195 184 6232345.95189 +226 184 -1097857.14286 +185 185 14132401.8011 +186 185 -5305.8480509 +188 185 1.11758708954e-8 +189 185 -2.56849452853e-5 +191 185 -7064186.05146 +227 185 -2014.84906751 +228 185 2652.92402545 +230 185 278557.022672 +231 185 211559.152089 +186 186 1308508.61652 +187 186 -4.76837158203e-7 +188 186 2.33203172684e-6 +189 186 -1.11758708954e-7 +190 186 308467.450514 +192 186 -648275.996988 +193 186 -21556628.1215 +195 186 -10257233.2606 +227 186 2652.92402545 +228 186 -5978.311271 +230 186 -627722.683454 +231 186 -278557.022672 +187 187 2372726014.84 +188 187 -7.84225562585e-5 +189 187 1140186442.01 +190 187 -10257233.2606 +192 187 21556628.1215 +193 187 247268663.971 +195 187 112058714.222 +229 187 -19388.5714286 +188 188 178279057.412 +189 188 79041961.8527 +190 188 1.12219804765e-6 +192 188 -2.35841433819e-6 +193 188 -7.84225562585e-5 +194 188 -205820.7294 +195 188 -3.73155972214e-5 +227 188 -278557.022672 +228 188 627722.683454 +230 188 42888055.5487 +231 188 18975993.8348 +189 189 729030309.274 +190 189 -6232345.95189 +191 189 2.56993591446e-5 +192 189 10257233.2606 +193 189 112058714.222 +195 189 79853210.6342 +227 189 -211559.152089 +228 189 278557.022672 +230 189 18975993.8348 +231 189 14537965.7548 +190 190 2570567.00041 +192 190 -616934.901029 +193 190 -1.78813934326e-7 +194 190 1.12219804765e-6 +196 190 -187426.357349 +198 190 308467.450514 +199 190 10257233.2606 +201 190 6232345.95189 +232 190 -1097857.14286 +191 191 14132401.8011 +192 191 -5305.8480509 +194 191 3.72529029846e-9 +195 191 2.57082283497e-5 +197 191 -7064186.05146 +233 191 -2014.84906751 +234 191 2652.92402545 +236 191 278557.022672 +237 191 211559.152089 +192 192 1308508.61652 +193 192 -2.38418579102e-7 +194 192 -2.37673521042e-6 +195 192 7.45058059692e-9 +196 192 308467.450514 +198 192 -648275.996988 +199 192 -21556628.1215 +201 192 -10257233.2606 +233 192 2652.92402545 +234 192 -5978.311271 +236 192 -627722.683454 +237 192 -278557.022672 +193 193 2372726014.84 +194 193 7.84225562585e-5 +195 193 1140186442.01 +196 193 -10257233.2606 +198 193 21556628.1215 +199 193 247268663.971 +201 193 112058714.222 +235 193 -19388.5714286 +194 194 178279057.412 +195 194 79041961.8527 +196 194 -1.12219804765e-6 +198 194 2.35841433819e-6 +199 194 7.84225562585e-5 +200 194 -205820.7294 +201 194 3.73155972214e-5 +233 194 -278557.022672 +234 194 627722.683454 +236 194 42888055.5487 +237 194 18975993.8348 +195 195 729030309.274 +196 195 -6232345.95189 +197 195 -2.56993591446e-5 +198 195 10257233.2606 +199 195 112058714.222 +201 195 79853210.6342 +233 195 -211559.152089 +234 195 278557.022672 +236 195 18975993.8348 +237 195 14537965.7548 +196 196 2615615.55157 +198 196 -684029.893911 +199 196 1013788.37464 +200 196 1.57772290739e-6 +201 196 744469.140943 +202 196 -232474.908504 +204 196 375562.443396 +205 196 11271021.6352 +207 196 6976815.09283 +238 196 -1097857.14286 +197 197 14895333.8946 +198 197 -5305.8480509 +199 197 -7.11872248304e-6 +200 197 1.11758708954e-8 +201 197 2.84891575575e-5 +203 197 -7827118.14501 +239 197 -2014.84906751 +240 197 2652.92402545 +242 197 278557.022672 +243 197 211559.152089 +198 198 1453796.92795 +199 198 -2259066.56878 +200 198 -3.25590372086e-6 +201 198 -1013788.37464 +202 198 375562.443396 +204 198 -793564.308415 +205 198 -23815694.6903 +207 198 -11271021.6352 +239 198 2652.92402545 +240 198 -5978.311271 +242 198 -627722.683454 +243 198 -278557.022672 +199 199 2421363821.66 +200 199 9.68919270203e-5 +201 199 1162099482.64 +202 199 -11271021.6352 +203 199 7.11872248304e-6 +204 199 23815694.6903 +205 199 194486375.663 +207 199 84504028.1182 +241 199 -19388.5714286 +200 200 178301286.051 +201 200 79041961.8527 +202 200 -1.57772290739e-6 +204 200 3.22854218853e-6 +205 200 9.68919270203e-5 +206 200 -228049.368176 +207 200 4.73491142053e-5 +239 200 -278557.022672 +240 200 627722.683454 +242 200 42888055.5487 +243 200 18975993.8348 +201 201 744930085.353 +202 201 -6976815.09283 +203 201 -2.84748899322e-5 +204 201 11271021.6352 +205 201 84504028.1182 +207 201 68237558.0284 +239 201 -211559.152089 +240 201 278557.022672 +242 201 18975993.8348 +243 201 14537965.7548 +202 202 1426897.90206 +204 202 -375562.443396 +205 202 -11271021.6352 +206 202 -4238867.37393 +207 202 -6976815.09283 +209 202 -4238867.37393 +244 202 -568952.380952 +203 203 7963181.9287 +204 203 -2513.28460532 +205 203 10097137.073 +206 203 1.49011611938e-8 +207 203 2.5425106287e-7 +208 203 10097137.073 +210 203 2.44887804558e-7 +245 203 -717.644689064 +246 203 1256.64230266 +248 203 131947.441779 +249 203 75352.6923517 +204 204 8532087.77237 +205 204 23815694.6903 +206 204 -1.37835741043e-7 +207 204 11271021.6352 +209 204 -1.02806064483e-7 +245 204 1256.64230266 +246 204 -2595.06530952 +248 204 -272481.8575 +249 204 -131947.441779 +205 205 2299674329 +207 205 592006261.636 +208 205 449885280.472 +210 205 1.0936474439e-5 +247 205 -3811.42857143 +206 206 504732476.278 +207 206 37185988.0523 +209 206 208101719.712 +245 206 -131947.441779 +246 206 272481.8575 +248 206 18833169.809 +249 206 9115968.74745 +207 207 372789737.088 +208 207 1.0936474439e-5 +210 207 -1.044e6 +245 207 -75352.6923517 +246 207 131947.441779 +248 207 9115968.74745 +249 207 5213934.11585 +208 208 1064685280.47 +210 208 2.57966962447e-5 +209 209 427728386.378 +210 210 1.044e6 +211 211 1064685280.47 +212 211 -2.50983148154e-19 +213 211 5.15933924894e-5 +214 211 -3.44593572847e-21 +215 211 10097137.073 +216 211 1.22443902279e-7 +217 211 449885280.472 +218 211 -1.23023135676e-19 +219 211 2.18729488781e-5 +212 212 427728386.378 +213 212 5.1742291832e-6 +214 212 -4238867.37393 +215 212 6.16297582204e-33 +216 212 -2.05612128966e-7 +217 212 -1.23023135676e-19 +218 212 208101719.712 +219 212 2.53622565301e-6 +213 213 1.044e6 +214 213 -5.14030322414e-8 +215 213 4.89775609117e-7 +216 213 3.44593572846e-21 +217 213 2.18729488781e-5 +218 213 2.53622565301e-6 +219 213 -1.044e6 +214 214 1524087.44614 +215 214 4.46968982994e-21 +216 214 -308467.450515 +217 214 10257233.2606 +218 214 -4238867.37393 +219 214 6232345.95189 +220 214 -187426.357349 +222 214 308467.450514 +223 214 10257233.2606 +225 214 6232345.95189 +256 214 -711190.47619 +215 215 7200916.3671 +216 215 -3676.41742948 +217 215 10097137.073 +218 215 71313.6688665 +219 215 40918.1464722 +221 215 -7064186.05146 +257 215 -1384.17665327 +258 215 2419.77512673 +260 215 203261.110646 +261 215 116270.838875 +216 216 8389203.71059 +217 216 -21556628.1215 +218 216 -147460.598708 +219 216 -10328546.9294 +220 216 308467.450514 +222 216 -648275.996988 +223 216 -21556628.1215 +225 216 -10257233.2606 +257 216 2419.77512673 +258 216 -4999.31495491 +260 216 -419942.456212 +261 216 -203261.110646 +217 217 2251037475.04 +218 217 -.000175502911128 +219 217 570093221.007 +220 217 -10257233.2606 +221 217 -1.28496795723e-5 +222 217 21556628.1215 +223 217 247268663.971 +225 217 112058714.222 +259 217 -4764.28571429 +218 218 513819175.231 +219 218 41590068.1195 +220 218 2.58532265326e-6 +222 218 -5.2779277002e-6 +223 218 -.000175502911128 +224 218 -205820.7294 +225 218 -8.59677656884e-5 +257 218 -203261.110646 +258 218 419942.456212 +260 218 23053384.7863 +261 218 11150792.495 +219 219 359419203.423 +220 219 -6232345.95189 +221 219 5.13987182891e-5 +222 219 10257233.2606 +223 219 112058714.222 +225 219 79853210.6342 +257 219 -116270.838875 +258 219 203261.110646 +260 219 11150792.495 +261 219 6394127.23914 +220 220 2845031.28613 +222 220 -616934.901029 +223 220 4.76837158203e-7 +224 220 1.70463278981e-7 +225 220 4.47034835815e-7 +226 220 -187426.357349 +228 220 308467.450514 +229 220 10257233.2606 +231 220 6232345.95189 +262 220 -1372321.42857 +221 221 14134246.7553 +222 221 -7689.2469996 +223 221 -6.42483978614e-6 +224 221 144494.107156 +225 221 112664.331461 +227 221 -7064186.05146 +263 221 -3859.80337559 +264 221 5036.32297415 +266 221 423051.129828 +267 221 324223.48355 +222 222 1313914.3632 +223 222 -1.19209289551e-6 +224 222 -328538.184932 +225 222 -144494.107156 +226 222 308467.450514 +228 222 -648275.996988 +229 222 -21556628.1215 +231 222 -10257233.2606 +263 222 5036.32297415 +264 222 -11384.057957 +266 222 -956260.868386 +267 222 -423051.129828 +223 223 2372730861.98 +224 223 9.32889930534e-6 +225 223 1140186442.01 +226 223 -10257233.2606 +227 223 6.42483978614e-6 +228 223 21556628.1215 +229 223 247268663.971 +231 223 112058714.222 +265 223 -24235.7142857 +224 224 198449795.148 +225 224 87897892.7641 +226 224 -1.70463278981e-7 +228 224 2.80549511912e-7 +229 224 9.32889930534e-6 +230 224 -205820.7294 +231 224 5.66828562286e-6 +263 224 -423051.129828 +264 224 956260.868386 +266 224 51547380.1769 +267 224 22695677.9734 +225 225 735970307.227 +226 225 -6232345.95189 +228 225 10257233.2606 +229 225 112058714.222 +231 225 79853210.6342 +263 225 -324223.48355 +264 225 423051.129828 +266 225 22695677.9734 +267 225 17640091.1 +226 226 2845031.28613 +228 226 -616934.901029 +229 226 -1.19209289551e-7 +231 226 5.96046447754e-8 +232 226 -187426.357349 +234 226 308467.450514 +235 226 10257233.2606 +237 226 6232345.95189 +268 226 -1372321.42857 +227 227 14134246.7553 +228 227 -7689.24699959 +230 227 144494.107156 +231 227 112664.331461 +233 227 -7064186.05146 +269 227 -3859.80337559 +270 227 5036.32297415 +272 227 423051.129828 +273 227 324223.48355 +228 228 1313914.3632 +229 228 -4.76837158203e-7 +230 228 -328538.184931 +231 228 -144494.107156 +232 228 308467.450514 +234 228 -648275.996988 +235 228 -21556628.1215 +237 228 -10257233.2606 +269 228 5036.32297415 +270 228 -11384.057957 +272 228 -956260.868386 +273 228 -423051.129828 +229 229 2372730861.98 +231 229 1140186442.01 +232 229 -10257233.2606 +234 229 21556628.1215 +235 229 247268663.971 +237 229 112058714.222 +271 229 -24235.7142857 +230 230 198449795.148 +231 230 87897892.7641 +236 230 -205820.7294 +269 230 -423051.129828 +270 230 956260.868386 +272 230 51547380.1769 +273 230 22695677.9734 +231 231 735970307.227 +232 231 -6232345.95189 +234 231 10257233.2606 +235 231 112058714.222 +237 231 79853210.6342 +269 231 -324223.48355 +270 231 423051.129828 +272 231 22695677.9734 +273 231 17640091.1 +232 232 2845031.28613 +234 232 -616934.901029 +235 232 -1.78813934326e-7 +238 232 -187426.357349 +240 232 308467.450514 +241 232 10257233.2606 +243 232 6232345.95189 +274 232 -1372321.42857 +233 233 14134246.7553 +234 233 -7689.24699959 +236 233 144494.107156 +237 233 112664.331461 +239 233 -7064186.05146 +275 233 -3859.80337559 +276 233 5036.32297415 +278 233 423051.129828 +279 233 324223.48355 +234 234 1313914.3632 +235 234 -2.38418579102e-7 +236 234 -328538.184931 +237 234 -144494.107156 +238 234 308467.450514 +240 234 -648275.996988 +241 234 -21556628.1215 +243 234 -10257233.2606 +275 234 5036.32297415 +276 234 -11384.057957 +278 234 -956260.868386 +279 234 -423051.129828 +235 235 2372730861.98 +237 235 1140186442.01 +238 235 -10257233.2606 +240 235 21556628.1215 +241 235 247268663.971 +243 235 112058714.222 +277 235 -24235.7142857 +236 236 198449795.148 +237 236 87897892.7641 +242 236 -205820.7294 +275 236 -423051.129828 +276 236 956260.868386 +278 236 51547380.1769 +279 236 22695677.9734 +237 237 735970307.227 +238 237 -6232345.95189 +240 237 10257233.2606 +241 237 112058714.222 +243 237 79853210.6342 +275 237 -324223.48355 +276 237 423051.129828 +278 237 22695677.9734 +279 237 17640091.1 +238 238 2890079.83728 +240 238 -684029.893911 +241 238 1013788.37464 +242 238 2.94401111721e-6 +243 238 744469.140943 +244 238 -232474.908504 +246 238 375562.443396 +247 238 11271021.6352 +249 238 6976815.09283 +280 238 -1372321.42857 +239 239 14897178.8489 +240 239 -7689.24699959 +241 239 -7.11872248304e-6 +242 239 144494.107156 +243 239 112664.331518 +245 239 -7827118.14501 +281 239 -3859.80337559 +282 239 5036.32297415 +284 239 423051.129828 +285 239 324223.48355 +240 240 1459202.67463 +241 240 -2259066.56878 +242 240 -328538.184938 +243 240 -1158282.48179 +244 240 375562.443396 +246 240 -793564.308415 +247 240 -23815694.6903 +249 240 -11271021.6352 +281 240 5036.32297415 +282 240 -11384.057957 +284 240 -956260.868386 +285 240 -423051.129828 +241 241 2421368668.8 +242 241 .00018353291958 +243 241 1162099482.64 +244 241 -11271021.6352 +245 241 7.11872248304e-6 +246 241 23815694.6903 +247 241 194486375.663 +249 241 84504028.1182 +283 241 -24235.7142857 +242 242 198472023.786 +243 242 87897892.7642 +244 242 -2.94401111721e-6 +246 242 6.11551232461e-6 +247 242 .00018353291958 +248 242 -228049.368176 +249 242 8.83528520485e-5 +281 242 -423051.129828 +282 242 956260.868386 +284 242 51547380.1769 +285 242 22695677.9734 +243 243 751870083.305 +244 243 -6976815.09283 +245 243 -5.69497798643e-5 +246 243 11271021.6352 +247 243 84504028.1182 +249 243 68237558.0284 +281 243 -324223.48355 +282 243 423051.129828 +284 243 22695677.9734 +285 243 17640091.1 +244 244 1569135.9973 +246 244 -375562.443397 +247 244 -11271021.6352 +248 244 -4238867.37393 +249 244 -6976815.09283 +251 244 -4238867.37393 +286 244 -711190.47619 +245 245 7963848.46066 +246 245 -3676.41742939 +247 245 10097137.073 +248 245 71313.6688665 +249 245 40918.1465236 +250 245 10097137.073 +252 245 4.89775609117e-7 +287 245 -1384.17665327 +288 245 2419.77512673 +290 245 203261.110646 +291 245 116270.838875 +246 246 8534492.02201 +247 246 23815694.6903 +248 246 -147460.598713 +249 246 11199707.9663 +251 246 -2.05612128966e-7 +287 246 2419.77512673 +288 246 -4999.31495491 +290 246 -419942.456212 +291 246 -203261.110646 +247 247 2299675281.85 +249 247 592006261.636 +250 247 449885280.472 +252 247 2.18729488781e-5 +289 247 -4764.28571429 +248 248 513841403.87 +249 248 41590068.1196 +251 248 208101719.712 +287 248 -203261.110646 +288 248 419942.456212 +290 248 23053384.7863 +291 248 11150792.495 +249 249 375318979.502 +250 249 2.18729488781e-5 +252 249 -1.044e6 +287 249 -116270.838875 +288 249 203261.110646 +290 249 11150792.495 +291 249 6394127.23914 +250 250 1064685280.47 +252 250 5.15933924894e-5 +251 251 427728386.378 +252 252 1.044e6 +253 253 1064685280.47 +254 253 -2.50983148154e-19 +255 253 5.15933924894e-5 +256 253 -3.44593572847e-21 +257 253 10097137.073 +258 253 1.22443902279e-7 +259 253 449885280.472 +260 253 -1.23023135676e-19 +261 253 2.18729488781e-5 +254 254 427728386.378 +255 254 5.1742291832e-6 +256 254 -4238867.37393 +257 254 6.16297582204e-33 +258 254 -2.05612128966e-7 +259 254 -1.23023135676e-19 +260 254 208101719.712 +261 254 2.53622565301e-6 +255 255 1.044e6 +256 255 -5.14030322414e-8 +257 255 4.89775609117e-7 +258 255 3.44593572846e-21 +259 255 2.18729488781e-5 +260 255 2.53622565301e-6 +261 255 -1.044e6 +256 256 1452968.39852 +257 256 4.46968982994e-21 +258 256 -308467.450515 +259 256 10257233.2606 +260 256 -4238867.37394 +261 256 6232345.95189 +262 256 -187426.357349 +264 256 308467.450514 +265 256 10257233.2606 +267 256 6232345.95189 +298 256 -497833.333333 +257 257 7200682.03477 +258 257 -3266.67111903 +259 257 10097137.073 +260 257 -101633.591582 +261 257 -58273.3561599 +263 257 -7064186.05146 +299 257 -483.31235681 +300 257 846.8959922 +302 257 101627.519064 +303 257 57997.4828172 +258 258 8388357.21824 +259 258 -21556628.1215 +260 258 210113.7009 +261 258 -10155599.669 +262 258 308467.450514 +264 258 -648275.996988 +265 258 -21556628.1215 +267 258 -10257233.2606 +299 258 846.8959922 +300 258 -1748.57296102 +302 258 -209828.755322 +303 258 -101627.519064 +259 259 2251036998.61 +260 259 -.000332348023644 +261 259 570093221.007 +262 259 -10257233.2606 +263 259 -1.28496795723e-5 +264 259 21556628.1215 +265 259 247268663.971 +267 259 112058714.222 +301 259 -3335 +260 260 509165852.679 +261 260 39338574.9404 +262 260 4.82971874856e-6 +264 260 -9.99475637658e-6 +265 260 -.000332348023644 +266 260 -205820.7294 +267 260 -.000160598960131 +299 260 -101627.519064 +300 260 209828.755322 +302 260 16624203.5638 +303 260 8049103.72824 +261 261 358129606.341 +262 261 -6232345.95189 +263 261 .000102797436578 +264 261 10257233.2606 +265 261 112058714.222 +267 261 79853210.6342 +299 261 -57997.4828172 +300 261 101627.519064 +302 261 8049103.72824 +303 261 4598861.67962 +262 262 2707799.14327 +264 262 -616934.901029 +265 262 4.76837158203e-7 +266 262 2.41485937428e-6 +267 262 4.47034835815e-7 +268 262 -187426.357349 +270 262 308467.450514 +271 262 10257233.2606 +273 262 6232345.95189 +304 262 -960625 +263 263 14133592.8754 +264 263 -6835.10732926 +265 263 -6.42483978614e-6 +266 263 -207197.007215 +267 263 -160907.192627 +269 263 -7064186.05146 +305 263 -1360.96909059 +306 263 1798.78435511 +308 263 215854.122614 +309 263 163316.290872 +264 264 1311984.40059 +265 264 -1.19209289551e-6 +266 264 470459.030152 +267 264 207197.007214 +268 264 308467.450514 +270 264 -648275.996988 +271 264 -21556628.1215 +273 264 -10257233.2606 +305 264 1798.78435511 +306 264 -4048.34865191 +308 264 -485801.838229 +309 264 -215854.122614 +265 265 2372728438.41 +266 265 .000166174011822 +267 265 1140186442.01 +268 265 -10257233.2606 +269 265 6.42483978614e-6 +270 265 21556628.1215 +271 265 247268663.971 +273 265 112058714.222 +307 265 -16965 +266 266 187957280.696 +267 266 83267838.404 +268 266 -2.41485937428e-6 +270 266 4.99737818829e-6 +271 266 .000166174011822 +272 266 -205820.7294 +273 266 8.02994800656e-5 +305 266 -215854.122614 +306 266 485801.838229 +308 266 38151247.6502 +309 266 16914062.8611 +267 267 732395083.01 +268 267 -6232345.95189 +269 267 -5.13987182891e-5 +270 267 10257233.2606 +271 267 112058714.222 +273 267 79853210.6342 +305 267 -163316.290872 +306 267 215854.122614 +308 267 16914062.8611 +309 267 12881677.8419 +268 268 2707799.14327 +270 268 -616934.901029 +271 268 -1.19209289551e-7 +273 268 5.96046447754e-8 +274 268 -187426.357349 +276 268 308467.450514 +277 268 10257233.2606 +279 268 6232345.95189 +310 268 -960625 +269 269 14133592.8754 +270 269 -6835.10732926 +272 269 -207197.007215 +273 269 -160907.192678 +275 269 -7064186.05146 +311 269 -1360.96909059 +312 269 1798.78435511 +314 269 215854.122614 +315 269 163316.290872 +270 270 1311984.40059 +271 270 -4.76837158203e-7 +272 270 470459.030157 +273 270 207197.007214 +274 270 308467.450514 +276 270 -648275.996988 +277 270 -21556628.1215 +279 270 -10257233.2606 +311 270 1798.78435511 +312 270 -4048.34865191 +314 270 -485801.838229 +315 270 -215854.122614 +271 271 2372728438.41 +273 271 1140186442.01 +274 271 -10257233.2606 +276 271 21556628.1215 +277 271 247268663.971 +279 271 112058714.222 +313 271 -16965 +272 272 187957280.696 +273 272 83267838.4039 +278 272 -205820.7294 +311 272 -215854.122614 +312 272 485801.838229 +314 272 38151247.6502 +315 272 16914062.8611 +273 273 732395083.01 +274 273 -6232345.95189 +276 273 10257233.2606 +277 273 112058714.222 +279 273 79853210.6342 +311 273 -163316.290872 +312 273 215854.122614 +314 273 16914062.8611 +315 273 12881677.8419 +274 274 2707799.14327 +276 274 -616934.901029 +277 274 -1.78813934326e-7 +280 274 -187426.357349 +282 274 308467.450514 +283 274 10257233.2606 +285 274 6232345.95189 +316 274 -960625 +275 275 14133592.8754 +276 275 -6835.10732926 +278 275 -207197.007215 +279 275 -160907.192678 +281 275 -7064186.05146 +317 275 -1360.9690906 +318 275 1798.78435511 +320 275 215854.122614 +321 275 163316.290872 +276 276 1311984.40059 +277 276 -2.38418579102e-7 +278 276 470459.030157 +279 276 207197.007215 +280 276 308467.450514 +282 276 -648275.996988 +283 276 -21556628.1215 +285 276 -10257233.2606 +317 276 1798.78435511 +318 276 -4048.34865191 +320 276 -485801.838229 +321 276 -215854.122614 +277 277 2372728438.41 +279 277 1140186442.01 +280 277 -10257233.2606 +282 277 21556628.1215 +283 277 247268663.971 +285 277 112058714.222 +319 277 -16965 +278 278 187957280.696 +279 278 83267838.4039 +284 278 -205820.7294 +317 278 -215854.122614 +318 278 485801.838229 +320 278 38151247.6502 +321 278 16914062.8611 +279 279 732395083.01 +280 279 -6232345.95189 +282 279 10257233.2606 +283 279 112058714.222 +285 279 79853210.6342 +317 279 -163316.290872 +318 279 215854.122614 +320 279 16914062.8611 +321 279 12881677.8419 +280 280 2752847.69442 +282 280 -684029.893911 +283 280 1013788.37464 +284 280 2.94401111721e-6 +285 280 744469.140943 +286 280 -232474.908504 +288 280 375562.443396 +289 280 11271021.6352 +291 280 6976815.09283 +322 280 -960625 +281 281 14896524.9689 +282 281 -6835.10732926 +283 281 -7.11872248304e-6 +284 281 -207197.007215 +285 281 -160907.192621 +287 281 -7827118.14501 +323 281 -1360.96909059 +324 281 1798.78435511 +326 281 215854.122614 +327 281 163316.290872 +282 282 1457272.71201 +283 282 -2259066.56878 +284 282 470459.030151 +285 282 -806591.367422 +286 282 375562.443396 +288 282 -793564.308415 +289 282 -23815694.6903 +291 282 -11271021.6352 +323 282 1798.78435511 +324 282 -4048.34865191 +326 282 -485801.838229 +327 282 -215854.122614 +283 283 2421366245.23 +284 283 .00018353291958 +285 283 1162099482.64 +286 283 -11271021.6352 +287 283 7.11872248304e-6 +288 283 23815694.6903 +289 283 194486375.663 +291 283 84504028.1182 +325 283 -16965 +284 284 187979509.334 +285 284 83267838.404 +286 284 -2.94401111721e-6 +288 284 6.11551232461e-6 +289 284 .00018353291958 +290 284 -228049.368176 +291 284 8.83528520485e-5 +323 284 -215854.122614 +324 284 485801.838229 +326 284 38151247.6502 +327 284 16914062.8611 +285 285 748294859.089 +286 285 -6976815.09283 +287 285 -5.69497798643e-5 +288 285 11271021.6352 +289 285 84504028.1182 +291 285 68237558.0284 +323 285 -163316.290872 +324 285 215854.122614 +326 285 16914062.8611 +327 285 12881677.8419 +286 286 1498016.94968 +288 286 -375562.443397 +289 286 -11271021.6352 +290 286 -4238867.37393 +291 286 -6976815.09283 +293 286 -4238867.37393 +328 286 -497833.333333 +287 287 7963614.12833 +288 287 -3266.67111893 +289 287 10097137.073 +290 287 -101633.591582 +291 287 -58273.3560571 +292 287 10097137.073 +294 287 4.89775609117e-7 +329 287 -483.31235681 +330 287 846.8959922 +332 287 101627.519064 +333 287 57997.4828172 +288 288 8533645.52966 +289 288 23815694.6903 +290 288 210113.70089 +291 288 11372655.2268 +293 288 -2.05612128966e-7 +329 288 846.8959922 +330 288 -1748.57296102 +332 288 -209828.755322 +333 288 -101627.519064 +289 289 2299674805.43 +291 289 592006261.636 +292 289 449885280.472 +294 289 2.18729488781e-5 +331 289 -3335 +290 290 509188081.317 +291 290 39338574.9406 +293 290 208101719.712 +329 290 -101627.519064 +330 290 209828.755322 +332 290 16624203.5638 +333 290 8049103.72824 +291 291 374029382.42 +292 291 2.18729488781e-5 +294 291 -1.044e6 +329 291 -57997.4828172 +330 291 101627.519064 +332 291 8049103.72824 +333 291 4598861.67962 +292 292 1064685280.47 +294 292 5.15933924894e-5 +293 293 427728386.378 +294 294 1.044e6 +295 295 1064685280.47 +296 295 -2.50983148154e-19 +297 295 5.15933924894e-5 +298 295 -3.44593572847e-21 +299 295 10097137.073 +300 295 1.22443902279e-7 +301 295 449885280.472 +302 295 -1.23023135676e-19 +303 295 2.18729488781e-5 +296 296 427728386.378 +297 296 5.1742291832e-6 +298 296 -4238867.37393 +299 296 6.16297582204e-33 +300 296 -2.05612128966e-7 +301 296 -1.23023135676e-19 +302 296 208101719.712 +303 296 2.53622565301e-6 +297 297 1.044e6 +298 297 -5.14030322414e-8 +299 297 4.89775609117e-7 +300 297 3.44593572846e-21 +301 297 2.18729488781e-5 +302 297 2.53622565301e-6 +303 297 -1.044e6 +298 298 1239611.25567 +299 298 -3.20936423661e-12 +300 298 -308467.450515 +301 298 10257233.2606 +302 298 -4238867.37394 +303 298 6232345.95189 +304 298 -187426.357349 +306 298 308467.450514 +307 298 10257233.2606 +309 298 6232345.95189 +340 298 -497833.333333 +341 298 3.20936424108e-12 +342 298 1.87994358829e-9 +343 298 -1.45944988666e-24 +344 298 -7.95158921856e-10 +345 298 -3.85123708929e-10 +299 299 7199781.17048 +300 299 -1693.79198449 +301 299 10097137.073 +302 299 -9.31322574615e-10 +303 299 -.000102307880297 +305 299 -7064186.05146 +340 299 3.20936424108e-12 +341 299 -483.31235681 +342 299 846.8959922 +343 299 2.1978501391e-10 +344 299 101627.519064 +345 299 57997.4828172 +300 300 8385106.47624 +301 300 -21556628.1215 +302 300 9.79099422693e-6 +303 300 -10257233.2606 +304 300 308467.450514 +306 300 -648275.996988 +307 300 -21556628.1215 +309 300 -10257233.2606 +340 300 1.87994358829e-9 +341 300 846.8959922 +342 300 -1748.57296102 +343 300 -3.85123708929e-10 +344 300 -209828.755322 +345 300 -101627.519064 +301 301 2251035569.32 +302 301 -.000332286096526 +303 301 570093221.007 +304 301 -10257233.2606 +305 301 -1.28496795723e-5 +306 301 21556628.1215 +307 301 247268663.971 +309 301 112058714.222 +340 301 1.45944988666e-24 +341 301 -2.1978501391e-10 +342 301 3.85123708929e-10 +343 301 -3335 +344 301 3.05025716452e-8 +345 301 1.7440306236e-8 +302 302 495403602.535 +303 302 32683001.6941 +304 302 4.82971874856e-6 +306 302 -9.99475637658e-6 +307 302 -.000332348023644 +308 302 -205820.7294 +309 302 -.000160598960131 +340 302 7.95158921865e-10 +341 302 -101627.519064 +342 302 209828.755322 +343 302 3.05025716452e-8 +344 302 16624203.5638 +345 302 8049103.72824 +303 303 354310766.846 +304 303 -6232345.95189 +305 303 .000102797436578 +306 303 10257233.2606 +307 303 112058714.222 +309 303 79853210.6342 +340 303 3.85123708929e-10 +341 303 -57997.4828172 +342 303 101627.519064 +343 303 1.7440306236e-8 +344 303 8049103.72824 +345 303 4598861.67962 +304 304 2296102.7147 +306 304 -616934.901029 +307 304 4.76837158203e-7 +308 304 2.41485937428e-6 +309 304 4.47034835815e-7 +310 304 -187426.357349 +312 304 308467.450514 +313 304 10257233.2606 +315 304 6232345.95189 +346 304 -960625 +305 305 14131094.0411 +306 305 -3597.56871023 +307 305 -6.42483978614e-6 +308 305 -4.65661287308e-9 +309 305 5.13987615704e-5 +311 305 -7064186.05146 +347 305 -1360.96909059 +348 305 1798.78435511 +350 305 215854.122614 +351 305 163316.290872 +306 306 1304648.69128 +307 306 -1.19209289551e-6 +308 306 -4.99375164509e-6 +309 306 -4.842877388e-7 +310 306 308467.450514 +312 306 -648275.996988 +313 306 -21556628.1215 +315 306 -10257233.2606 +347 306 1798.78435511 +348 306 -4048.34865191 +350 306 -485801.838229 +351 306 -215854.122614 +307 307 2372721167.7 +308 307 .000166174011822 +309 307 1140186442.01 +310 307 -10257233.2606 +311 307 6.42483978614e-6 +312 307 21556628.1215 +313 307 247268663.971 +315 307 112058714.222 +349 307 -16965 +308 308 157294028.508 +309 308 69781853.1324 +310 308 -2.41485937428e-6 +312 308 4.99737818829e-6 +313 308 .000166174011822 +314 308 -205820.7294 +315 308 8.02994800656e-5 +347 308 -215854.122614 +348 308 485801.838229 +350 308 38151247.6502 +351 308 16914062.8611 +309 309 721879860.841 +310 309 -6232345.95189 +311 309 -5.13987182891e-5 +312 309 10257233.2606 +313 309 112058714.222 +315 309 79853210.6342 +347 309 -163316.290872 +348 309 215854.122614 +350 309 16914062.8611 +351 309 12881677.8419 +310 310 2296102.7147 +311 310 -6.81660350253e-12 +312 310 -616934.901029 +313 310 -1.19209289551e-7 +314 310 -2.24623707112e-6 +315 310 5.87866523551e-8 +316 310 -187426.357349 +318 310 308467.450514 +319 310 10257233.2606 +321 310 6232345.95189 +352 310 -960625 +353 310 6.81660350253e-12 +354 310 3.62500581767e-9 +355 310 -3.09983238482e-24 +356 310 -1.84097582492e-9 +357 310 -8.17992420304e-10 +311 311 14131094.0411 +312 311 -3597.56871023 +313 311 6.18897088587e-10 +314 311 -4.65661287308e-9 +315 311 -5.13978302479e-5 +317 311 -7064186.05146 +352 311 6.81660350253e-12 +353 311 -1360.96909059 +354 311 1798.78435511 +355 311 6.18897088587e-10 +356 311 215854.122614 +357 311 163316.290872 +312 312 1304648.69128 +313 312 -4.77655150623e-7 +314 312 4.72366809845e-6 +315 312 -6.70552253723e-8 +316 312 308467.450514 +318 312 -648275.996988 +319 312 -21556628.1215 +321 312 -10257233.2606 +352 312 3.62500581767e-9 +353 312 1798.78435511 +354 312 -4048.34865191 +355 312 -8.17992420304e-10 +356 312 -485801.838229 +357 312 -215854.122614 +313 313 2372721167.7 +314 313 -.000156712891213 +315 313 1140186442.01 +316 313 -10257233.2606 +318 313 21556628.1215 +319 313 247268663.971 +321 313 112058714.222 +352 313 3.09983238482e-24 +353 313 -6.18897088587e-10 +354 313 8.17992420304e-10 +355 313 -16965 +356 313 6.40968773234e-8 +357 313 4.88801971864e-8 +314 314 157294028.508 +315 314 69781853.1322 +316 314 2.2443960953e-6 +318 314 -4.71682867638e-6 +319 314 -.000156845112517 +320 314 -205820.7294 +321 314 -7.46311944427e-5 +352 314 1.84097582493e-9 +353 314 -215854.122614 +354 314 485801.838229 +355 314 6.40968773234e-8 +356 314 38151247.6502 +357 314 16914062.8611 +315 315 721879860.841 +316 315 -6232345.95189 +317 315 5.13987182891e-5 +318 315 10257233.2606 +319 315 112058714.222 +321 315 79853210.6342 +352 315 8.17992420304e-10 +353 315 -163316.290872 +354 315 215854.122614 +355 315 4.88801971864e-8 +356 315 16914062.8611 +357 315 12881677.8419 +316 316 2296102.7147 +317 316 -1.36332070051e-11 +318 316 -616934.901029 +319 316 -1.78813934326e-7 +320 316 2.24071414365e-6 +321 316 -1.63598484061e-9 +322 316 -187426.357349 +324 316 308467.450514 +325 316 10257233.2606 +327 316 6232345.95189 +358 316 -960625 +359 316 1.36332070051e-11 +360 316 7.25001163534e-9 +361 316 -1.23993295393e-23 +362 316 -3.68195164984e-9 +363 316 -1.63598484061e-9 +317 317 14131094.0411 +318 317 -3597.56871023 +319 317 1.23779417717e-9 +320 317 -9.31322574615e-9 +321 317 5.13913109899e-5 +323 317 -7064186.05146 +358 317 1.36332070051e-11 +359 317 -1360.96909059 +360 317 1798.78435511 +361 317 1.23779417717e-9 +362 317 215854.122614 +363 317 163316.290872 +318 318 1304648.69128 +319 318 -2.40054563942e-7 +320 318 -4.69572842121e-6 +321 318 5.21540641785e-8 +322 318 308467.450514 +324 318 -648275.996988 +325 318 -21556628.1215 +327 318 -10257233.2606 +358 318 7.25001163534e-9 +359 318 1798.78435511 +360 318 -4048.34865191 +361 318 -1.63598484061e-9 +362 318 -485801.838229 +363 318 -215854.122614 +319 319 2372721167.7 +320 319 .000157109555124 +321 319 1140186442.01 +322 319 -10257233.2606 +324 319 21556628.1215 +325 319 247268663.971 +327 319 112058714.222 +358 319 1.23993295393e-23 +359 319 -1.23779417717e-9 +360 319 1.63598484061e-9 +361 319 -16965 +362 319 1.28193754647e-7 +363 319 9.77603943728e-8 +320 320 157294028.508 +321 320 69781853.1324 +322 320 -2.2443960953e-6 +324 320 4.71682867638e-6 +325 320 .000156845112517 +326 320 -205820.7294 +327 320 7.46311944427e-5 +358 320 3.68195164986e-9 +359 320 -215854.122614 +360 320 485801.838229 +361 320 1.28193754647e-7 +362 320 38151247.6502 +363 320 16914062.8611 +321 321 721879860.841 +322 321 -6232345.95189 +323 321 -5.13987182891e-5 +324 321 10257233.2606 +325 321 112058714.222 +327 321 79853210.6342 +358 321 1.63598484061e-9 +359 321 -163316.290872 +360 321 215854.122614 +361 321 9.77603943728e-8 +362 321 16914062.8611 +363 321 12881677.8419 +322 322 2341151.26585 +323 322 -6.81660350253e-12 +324 322 -684029.893911 +325 322 1013788.37464 +326 322 2.94217014139e-6 +327 322 744469.140943 +328 322 -232474.908504 +330 322 375562.443396 +331 322 11271021.6352 +333 322 6976815.09283 +364 322 -960625 +365 322 6.81660350253e-12 +366 322 3.62500581767e-9 +367 322 -3.09983238482e-24 +368 322 -1.84097582492e-9 +369 322 -8.17992420304e-10 +323 323 14894026.1346 +324 323 -3597.56871023 +325 323 -7.11810358595e-6 +326 323 -4.65661287308e-9 +327 323 5.69503754377e-5 +329 323 -7827118.14501 +364 323 6.81660350253e-12 +365 323 -1360.96909059 +366 323 1798.78435511 +367 323 6.18897088587e-10 +368 323 215854.122614 +369 323 163316.290872 +324 324 1449937.00271 +325 324 -2259066.56878 +326 324 -6.10947608948e-6 +327 324 -1013788.37464 +328 324 375562.443396 +330 324 -793564.308415 +331 324 -23815694.6903 +333 324 -11271021.6352 +364 324 3.62500581767e-9 +365 324 1798.78435511 +366 324 -4048.34865191 +367 324 -8.17992420304e-10 +368 324 -485801.838229 +369 324 -215854.122614 +325 325 2421358974.52 +326 325 .000183665140883 +327 325 1162099482.64 +328 325 -11271021.6352 +329 325 7.11872248304e-6 +330 325 23815694.6903 +331 325 194486375.663 +333 325 84504028.1182 +364 325 3.09983238482e-24 +365 325 -6.18897088587e-10 +366 325 8.17992420304e-10 +367 325 -16965 +368 325 6.40968773234e-8 +369 325 4.88801971864e-8 +326 326 157316257.147 +327 326 69781853.1324 +328 326 -2.94401111721e-6 +330 326 6.11551232461e-6 +331 326 .00018353291958 +332 326 -228049.368176 +333 326 8.83528520485e-5 +364 326 1.84097582493e-9 +365 326 -215854.122614 +366 326 485801.838229 +367 326 6.40968773234e-8 +368 326 38151247.6502 +369 326 16914062.8611 +327 327 737779636.92 +328 327 -6976815.09283 +329 327 -5.69497798643e-5 +330 327 11271021.6352 +331 327 84504028.1182 +333 327 68237558.0284 +364 327 8.17992420304e-10 +365 327 -163316.290872 +366 327 215854.122614 +367 327 4.88801971864e-8 +368 327 16914062.8611 +369 327 12881677.8419 +328 328 1284659.80682 +329 328 -3.20936424108e-12 +330 328 -375562.443397 +331 328 -11271021.6352 +332 328 -4238867.37393 +333 328 -6976815.09283 +335 328 -4238867.37393 +370 328 -497833.333333 +371 328 3.20936424108e-12 +372 328 1.87994358829e-9 +373 328 -1.45944988666e-24 +374 328 -7.95158921856e-10 +375 328 -3.85123708929e-10 +329 329 7962713.26403 +330 329 -1693.7919844 +331 329 10097137.073 +332 329 -9.31322574615e-10 +333 329 4.8941001296e-7 +334 329 10097137.073 +336 329 4.89775609117e-7 +370 329 3.20936424108e-12 +371 329 -483.31235681 +372 329 846.8959922 +373 329 2.1978501391e-10 +374 329 101627.519064 +375 329 57997.4828172 +330 330 8530394.78767 +331 330 23815694.6903 +332 330 -2.03028321266e-7 +333 330 11271021.6352 +335 330 -2.05612128966e-7 +370 330 1.87994358829e-9 +371 330 846.8959922 +372 330 -1748.57296102 +373 330 -3.85123708929e-10 +374 330 -209828.755322 +375 330 -101627.519064 +331 331 2299673376.14 +332 331 6.19271184978e-8 +333 331 592006261.636 +334 331 449885280.472 +336 331 2.18729488781e-5 +370 331 1.45944988666e-24 +371 331 -2.1978501391e-10 +372 331 3.85123708929e-10 +373 331 -3335 +374 331 3.05025716452e-8 +375 331 1.7440306236e-8 +332 332 495425831.174 +333 332 32683001.6942 +335 332 208101719.712 +370 332 7.95158921865e-10 +371 332 -101627.519064 +372 332 209828.755322 +373 332 3.05025716452e-8 +374 332 16624203.5638 +375 332 8049103.72824 +333 333 370210542.925 +334 333 2.18729488781e-5 +336 333 -1.044e6 +370 333 3.85123708929e-10 +371 333 -57997.4828172 +372 333 101627.519064 +373 333 1.7440306236e-8 +374 333 8049103.72824 +375 333 4598861.67962 +334 334 1064685280.47 +336 334 5.15933924894e-5 +335 335 427728386.378 +336 336 1.044e6 +337 337 1064685280.47 +338 337 -2.50983148154e-19 +339 337 5.15933924894e-5 +340 337 -3.44593572846e-21 +341 337 10097137.073 +342 337 1.22443902279e-7 +343 337 449885280.472 +344 337 -1.23023135676e-19 +345 337 2.18729488781e-5 +338 338 427728386.378 +339 338 5.1742291832e-6 +340 338 -4238867.37393 +341 338 3.08148791102e-33 +342 338 -2.05612128966e-7 +343 338 -1.23023135676e-19 +344 338 208101719.712 +345 338 2.53622565301e-6 +339 339 1.044e6 +340 339 -5.14030322414e-8 +341 339 4.89775609117e-7 +342 339 3.44593572846e-21 +343 339 2.18729488781e-5 +344 339 2.53622565301e-6 +345 339 -1.044e6 +340 340 1239611.25567 +341 340 3.77268610595e-9 +342 340 -308467.450515 +343 340 10257233.2606 +344 340 -4238867.37394 +345 340 6232345.95189 +346 340 -187426.357349 +348 340 308467.450514 +349 340 10257233.2606 +351 340 6232345.95189 +382 340 -497833.333333 +383 340 -3.77589547019e-9 +384 340 -3.76630590506e-9 +385 340 -8.72165868025e-24 +386 340 8.20070425853e-10 +387 340 3.30677390038e-10 +341 341 7199781.17048 +342 341 -1693.79198449 +343 341 10097137.073 +344 341 -5.58793544769e-9 +345 341 -.000102310208604 +347 341 -7064186.05146 +382 341 -3.77589547019e-9 +383 341 -483.31235681 +384 341 846.8959922 +385 341 -1.20981744568e-9 +386 341 101627.519064 +387 341 57997.4828172 +342 342 8385106.47624 +343 342 -21556628.1215 +344 342 9.51625406742e-6 +345 342 -10257233.2606 +346 342 308467.450514 +348 342 -648275.996988 +349 342 -21556628.1215 +351 342 -10257233.2606 +382 342 -3.76630590506e-9 +383 342 846.8959922 +384 342 -1748.57296102 +385 342 2.36056526157e-9 +386 342 -209828.755322 +387 342 -101627.519064 +343 343 2251035569.32 +344 343 -.000323336705588 +345 343 570093221.007 +346 343 -10257233.2606 +347 343 -6.42483978614e-6 +348 343 21556628.1215 +349 343 247268663.971 +351 343 112058714.222 +382 343 8.72165868023e-24 +383 343 1.20981744568e-9 +384 343 -2.36056526157e-9 +385 343 -3335 +386 343 -1.87027295185e-7 +387 343 -9.58857557624e-8 +344 344 495403602.535 +345 344 32683001.6941 +346 344 4.65925546958e-6 +348 344 -9.71420686467e-6 +349 344 -.000323019124339 +350 344 -205820.7294 +351 344 -.000154930674508 +382 344 -8.20070425873e-10 +383 344 -101627.519064 +384 344 209828.755322 +385 344 -1.87027295185e-7 +386 344 16624203.5638 +387 344 8049103.72824 +345 345 354310766.846 +346 345 -6232345.95189 +347 345 .000102797436578 +348 345 10257233.2606 +349 345 112058714.222 +351 345 79853210.6342 +382 345 -3.30677390018e-10 +383 345 -57997.4828172 +384 345 101627.519064 +385 345 -9.58857557624e-8 +386 345 8049103.72824 +387 345 4598861.67962 +346 346 2296102.7147 +347 346 1.36332070051e-11 +348 346 -616934.901029 +349 346 4.76837158203e-7 +350 346 2.24807804695e-6 +351 346 4.48670820656e-7 +352 346 -187426.357349 +354 346 308467.450514 +355 346 10257233.2606 +357 346 6232345.95189 +388 346 -960625 +389 346 -1.36332070051e-11 +390 346 -7.25001163534e-9 +391 346 -1.23993295393e-23 +392 346 3.68195164984e-9 +393 346 1.63598484061e-9 +347 347 14131094.0411 +348 347 -3597.56871023 +349 347 -1.23779417717e-9 +350 347 -2.88709998131e-8 +351 347 5.13838604093e-5 +353 347 -7064186.05146 +388 347 -1.36332070051e-11 +389 347 -1360.96909059 +390 347 1798.78435511 +391 347 -1.23779417717e-9 +392 347 215854.122613 +393 347 163316.290871 +348 348 1304648.69128 +349 348 -1.19045691067e-6 +350 348 -4.65847551823e-6 +351 348 -4.58210706711e-7 +352 348 308467.450514 +354 348 -648275.996988 +355 348 -21556628.1215 +357 348 -10257233.2606 +388 348 -7.25001163534e-9 +389 348 1798.78435511 +390 348 -4048.34865191 +391 348 1.63598484061e-9 +392 348 -485801.838229 +393 348 -215854.122613 +349 349 2372721167.7 +350 349 .00015658066991 +351 349 1140186442.01 +352 349 -10257233.2606 +354 349 21556628.1215 +355 349 247268663.971 +357 349 112058714.222 +388 349 1.23993295393e-23 +389 349 1.23779417717e-9 +390 349 -1.63598484061e-9 +391 349 -16965 +392 349 -1.28193754647e-7 +393 349 -9.77603943728e-8 +350 350 157294028.508 +351 350 69781853.1324 +352 350 -2.2443960953e-6 +354 350 4.71682867638e-6 +355 350 .000156845112517 +356 350 -205820.7294 +357 350 7.46311944427e-5 +388 350 -3.68195164986e-9 +389 350 -215854.122613 +390 350 485801.838229 +391 350 -1.28193754647e-7 +392 350 38151247.6502 +393 350 16914062.8611 +351 351 721879860.841 +352 351 -6232345.95189 +353 351 -5.13987182891e-5 +354 351 10257233.2606 +355 351 112058714.222 +357 351 79853210.6342 +388 351 -1.63598484061e-9 +389 351 -163316.290871 +390 351 215854.122613 +391 351 -9.77603943728e-8 +392 351 16914062.8611 +393 351 12881677.8419 +352 352 2296102.7147 +353 352 3.64200641064e-9 +354 352 -616934.901029 +355 352 -1.19209289551e-7 +356 352 -2.41015443923e-6 +357 352 9.04240549151e-8 +358 352 -187426.357349 +360 352 308467.450514 +361 352 10257233.2606 +363 352 6232345.95189 +394 352 -960625 +395 352 -3.64882301414e-9 +396 352 -7.25682823884e-9 +397 352 -1.85617816198e-23 +398 352 2.86395922953e-9 +399 352 1.01708775202e-9 +353 353 14131094.0411 +354 353 -3597.56871023 +355 353 6.42216510245e-6 +356 353 -2.88709998131e-8 +357 353 -5.14145940542e-5 +359 353 -7064186.05146 +394 353 -3.64882301414e-9 +395 353 -1360.96909059 +396 353 1798.78435511 +397 353 -2.05578659748e-9 +398 353 215854.122613 +399 353 163316.290871 +354 354 1304648.69128 +355 354 -4.73360197538e-7 +356 354 5.05521893501e-6 +357 354 -3.91155481339e-8 +358 354 308467.450514 +360 354 -648275.996988 +361 354 -21556628.1215 +363 354 -10257233.2606 +394 354 -7.25682823884e-9 +395 354 1798.78435511 +396 354 -4048.34865191 +397 354 3.47696066553e-9 +398 354 -485801.838229 +399 354 -215854.122613 +355 355 2372721167.7 +356 355 -.000166603426544 +357 355 1140186442.01 +358 355 -10257233.2606 +359 355 -6.42483978614e-6 +360 355 21556628.1215 +361 355 247268663.971 +363 355 112058714.222 +394 355 1.85617816198e-23 +395 355 2.05578659748e-9 +396 355 -3.47696066553e-9 +397 355 -16965 +398 355 -2.7283453457e-7 +399 355 -1.61857271696e-7 +356 356 157294028.508 +357 356 69781853.1322 +358 356 2.41485937428e-6 +360 356 -4.99737818829e-6 +361 356 -.000166174011822 +362 356 -205820.7294 +363 356 -8.02994800656e-5 +394 356 -2.86395922955e-9 +395 356 -215854.122613 +396 356 485801.838229 +397 356 -2.7283453457e-7 +398 356 38151247.6502 +399 356 16914062.8611 +357 357 721879860.841 +358 357 -6232345.95189 +359 357 5.13987182891e-5 +360 357 10257233.2606 +361 357 112058714.222 +363 357 79853210.6342 +394 357 -1.01708775201e-9 +395 357 -163316.290871 +396 357 215854.122613 +397 357 -1.61857271696e-7 +398 357 16914062.8611 +399 357 12881677.8419 +358 358 2296102.7147 +359 358 3.64200641064e-9 +360 358 -616934.901029 +361 358 -1.78813934326e-7 +362 358 2.42324626099e-6 +363 358 3.163740256e-8 +364 358 -187426.357349 +366 358 308467.450514 +367 358 10257233.2606 +369 358 6232345.95189 +400 358 -960625 +401 358 -3.65563961765e-9 +402 358 -1.08818340565e-8 +403 358 -3.86920857766e-23 +404 358 4.70493505445e-9 +405 358 1.83508017232e-9 +359 359 14131094.0411 +360 359 -3597.56871023 +361 359 -6.428752264e-6 +362 359 -2.88709998131e-8 +363 359 5.13819977641e-5 +365 359 -7064186.05146 +400 359 -3.65563961765e-9 +401 359 -1360.96909059 +402 359 1798.78435511 +403 359 -2.67468368606e-9 +404 359 215854.122613 +405 359 163316.290871 +360 360 1304648.69128 +361 360 -2.34123626016e-7 +362 360 -4.93787229061e-6 +363 360 8.00937414169e-8 +364 360 308467.450514 +366 360 -648275.996988 +367 360 -21556628.1215 +369 360 -10257233.2606 +400 360 -1.08818340565e-8 +401 360 1798.78435511 +402 360 -4048.34865191 +403 360 4.29495308583e-9 +404 360 -485801.838229 +405 360 -215854.122613 +361 361 2372721167.7 +362 361 .000165744597101 +363 361 1140186442.01 +364 361 -10257233.2606 +365 361 6.42483978614e-6 +366 361 21556628.1215 +367 361 247268663.971 +369 361 112058714.222 +400 361 3.86920857766e-23 +401 361 2.67468368606e-9 +402 361 -4.29495308583e-9 +403 361 -16965 +404 361 -3.36931411893e-7 +405 361 -2.10737468883e-7 +362 362 157294028.508 +363 362 69781853.1324 +364 362 -2.41485937428e-6 +366 362 4.99737818829e-6 +367 362 .000166174011822 +368 362 -205820.7294 +369 362 8.02994800656e-5 +400 362 -4.70493505449e-9 +401 362 -215854.122613 +402 362 485801.838229 +403 362 -3.36931411893e-7 +404 362 38151247.6502 +405 362 16914062.8611 +363 363 721879860.841 +364 363 -6232345.95189 +365 363 -5.13987182891e-5 +366 363 10257233.2606 +367 363 112058714.222 +369 363 79853210.6342 +400 363 -1.83508017231e-9 +401 363 -163316.290871 +402 363 215854.122613 +403 363 -2.10737468883e-7 +404 363 16914062.8611 +405 363 12881677.8419 +364 364 2341151.26585 +365 364 1.82441150707e-9 +366 364 -684029.893911 +367 364 1013788.37464 +368 364 2.94912504847e-6 +369 364 744469.140943 +370 364 -232474.908504 +372 364 375562.443396 +373 364 11271021.6352 +375 364 6976815.09283 +406 364 -960625 +407 364 -1.83122811058e-9 +408 364 -7.25341993709e-9 +409 364 -1.62555136758e-23 +410 364 3.27295543969e-9 +411 364 1.32653629631e-9 +365 365 14894026.1346 +366 365 -3597.56871023 +367 365 -7.12098817052e-6 +368 365 -2.88709998131e-8 +369 365 5.69336116314e-5 +371 365 -7827118.14501 +406 365 -1.83122811058e-9 +407 365 -1360.96909059 +408 365 1798.78435511 +409 365 -1.64679038732e-9 +410 365 215854.122613 +411 365 163316.290871 +366 366 1449937.00271 +367 366 -2259066.56878 +368 366 -6.0573220253e-6 +369 366 -1013788.37464 +370 366 375562.443396 +372 366 -793564.308415 +373 366 -23815694.6903 +375 366 -11271021.6352 +406 366 -7.25341993709e-9 +407 366 1798.78435511 +408 366 -4048.34865191 +409 366 2.55647275307e-9 +410 366 -485801.838229 +411 366 -215854.122613 +367 367 2421358974.52 +368 367 .000183252101567 +369 367 1162099482.64 +370 367 -11271021.6352 +371 367 7.11872248304e-6 +372 367 23815694.6903 +373 367 194486375.663 +375 367 84504028.1182 +406 367 1.62555136758e-23 +407 367 1.64679038732e-9 +408 367 -2.55647275307e-9 +409 367 -16965 +410 367 -2.00514144608e-7 +411 367 -1.29808833034e-7 +368 368 157316257.147 +369 368 69781853.1324 +370 368 -2.94401111721e-6 +372 368 6.11551232461e-6 +373 368 .00018353291958 +374 368 -228049.368176 +375 368 8.83528520485e-5 +406 368 -3.27295543971e-9 +407 368 -215854.122613 +408 368 485801.838229 +409 368 -2.00514144608e-7 +410 368 38151247.6502 +411 368 16914062.8611 +369 369 737779636.92 +370 369 -6976815.09283 +371 369 -5.69497798643e-5 +372 369 11271021.6352 +373 369 84504028.1182 +375 369 68237558.0284 +406 369 -1.32653629631e-9 +407 369 -163316.290871 +408 369 215854.122613 +409 369 -1.29808833034e-7 +410 369 16914062.8611 +411 369 12881677.8419 +370 370 1284659.80682 +371 370 3.20936424108e-12 +372 370 -375562.443397 +373 370 -11271021.6352 +374 370 -4238867.37393 +375 370 -6976815.09283 +377 370 -4238867.37393 +412 370 -497833.333333 +413 370 -6.41872848215e-12 +414 370 -3.75988717657e-9 +415 370 -5.83779954664e-24 +416 370 1.59031784371e-9 +417 370 7.70247417858e-10 +371 371 7962713.26403 +372 371 -1693.7919844 +373 371 10097137.073 +374 371 -4.65661287308e-9 +375 371 4.87547367811e-7 +376 371 10097137.073 +378 371 4.89775609117e-7 +412 371 -6.41872848215e-12 +413 371 -483.31235681 +414 371 846.8959922 +415 371 -4.3957002782e-10 +416 371 101627.519064 +417 371 57997.4828172 +372 372 8530394.78767 +373 372 23815694.6903 +374 372 -1.97440385819e-7 +375 372 11271021.6352 +377 372 -2.05612128966e-7 +412 372 -3.75988717657e-9 +413 372 846.8959922 +414 372 -1748.57296102 +415 372 7.70247417858e-10 +416 372 -209828.755322 +417 372 -101627.519064 +373 373 2299673376.14 +374 373 -6.19271184978e-8 +375 373 592006261.636 +376 373 449885280.472 +378 373 2.18729488781e-5 +412 373 5.83779954664e-24 +413 373 4.3957002782e-10 +414 373 -7.70247417858e-10 +415 373 -3335 +416 373 -6.10051432904e-8 +417 373 -3.4880612472e-8 +374 374 495425831.174 +375 374 32683001.6942 +377 374 208101719.712 +412 374 -1.59031784374e-9 +413 374 -101627.519064 +414 374 209828.755322 +415 374 -6.10051432904e-8 +416 374 16624203.5638 +417 374 8049103.72824 +375 375 370210542.925 +376 375 2.18729488781e-5 +378 375 -1.044e6 +412 375 -7.70247417858e-10 +413 375 -57997.4828172 +414 375 101627.519064 +415 375 -3.4880612472e-8 +416 375 8049103.72824 +417 375 4598861.67962 +376 376 1064685280.47 +378 376 5.15933924894e-5 +377 377 427728386.378 +378 378 1.044e6 +379 379 1064685280.47 +383 379 10097137.073 +385 379 449885280.472 +380 380 427728386.378 +382 380 -4238867.37393 +386 380 208101719.712 +381 381 1.044e6 +387 381 -1.044e6 +382 382 642010.94971 +383 382 3.77589547019e-9 +384 382 -139397.060211 +385 382 4635264.30435 +386 382 -4238867.37393 +387 382 2914870.77518 +388 382 -87659.3847246 +390 382 139397.060211 +391 382 4635264.30435 +393 382 2914870.77518 +383 383 4626538.74061 +384 383 -846.8959922 +385 383 10097137.073 +386 383 -101627.519064 +387 383 -57997.4828825 +389 383 -4491426.93395 +384 384 8031000.16844 +385 384 -9839944.65487 +386 384 209828.755327 +387 384 -4533636.78529 +388 384 139397.060211 +390 384 -295918.262146 +391 384 -9839944.65487 +393 384 -4635264.30435 +385 385 1572032934.94 +386 385 -.000147785297163 +387 385 241034053.726 +388 385 -4635264.30435 +389 385 -4.08492899983e-6 +390 385 9839944.65487 +391 385 147056055.205 +393 385 67231775.3905 +386 386 461552214.933 +387 386 16341500.8471 +388 386 2.10821994924e-6 +390 386 -4.43295835297e-6 +391 386 -.000147405788796 +392 386 -89130.8412912 +393 386 -7.01030327444e-5 +387 387 157604548.934 +388 387 -2914870.77518 +389 387 6.53588639972e-5 +390 387 4635264.30435 +391 387 67231775.3905 +393 387 46611942.19 +388 388 1135943.76945 +389 388 1.36332070051e-11 +390 388 -278794.120422 +391 388 5.36441802978e-7 +392 388 1.01056514999e-6 +393 388 3.12924385071e-7 +394 388 -87659.3847246 +396 388 139397.060211 +397 388 4635264.30435 +399 388 2914870.77518 +389 389 8984214.83699 +390 389 -1798.78435511 +391 389 1.23779417717e-9 +392 389 -215854.122613 +393 389 -163316.290839 +395 389 -4491426.93395 +390 390 595884.872943 +391 390 -1.13248825073e-6 +392 390 485801.838227 +393 390 215854.122613 +394 390 139397.060211 +396 390 -295918.262146 +397 390 -9839944.65487 +399 390 -4635264.30435 +391 391 1014705603.94 +392 391 7.13305776276e-5 +393 391 482068107.452 +394 391 -4635264.30435 +396 391 9839944.65487 +397 391 147056055.205 +399 391 67231775.3905 +392 392 78619455.2073 +393 392 34890926.5662 +394 392 -1.01424710164e-6 +396 392 2.15308873264e-6 +397 392 7.15950202347e-5 +398 392 -89130.8412912 +399 392 3.3725986609e-5 +393 393 320794261.443 +394 393 -2914870.77518 +395 393 -3.26794319986e-5 +396 393 4635264.30435 +397 393 67231775.3905 +399 393 46611942.19 +394 394 1135943.76945 +395 394 3.64882301414e-9 +396 394 -278794.120422 +397 394 -5.96046447754e-8 +398 394 -1.01711106087e-6 +399 394 -1.04308128357e-7 +400 394 -87659.3847246 +402 394 139397.060211 +403 394 4635264.30435 +405 394 2914870.77518 +395 395 8984214.83699 +396 395 -1798.78435511 +397 395 2.05578659748e-9 +398 395 -215854.122613 +399 395 -163316.290904 +401 395 -4491426.93395 +396 396 595884.872943 +397 396 2.98023223877e-7 +398 396 485801.838231 +399 396 215854.122614 +400 396 139397.060211 +402 396 -295918.262146 +403 396 -9839944.65487 +405 396 -4635264.30435 +397 397 1014705603.94 +398 397 -7.21566562599e-5 +399 397 482068107.452 +400 397 -4635264.30435 +402 397 9839944.65487 +403 397 147056055.205 +405 397 67231775.3905 +398 398 78619455.2073 +399 398 34890926.5661 +400 398 1.01424710164e-6 +402 398 -2.15308873264e-6 +403 398 -7.15950202347e-5 +404 398 -89130.8412912 +405 398 -3.3725986609e-5 +399 399 320794261.443 +400 399 -2914870.77518 +401 399 3.26794319986e-5 +402 399 4635264.30435 +403 399 67231775.3905 +405 399 46611942.19 +400 400 1135943.76945 +401 400 3.65563961765e-9 +402 400 -278794.120422 +403 400 5.96046447754e-8 +404 400 1.00954216658e-6 +405 400 -1.49011611938e-8 +406 400 -87659.3847246 +408 400 139397.060211 +409 400 4635264.30435 +411 400 2914870.77518 +401 401 8984214.83699 +402 401 -1798.78435511 +403 401 2.67468368606e-9 +404 401 -215854.122613 +405 401 -163316.290839 +407 401 -4491426.93395 +402 402 595884.872943 +404 402 485801.838227 +405 402 215854.122613 +406 402 139397.060211 +408 402 -295918.262146 +409 402 -9839944.65487 +411 402 -4635264.30435 +403 403 1014705603.94 +404 403 7.0901162906e-5 +405 403 482068107.452 +406 403 -4635264.30435 +408 403 9839944.65487 +409 403 147056055.205 +411 403 67231775.3905 +404 404 78619455.2073 +405 404 34890926.5662 +406 404 -1.01424710164e-6 +408 404 2.15308873264e-6 +409 404 7.15950202347e-5 +410 404 -89130.8412912 +411 404 3.3725986609e-5 +405 405 320794261.443 +406 405 -2914870.77518 +407 405 -3.26794319986e-5 +408 405 4635264.30435 +409 405 67231775.3905 +411 405 46611942.19 +406 406 1159106.56644 +407 406 1.83122811058e-9 +408 406 -312543.835606 +409 406 561051.477096 +410 406 1.35732783047e-6 +411 406 411018.779791 +412 406 -110822.181711 +414 406 173146.775395 +415 406 5196315.78145 +417 406 3325889.55497 +407 407 9469288.94586 +408 407 -1798.78435511 +409 407 -4.52445454142e-6 +410 407 -215854.122613 +411 407 -163316.290835 +413 407 -4976501.04282 +408 408 669469.664388 +409 408 -1249228.35622 +410 408 485801.838226 +411 408 -345197.354483 +412 408 173146.775395 +414 408 -369503.05359 +415 408 -11089173.0111 +417 408 -5196315.78145 +409 409 1039758660.39 +410 409 8.49973351609e-5 +411 409 493267386.08 +412 409 -5196315.78145 +413 409 4.52610133181e-6 +414 409 11089173.0111 +415 409 133197979.113 +417 409 59660407.959 +410 410 78629081.3382 +411 410 34890926.5662 +412 410 -1.36060078591e-6 +414 410 2.84596463108e-6 +415 410 8.5410374477e-5 +416 410 -98756.9721507 +417 410 4.08330523047e-5 +411 411 329115622.176 +412 411 -3325889.55497 +413 411 -3.62088106545e-5 +414 411 5196315.78145 +415 411 59660407.959 +417 411 44065471.0872 +412 412 665173.746697 +413 412 6.41872848215e-12 +414 412 -173146.775395 +415 412 -5196315.78145 +416 412 -4238867.37393 +417 412 -3325889.55497 +419 412 -4238867.37393 +413 413 5111612.84948 +414 413 -846.8959922 +415 413 10097137.073 +416 413 -101627.519064 +417 413 -57997.4828172 +418 413 10097137.073 +414 414 8104584.95988 +415 414 11089173.0111 +416 414 209828.755322 +417 414 5297943.30051 +415 415 1597085991.39 +416 415 -1.23854236996e-7 +417 415 252233332.355 +418 415 449885280.472 +416 416 461561841.064 +417 416 16341500.8471 +419 416 208101719.712 +417 417 165925909.667 +420 417 -1.044e6 +418 418 1064685280.47 +419 419 427728386.378 +420 420 1.044e6 diff --git a/doc/tutorial/examples/data/west0479.mtx b/doc/tutorial/examples/data/west0479.mtx new file mode 100644 index 00000000..4dc45500 --- /dev/null +++ b/doc/tutorial/examples/data/west0479.mtx @@ -0,0 +1,1924 @@ +%%MatrixMarket matrix coordinate real general +%------------------------------------------------------------------------------- +% UF Sparse Matrix Collection, Tim Davis +% http://www.cise.ufl.edu/research/sparse/matrices/HB/west0479 +% name: HB/west0479 +% [U 8 STAGE COLUMN SECTION, ALL SECTIONS RIGOROUS ( CHEM. ENG. )] +% id: 267 +% date: 1983 +% author: A. Westerberg +% ed: I. Duff, R. Grimes, J. Lewis +% fields: title A Zeros name id date author ed kind +% kind: chemical process simulation problem +%------------------------------------------------------------------------------- +479 479 1910 +25 1 1 +31 1 -.03764813 +87 1 -.3442396 +26 2 1 +31 2 -.02452262 +88 2 -.3737086 +27 3 1 +31 3 -.03661304 +89 3 -.8369379 +28 4 130 +29 4 -2.433767 +29 5 1 +30 5 -1.614091 +30 6 1.614091 +31 6 -.2187321 +87 6 -1 +88 6 -1 +89 6 -1 +32 7 -1.138352 +43 7 .03669428 +111 7 .09931636 +112 7 .09931636 +113 7 .09931636 +33 8 -.5 +43 8 .01611729 +111 8 .08724576 +34 9 -.3611918 +43 9 .01164286 +112 9 .1050415 +35 10 -.3218876 +43 10 .01037591 +113 10 .1404166 +36 11 -.4362416 +37 11 -.7680425 +38 11 -.1430279 +39 11 -.1593886 +37 12 1 +43 12 -.04856082 +111 12 -.2628684 +38 13 1 +43 13 -.03092595 +112 13 -.2790128 +39 14 1 +43 14 -.04612359 +113 14 -.6241882 +40 15 5.282436 +42 15 -.6123921 +41 16 .2886822 +42 16 -.2163815 +42 17 1.328774 +43 17 -.3694686 +111 17 -1 +112 17 -1 +113 17 -1 +2 18 48.17647 +8 18 -1 +11 18 -3.347484e-5 +3 19 83.5 +9 19 -1 +12 19 -4.136539e-5 +4 20 171.9412 +10 20 -1 +13 20 -8.484345e-5 +5 21 96.65138 +8 21 2.5 +11 21 3.347484e-5 +6 22 168.2706 +9 22 2.5 +12 22 4.136539e-5 +7 23 347.5872 +10 23 2.5 +13 23 8.484345e-5 +8 24 1 +17 24 -.1106967 +27 24 1.605232 +9 25 1 +17 25 -.08980852 +10 26 1 +17 26 -.1517369 +11 27 1.010455 +18 27 -.5 +12 28 1.005978 +18 28 -.3 +13 29 1.002885 +18 29 -.2 +14 30 1 +17 30 -.1811855 +15 31 1 +17 31 -.2400239 +16 32 1 +17 32 -.2265484 +17 33 1 +19 33 -1 +30 33 1.5 +42 33 .5 +18 34 1 +20 34 -316220 +23 34 -12323.69 +24 34 -1 +30 34 -35226.8 +41 34 18449.02 +19 35 5.298339 +21 35 .002452687 +20 36 1080.859 +21 36 -.2050215 +21 37 .144192 +22 37 63.05986 +30 37 -.1159299 +22 38 -18449.02 +24 38 .8453339 +40 38 -18449.02 +41 38 -15595.58 +23 39 1 +30 39 .2020493 +42 39 -.885471 +24 40 .0001000234 +30 40 2.448339 +42 40 .816113 +68 41 1 +74 41 -.0002278669 +99 41 -.2624395 +69 42 1 +74 42 -.0004188763 +100 42 -.3216196 +70 43 1 +74 43 -.001576933 +101 43 -.7264761 +71 44 300 +72 44 -2.851891 +72 45 1 +73 45 -.2870159 +73 46 .2870159 +74 46 -.004341322 +99 46 -1 +100 46 -1 +101 46 -1 +75 47 -1.138352 +86 47 .04200286 +123 47 .9414806 +124 47 .9414806 +125 47 .9414806 +76 48 -.3218876 +86 48 .011877 +123 48 1.331095 +77 49 -.3611918 +86 49 .01332724 +124 49 .9957529 +78 50 -.5 +86 50 .01844898 +125 50 .827056 +79 51 -1 +80 51 -19.24 +81 51 -3.803 +82 51 -9.481 +80 52 1 +86 52 -.0008875784 +123 52 -.09947392 +81 53 1 +86 53 -.001331368 +124 53 -.09947392 +82 54 1 +86 54 -.002218946 +125 54 -.09947392 +83 55 1.177613 +85 55 -3.108963 +84 56 .9919425 +85 56 -2.640056 +85 57 3.192112 +86 57 -.04461363 +123 57 -1 +124 57 -1 +125 57 -1 +45 58 109.8688 +51 58 -1 +54 58 -3.30811e-5 +46 59 191.3846 +52 59 -1 +55 59 -4.108467e-5 +47 60 395.4796 +53 60 -1 +56 60 -8.456383e-5 +48 61 221.7339 +51 61 2.5 +54 61 3.30811e-5 +49 62 387.0092 +52 62 2.5 +55 62 4.108467e-5 +50 63 800.8165 +53 63 2.5 +56 63 8.456383e-5 +51 64 1 +60 64 -.009170229 +52 65 1 +60 65 -.0464411 +53 66 1 +60 66 -.4899919 +54 67 1.00453 +61 67 -.2 +55 68 1.002591 +61 68 -.3 +56 69 1.00125 +61 69 -.5 +57 70 1 +60 70 -.03750053 +58 71 1 +60 71 -.124143 +59 72 1 +60 72 -.2927532 +60 73 1 +62 73 -1 +73 73 .1853733 +85 73 .06179109 +61 74 1 +63 74 -316220 +66 74 -16364.19 +67 74 -1 +73 74 -4696.782 +84 74 131.854 +62 75 22.12913 +64 75 .9537526 +63 76 2494.29 +64 76 -1.021587 +64 77 .8878281 +65 77 1.040042 +73 77 -2.640056 +65 78 -131.854 +67 78 .00805747 +83 78 -131.854 +84 78 -1.062409 +66 79 1 +73 79 .1016426 +85 79 -.06179109 +67 80 .007645257 +73 80 23.09895 +85 80 7.699649 +31 81 1 +244 81 1 +43 82 1 +1 83 1 +17 83 -3.850231 +18 83 -8.459935e-5 +31 83 2.01591 +35 83 1 +43 83 1.596171 +243 83 -.7897512 +74 84 1 +388 84 .8817562 +86 85 1 +44 86 1 +60 86 -2.79376 +61 86 -8.445823e-5 +74 86 2.015665 +78 86 1 +86 86 1.593994 +384 86 0 +387 86 -.961915 +385 87 .002424669 +386 87 .03036278 +387 87 .6730451 +388 87 1.45936 +96 88 .01689661 +97 88 .03484803 +98 88 -.06008018 +120 88 -.5552717 +121 88 -.4770398 +122 88 -.1858293 +141 88 -1 +142 88 -1 +143 88 -1 +185 88 -55.18857 +186 88 -95.67686 +187 88 -215.8377 +389 88 1 +438 88 1 +439 88 1 +440 88 1 +441 88 1 +442 88 1 +443 88 1 +450 88 -.001890756 +451 88 -.00153114 +452 88 -.001013726 +455 88 2.5 +456 88 26.0637 +458 88 1 +459 88 -1.5 +461 88 -9.603181 +462 88 -9.454185 +463 88 -9.437681 +464 88 -48.29572 +472 88 .9075922 +473 88 -4.375967 +474 88 -8.997992 +475 88 -8.881958 +476 88 1 +182 89 1 +183 89 1 +184 89 1 +391 89 1 +455 89 -1 +456 89 -26.0637 +458 89 -1 +468 89 1 +388 90 -1.45936 +467 90 1 +479 91 1 +203 92 -1 +204 92 -1 +205 92 -1 +209 92 1 +210 92 1 +211 92 1 +382 92 56.9531 +385 92 .7058325 +437 92 1 +453 92 -.6491372 +454 92 -3.294841e-5 +467 92 .5380748 +469 92 1 +479 92 .08247112 +203 93 -1 +204 93 -1 +205 93 -1 +209 93 1 +210 93 1 +211 93 1 +383 93 11.58384 +386 93 .7058325 +437 93 1 +453 93 -1.021542 +454 93 -4.099033e-5 +467 93 -.3630248 +470 93 1 +479 93 .06952261 +203 94 -1 +204 94 -1 +205 94 -1 +209 94 1 +210 94 1 +211 94 1 +384 94 .3209631 +387 94 .7058325 +437 94 1 +453 94 -2.049007 +454 94 -8.447003e-5 +467 94 .9135362 +471 94 1 +479 94 .8397638 +241 95 .5508352 +242 95 .4489223 +243 95 .0002425203 +244 95 .4052833 +108 96 -.06727662 +109 96 -.1570061 +110 96 -.0974757 +132 96 -.2506007 +133 96 -.2802617 +134 96 .1008491 +245 96 -1 +395 96 1 +396 96 1 +397 96 1 +398 96 1 +399 96 1 +400 96 1 +407 96 -.002863602 +408 96 -.002316259 +409 96 -.00153089 +412 96 2.5 +413 96 11.5878 +415 96 1 +416 96 -1.101224 +418 96 -1.364432 +419 96 -1.218821 +420 96 -1.210859 +421 96 -34.62105 +429 96 .6031359 +430 96 -.5996384 +431 96 -1.403392 +432 96 -1.370386 +433 96 1 +246 97 1 +412 97 -1 +413 97 -11.5878 +415 97 -1 +425 97 1 +244 98 -.4052833 +424 98 1 +436 99 1 +160 100 -.859384 +161 100 -.773339 +162 100 -.795174 +238 100 -1 +241 100 1 +394 100 1 +410 100 -1.623659 +411 100 -3.303278e-5 +424 100 3.572035 +426 100 1 +436 100 .9243532 +160 101 -1.171821 +161 101 -1.277352 +162 101 -1.250508 +239 101 -1 +242 101 1 +394 101 1 +410 101 -2.460289 +411 101 -4.105081e-5 +424 101 -2.178533 +427 101 1 +436 101 .6509842 +160 102 -2.328047 +161 102 -2.416155 +162 102 -2.512166 +240 102 -1 +243 102 1 +394 102 1 +410 102 -4.75362 +411 102 -8.45305e-5 +424 102 6.16714 +428 102 1 +436 102 3.396691 +241 103 .04373657 +242 103 .5739646 +243 103 .158349 +244 103 .1878125 +253 103 -.04434413 +254 103 -.5819378 +255 103 -.1605487 +256 103 -1 +135 104 -1 +136 104 -1 +137 104 -1 +151 104 -62.6809 +152 104 -123.89 +153 104 -464.3555 +245 104 1 +248 104 -.05164655 +249 104 -.3241762 +148 105 1 +149 105 1 +150 105 1 +247 105 1 +244 106 -.1878125 +248 106 1 +256 106 1 +249 107 1 +147 108 1 +169 108 -1 +170 108 -1 +171 108 -1 +175 108 1 +176 108 1 +177 108 1 +238 108 9.773875 +241 108 .7760502 +248 108 7.252831 +249 108 .78041 +253 108 -.7868306 +147 109 1 +169 109 -1 +170 109 -1 +171 109 -1 +175 109 1 +176 109 1 +177 109 1 +239 109 .6069821 +242 109 .7760502 +248 109 -2.389508 +249 109 .6849398 +254 109 -.7868306 +147 110 1 +169 110 -1 +170 110 -1 +171 110 -1 +175 110 1 +176 110 1 +177 110 1 +240 110 .001188564 +243 110 .7760502 +248 110 11.55883 +249 110 2.202644 +255 110 -.7868306 +363 111 -.1705148 +364 111 -.4342963 +365 111 -.2667421 +366 111 -2.609302 +385 111 .1956447 +386 111 .4983016 +387 111 .3060537 +388 111 .422396 +215 112 1 +216 112 1 +217 112 1 +218 112 1 +219 112 1 +220 112 1 +227 112 -.001890756 +228 112 -.00153114 +229 112 -.001013726 +232 112 2.5 +233 112 16.67241 +235 112 1 +236 112 -1.5 +389 112 -1 +392 112 -.4621213 +393 112 -.1234962 +232 113 -1 +233 113 -16.67241 +235 113 -1 +368 113 -1 +369 113 -1 +390 113 1 +366 114 2.609302 +388 114 -.422396 +392 114 1 +393 115 1 +181 116 1 +194 116 -.5869762 +195 116 -.5065155 +196 116 -.5139918 +230 116 -1.036685 +231 116 -3.294841e-5 +360 116 0 +363 116 -.8715531 +382 116 -1 +385 116 1 +392 116 2.63998 +393 116 .5574207 +181 117 1 +194 117 -.8001637 +195 117 -.836409 +196 117 -.8081026 +230 117 -1.637695 +231 117 -4.099033e-5 +361 117 0 +364 117 -.8715531 +383 117 -1 +386 117 1 +392 117 -1.773678 +393 117 .4075422 +181 118 1 +194 118 -1.589389 +195 118 -1.581811 +196 118 -1.623118 +230 118 -3.205686 +231 118 -8.447003e-5 +362 118 0 +365 118 -.8715531 +384 118 -1 +387 118 1 +392 118 4.467609 +393 118 2.247529 +93 119 -1 +96 119 -1 +248 119 -.4042156 +262 119 -.1818777 +284 119 -.1563455 +306 119 -.1545699 +328 119 -.1521778 +350 119 -.0919058 +372 119 -.007870537 +94 120 -1 +97 120 -1 +248 120 -1.809171 +262 120 -3.189655 +284 120 -3.349019 +306 120 -3.358644 +328 120 -3.308585 +350 120 -1.96787 +372 120 -.1133356 +95 121 -1 +98 121 -1 +248 121 -2.456602 +262 121 -4.101128 +284 121 -4.290892 +306 121 -4.302603 +328 121 -4.254147 +350 121 -2.952389 +372 121 -1.157365 +93 122 -.008121496 +96 122 -.01689661 +248 122 -.00453876 +262 122 -.00217052 +284 122 -.001874069 +306 122 -.001853318 +328 122 -.001824837 +350 122 -.001106561 +372 122 -.0001054494 +94 123 -.01674999 +97 123 -.03484803 +248 123 -.04189692 +262 123 -.07850668 +284 123 -.08279351 +306 123 -.08305532 +328 123 -.08182638 +350 123 -.04886605 +372 123 -.003131732 +95 124 -.02887803 +98 124 -.06008018 +248 124 -.09808224 +262 124 -.1740281 +284 124 -.1828855 +306 124 -.1834374 +328 124 -.1813914 +350 124 -.1263972 +372 124 -.05513677 +105 125 -1 +108 125 -1 +264 125 -.7636232 +286 125 -.5413211 +308 125 -.5290753 +330 125 -.528326 +352 125 -.5296534 +374 125 -.5990106 +392 125 -.5746768 +106 126 -1 +109 126 -1 +264 126 -1.467979 +286 126 -1.289806 +308 126 -1.279992 +330 126 -1.279385 +352 126 -1.280174 +374 126 -1.360143 +392 126 -.714919 +107 127 -1 +110 127 -1 +264 127 -.004370857 +286 127 -.003954101 +308 127 -.003931803 +330 127 -.003947668 +352 127 -.004749023 +374 127 -.07245539 +392 127 -1.602364 +105 128 -.1122933 +108 128 -.06727662 +264 128 -.05460137 +286 128 -.03887722 +308 128 -.03800866 +330 128 -.03795899 +352 128 -.03820886 +374 128 -.04808549 +392 128 -.05817862 +106 129 -.2620633 +109 129 -.1570061 +264 129 -.2449608 +286 129 -.2161806 +308 129 -.2145974 +330 129 -.2145191 +352 129 -.215523 +374 129 -.25481 +392 129 -.1689075 +107 130 -.1626995 +110 130 -.0974757 +264 130 -.0004528176 +286 130 -.0004114529 +308 130 -.0004092503 +330 130 -.0004109467 +352 130 -.0004963737 +374 130 -.008427185 +392 130 -.2350352 +117 131 -1 +120 131 -1 +249 131 -.06970284 +263 131 -.01924332 +285 131 -.01583793 +307 131 -.01561791 +329 131 -.01558321 +351 131 -.01471856 +373 131 -.005759952 +118 132 -1 +121 132 -1 +249 132 -.7417136 +263 132 -.8023494 +285 132 -.8065847 +307 132 -.8068286 +329 132 -.8055031 +351 132 -.7492698 +373 132 -.197197 +119 133 -1 +122 133 -1 +249 133 -.5127598 +263 133 -.5252253 +285 133 -.5261413 +307 133 -.5262245 +329 133 -.5273027 +351 133 -.5723186 +373 133 -1.025242 +117 134 -.4657593 +120 134 -.9690031 +249 134 -.04488488 +263 134 -.01317012 +285 134 -.01088739 +307 134 -.01073924 +329 134 -.01071655 +351 134 -.01016303 +373 134 -.004425719 +118 135 -.5744023 +121 135 -1.195033 +249 135 -.5890342 +263 135 -.6772174 +285 135 -.6838018 +307 135 -.6842053 +329 135 -.6831561 +351 135 -.638044 +373 135 -.1868616 +119 136 -.2292285 +122 136 -.4769055 +249 136 -.1625065 +263 136 -.1769142 +285 136 -.1780062 +307 136 -.1780856 +329 136 -.17847 +351 136 -.1944925 +373 136 -.3877026 +129 137 -1 +132 137 -1 +265 137 -.3427279 +287 137 -.2911376 +309 137 -.2876937 +331 137 -.2874889 +353 137 -.2882318 +375 137 -.3026987 +393 137 -.1750789 +130 138 -1 +133 138 -1 +265 138 -1.062334 +287 138 -1.118506 +309 138 -1.122253 +331 138 -1.122512 +353 138 -1.123285 +375 138 -1.108234 +393 138 -.3511864 +131 139 -1 +134 139 -1 +265 139 -.002399982 +287 139 -.00260173 +309 139 -.002615627 +331 139 -.002628033 +353 139 -.003161735 +375 139 -.04479381 +393 139 -.5972309 +129 140 -2.018253 +132 140 -1.209166 +265 140 -.4404489 +287 140 -.3758029 +309 140 -.3714643 +331 140 -.3712405 +353 140 -.3737109 +375 140 -.4367288 +393 140 -.3185628 +130 141 -2.562688 +133 141 -1.535345 +265 141 -1.733513 +287 141 -1.833245 +309 141 -1.839915 +331 141 -1.840541 +353 141 -1.849286 +375 141 -2.030266 +393 141 -.8113705 +131 142 -.9255429 +134 142 -.5545067 +265 142 -.001414409 +287 142 -.001540086 +309 142 -.001548758 +331 142 -.001556274 +353 142 -.001879925 +375 142 -.0296374 +393 142 -.4983387 +135 143 -1.433892 +141 143 -2.157704 +266 143 -1.523971 +288 143 -1.530708 +310 143 -1.531148 +332 143 -1.531316 +354 143 -1.537533 +376 143 -1.710928 +136 144 -.943206 +142 144 -1.419326 +267 144 -1.00246 +289 144 -1.006891 +311 144 -1.007181 +333 144 -1.007291 +355 144 -1.011381 +377 144 -1.125439 +137 145 -.5964527 +143 145 -.8975353 +268 145 -.6339227 +290 145 -.6367252 +312 145 -.6369083 +334 145 -.6369781 +356 145 -.6395642 +378 145 -.7116909 +135 146 -1 +141 146 -1 +266 146 -1 +288 146 -1 +310 146 -1 +332 146 -1 +354 146 -1 +376 146 -1 +136 147 -1 +142 147 -1 +267 147 -1 +289 147 -1 +311 147 -1 +333 147 -1 +355 147 -1 +377 147 -1 +137 148 -1 +143 148 -1 +268 148 -1 +290 148 -1 +312 148 -1 +334 148 -1 +356 148 -1 +378 148 -1 +138 149 -1 +148 149 1 +238 149 .5508352 +139 150 -1 +149 150 1.647495 +239 150 .7395973 +140 151 -1 +150 151 841.3516 +240 151 .2040448 +144 152 -1 +182 152 1 +382 152 .1956447 +145 153 -1 +183 153 1 +383 153 .4983016 +146 154 -1 +184 154 3.115622 +384 154 .9535478 +395 155 66.22492 +401 155 -1 +404 155 -3.328257e-5 +396 156 115.0623 +402 156 -1 +405 156 -4.122831e-5 +397 157 237.3387 +403 157 -1 +406 157 -8.47069e-5 +398 158 133.245 +401 158 2.5 +404 158 3.328257e-5 +399 159 232.2639 +402 159 2.5 +405 159 4.122831e-5 +400 160 480.1821 +403 160 2.5 +406 160 8.47069e-5 +160 161 -.473379 +401 161 1 +410 161 -.2116876 +161 162 -.5734317 +402 162 1 +410 162 -.3166715 +162 163 -.0006092511 +403 163 1 +410 163 -3.511874e-7 +163 164 -4575.004 +404 164 1.007562 +411 164 -.5508352 +164 165 -3681.416 +405 165 1.004324 +411 165 -.4489223 +165 166 -1787.818 +406 166 1.002087 +411 166 -.0002425203 +160 167 -.5260564 +161 167 -.4259823 +407 167 1 +410 167 -.4704884 +160 168 -.0005645987 +162 168 -.4380098 +408 168 1 +410 168 -.0005049593 +161 169 -.0005859667 +162 169 -.5613809 +409 169 1 +410 169 -.0006471876 +163 170 -1 +164 170 -1 +165 170 -1 +410 170 1 +412 170 -1 +423 170 .06144421 +435 170 .0204814 +157 171 -1 +163 171 4124.06 +164 171 4124.06 +165 171 4124.06 +411 171 1 +413 171 -316220 +416 171 -20034.24 +417 171 -1 +423 171 -2625.657 +434 171 222.129 +412 172 18.73727 +414 172 .9448816 +413 173 1494.367 +414 173 -1.02078 +158 174 -.9918601 +163 174 -17.92234 +164 174 -17.92234 +165 174 -17.92234 +414 174 .862829 +415 174 1.049719 +423 174 -.7341495 +157 175 .00813986 +415 175 -222.129 +417 175 .00813986 +433 175 -222.129 +434 175 -1.808099 +163 176 -1.091328 +164 176 -1.629397 +165 176 -1.444853 +416 176 1 +423 176 .04736406 +435 176 -.02789814 +417 177 .004538534 +423 177 7.57924 +435 177 2.526413 +148 178 -1 +178 178 -1 +149 179 -1 +179 179 -1 +150 180 -1 +180 180 -1 +148 181 1.026646 +166 181 -1 +149 182 1.073724 +167 182 -1 +150 183 1.208147 +168 183 -1 +148 184 -1 +154 184 -1 +149 185 -1 +155 185 -1 +150 186 -1 +156 186 -1 +215 187 99.14973 +221 187 -1 +224 187 -3.311398e-5 +216 188 172.6396 +222 188 -1 +225 188 -4.110812e-5 +217 189 356.6398 +223 189 -1 +226 189 -8.458718e-5 +218 190 200.0008 +221 190 2.5 +224 190 3.311398e-5 +219 191 349.0033 +222 191 2.5 +225 191 4.110812e-5 +220 192 722.0678 +223 192 2.5 +226 192 8.458718e-5 +194 193 -.1148388 +221 193 1 +230 193 -.01164592 +195 194 -.4167839 +222 194 1 +230 194 -.1700616 +196 195 -.4967614 +223 195 1 +230 195 -.2436893 +197 196 -5276.862 +224 196 1.005025 +231 196 -.1956447 +198 197 -4241.591 +225 197 1.002874 +231 197 -.4983016 +199 198 -2058.294 +226 198 1.001387 +231 198 -.3060537 +194 199 -.3987228 +195 199 -.09909709 +227 199 1 +230 199 -.08086976 +194 200 -.4864384 +196 200 -.1005598 +228 200 1 +230 200 -.09866041 +195 201 -.484119 +196 201 -.4026788 +229 201 1 +230 201 -.395073 +197 202 -1 +198 202 -1 +199 202 -1 +230 202 1 +232 202 -1 +191 203 -1 +197 203 3297.623 +198 203 3297.623 +199 203 3297.623 +231 203 1 +233 203 -316220 +236 203 -18966.66 +237 203 -1 +232 204 22.66987 +234 204 .9548602 +233 205 2248.706 +234 205 -1.020655 +192 206 -.9922951 +197 206 -21.89857 +198 206 -21.89857 +199 206 -21.89857 +234 206 .8900096 +235 206 1.039205 +191 207 .007704897 +235 207 -146.1362 +237 207 .007704897 +197 208 -.6589052 +198 208 -1.106497 +199 208 -1.000909 +236 208 1 +237 209 .006895657 +182 210 -1 +212 210 -1 +183 211 -1 +213 211 -1 +184 212 -1 +214 212 -1 +182 213 1 +200 213 -1 +183 214 1.022676 +201 214 -1 +184 215 1.091418 +202 215 -1 +182 216 -1 +188 216 -1 +183 217 -1 +189 217 -1 +184 218 -1 +190 218 -1 +241 219 -.1996961 +242 219 -.7859616 +243 219 -.0006412823 +244 219 .4069041 +253 219 .2024702 +254 219 .7968796 +255 219 .0006501906 +256 219 -2.166544 +257 220 -1 +264 220 -.300015 +265 220 -.4074615 +246 221 -1 +247 221 -1 +258 221 1 +244 222 .4069041 +256 222 -2.166544 +264 222 1 +265 223 1 +238 224 0 +241 224 -.9862989 +250 224 -1 +253 224 1 +261 224 1 +264 224 3.501858 +265 224 1.241884 +239 225 0 +242 225 -.9862989 +251 225 -1 +254 225 1 +261 225 1 +264 225 -2.149559 +265 225 .9360239 +240 226 0 +243 226 -.9862989 +252 226 -1 +255 226 1 +261 226 1 +264 226 6.025986 +265 226 4.086838 +253 227 .0119553 +254 227 .6147503 +255 227 .1605955 +256 227 .599181 +275 227 -.01194968 +276 227 -.6144612 +277 227 -.16052 +278 227 -1 +257 228 1 +262 228 -.09335086 +263 228 -.3468181 +266 228 -1 +267 228 -1 +268 228 -1 +259 229 1 +256 230 -.599181 +262 230 1 +278 230 1 +263 231 1 +250 232 13.33342 +253 232 .7873011 +260 232 1 +262 232 12.12026 +263 232 .7702509 +275 232 -.7869309 +251 233 1.020551 +254 233 .7873011 +260 233 1 +262 233 -3.984399 +263 233 .681342 +276 233 -.7869309 +252 234 .003187485 +255 234 .7873011 +260 234 1 +262 234 19.25216 +263 234 2.236907 +277 234 -.7869309 +253 235 -.1700813 +254 235 -.8296922 +255 235 -.0006970141 +256 235 2.567363 +275 235 .1700014 +276 235 .829302 +277 235 .0006966863 +278 235 -4.284787 +279 236 -1 +286 236 -.2554693 +287 236 -.4122457 +258 237 -1 +259 237 -1 +280 237 1 +256 238 2.567363 +278 238 -4.284787 +286 238 1 +287 239 1 +250 240 0 +253 240 -1.000471 +272 240 -1 +275 240 1 +283 240 1 +286 240 2.955529 +287 240 1.254414 +251 241 0 +254 241 -1.000471 +273 241 -1 +276 241 1 +283 241 1 +286 241 -1.815969 +287 241 .9452119 +252 242 0 +255 242 -1.000471 +274 242 -1 +277 242 1 +283 242 1 +286 242 5.084997 +287 242 4.136479 +250 243 .2024702 +269 243 -1 +251 244 .7968796 +270 244 -1 +252 245 .2039823 +271 245 -1 +275 246 .009818081 +276 246 .6166417 +278 246 .9557944 +297 246 -.009817569 +298 246 -.6166096 +299 246 -.1605148 +300 246 -1 +279 247 1 +284 247 -.0982179 +285 247 -.3485639 +288 247 -1 +289 247 -1 +290 247 -1 +281 248 1 +278 249 -.9557944 +284 249 1 +300 249 1 +285 250 1 +272 251 13.62671 +275 251 .786983 +282 251 1 +284 251 12.68233 +285 251 .7694287 +297 251 -.786942 +273 252 1.058389 +276 252 .786983 +282 252 1 +284 252 -4.168489 +285 252 .6810285 +298 252 -.786942 +274 253 .003415583 +277 253 .786983 +282 253 1 +284 253 20.13996 +285 253 2.239415 +299 253 -.786942 +275 254 -.1678698 +276 254 -.8314825 +277 254 -.0006999047 +278 254 4.328993 +297 254 .167861 +298 254 .8314391 +299 254 .0006998682 +300 254 -4.529209 +301 255 -1 +308 255 -.2530153 +309 255 -.4125625 +280 256 -1 +281 256 -1 +302 256 1 +278 257 4.328993 +300 257 -4.529209 +308 257 1 +309 258 1 +272 259 0 +275 259 -1.000052 +294 259 -1 +297 259 1 +305 259 1 +308 259 2.925436 +309 259 1.255249 +273 260 0 +276 260 -1.000052 +295 260 -1 +298 260 1 +305 260 1 +308 260 -1.797593 +309 260 .9458243 +274 261 0 +277 261 -1.000052 +296 261 -1 +299 261 1 +305 261 1 +308 261 5.033166 +309 261 4.139783 +272 262 .1700014 +291 262 -1 +273 263 .829302 +292 263 -1 +274 264 .2039729 +293 264 -1 +297 265 .00967985 +298 265 .6167109 +299 265 .1605181 +300 265 .9972984 +319 265 -.00968017 +320 265 -.6167313 +321 265 -.1605234 +322 265 -1 +301 266 1 +306 266 -.09852872 +307 266 -.348671 +310 266 -1 +311 266 -1 +312 266 -1 +303 267 1 +300 268 -.9972984 +306 268 1 +322 268 1 +307 269 1 +294 270 13.64601 +297 270 .7869089 +304 270 1 +306 270 12.71619 +307 270 .7693586 +319 270 -.7869349 +295 271 1.060897 +298 271 .7869089 +304 271 1 +306 271 -4.179575 +307 271 .6809936 +320 271 -.7869349 +296 272 .003430968 +299 272 .7869089 +304 272 1 +306 272 20.19341 +307 272 2.239532 +321 272 -.7869349 +297 273 -.1677233 +298 273 -.8315405 +299 273 -.0007031113 +300 273 4.531911 +319 273 .1677288 +320 273 .831568 +321 273 .0007031346 +322 273 -4.544188 +323 274 -1 +330 274 -.2528891 +331 274 -.412629 +302 275 -1 +303 275 -1 +324 275 1 +300 276 4.531911 +322 276 -4.544188 +330 276 1 +331 277 1 +294 278 0 +297 278 -.9999669 +316 278 -1 +319 278 1 +327 278 1 +330 278 2.92357 +331 278 1.255294 +295 279 0 +298 279 -.9999669 +317 279 -1 +320 279 1 +327 279 1 +330 279 -1.79649 +331 279 .9458516 +296 280 0 +299 280 -.9999669 +318 280 -1 +321 280 1 +327 280 1 +330 280 5.029935 +331 280 4.14014 +294 281 .167861 +313 281 -1 +295 282 .8314391 +314 282 -1 +296 283 .2039856 +315 283 -1 +319 284 .00964735 +320 284 .6149968 +321 284 .1606638 +322 284 1.012275 +341 284 -.009663071 +342 284 -.615999 +343 284 -.1609257 +344 284 -1 +323 285 1 +328 285 -.09774015 +329 285 -.348389 +332 285 -1 +333 285 -1 +334 285 -1 +325 286 1 +322 287 -1.012275 +328 287 1 +344 287 1 +329 288 1 +316 289 13.65337 +319 289 .785308 +326 289 1 +328 289 12.53604 +329 289 .7686138 +341 289 -.7865877 +317 290 1.061854 +320 290 .785308 +326 290 1 +328 290 -4.120345 +329 290 .6803446 +342 290 -.7865877 +318 291 .003436848 +321 291 .785308 +326 291 1 +328 291 19.9072 +329 291 2.237486 +343 291 -.7865877 +319 292 -.167696 +320 292 -.8298335 +321 292 -.0008435821 +322 292 4.531913 +341 292 .1679693 +342 292 .8311858 +343 292 .0008449567 +344 292 -4.476957 +345 293 -1 +352 293 -.2542282 +353 293 -.4146785 +324 294 -1 +325 294 -1 +346 294 1 +322 295 4.531912 +344 295 -4.476957 +352 295 1 +353 296 1 +316 297 0 +319 297 -.9983731 +338 297 -1 +341 297 1 +349 297 1 +352 297 2.9258 +353 297 1.254871 +317 298 0 +320 298 -.9983731 +339 298 -1 +342 298 1 +349 298 1 +352 298 -1.799474 +353 298 .9452959 +318 299 0 +321 299 -.9983731 +340 299 -1 +343 299 1 +349 299 1 +352 299 5.032978 +353 299 4.146532 +316 300 .1677288 +335 300 -1 +317 301 .831568 +336 301 -1 +318 302 .204587 +337 302 -1 +341 303 .008958003 +342 303 .5623915 +343 303 .1714316 +344 303 1.534987 +363 303 -.009368401 +364 303 -.5881567 +365 303 -.1792855 +366 303 -1 +345 304 1 +350 304 -.07642457 +351 304 -.336307 +354 304 -1 +355 304 -1 +356 304 -1 +347 305 1 +344 306 -1.534987 +350 306 1 +366 306 1 +351 307 1 +338 308 13.9277 +341 308 .7427812 +348 308 1 +350 308 7.712414 +351 308 .7375405 +363 308 -.7768106 +339 309 1.097792 +342 309 .7427812 +348 309 1 +350 309 -2.534534 +351 309 .653208 +364 309 -.7768106 +340 310 .00366104 +343 310 .7427812 +348 310 1 +350 310 12.24449 +351 310 2.151386 +365 310 -.7768106 +341 311 -.1672642 +342 311 -.7775783 +343 311 -.01135093 +344 311 3.941971 +363 311 .1749272 +364 311 .8132019 +365 311 .01187095 +366 311 -2.568082 +367 312 -1 +374 312 -.3113227 +375 312 -.4557268 +346 313 -1 +347 313 -1 +368 313 1 +344 314 3.941971 +366 314 -2.568082 +374 314 1 +375 315 1 +338 316 0 +341 316 -.9561935 +360 316 -1 +363 316 1 +371 316 1 +374 316 3.149454 +375 316 1.212998 +339 317 0 +342 317 -.9561935 +361 317 -1 +364 317 1 +371 317 1 +374 317 -1.985919 +375 317 .9070684 +340 318 0 +343 318 -.9561935 +362 318 -1 +365 318 1 +371 318 1 +374 318 5.393687 +375 318 4.227463 +338 319 .1679693 +357 319 -1 +339 320 .8311858 +358 320 -1 +340 321 .2307969 +359 321 -1 +363 322 .004956003 +364 322 .2092511 +365 322 .4341566 +366 322 6.177384 +385 322 -.005686404 +386 322 -.2400899 +387 322 -.4981413 +388 322 -1 +367 323 1 +372 323 -.05189959 +373 323 -.2281993 +376 323 -1 +377 323 -1 +378 323 -1 +369 324 1 +366 325 -6.177384 +372 325 1 +388 325 1 +373 326 1 +360 327 22.88466 +363 327 .6483637 +370 327 1 +372 327 1.04345 +373 327 .4217586 +385 327 -.7439176 +361 328 2.519703 +364 328 .6483637 +370 328 1 +372 328 -.3414664 +373 328 .3798898 +386 328 -.7439176 +362 329 .01772792 +365 329 .6483637 +370 329 1 +372 329 1.646052 +373 329 1.305476 +387 329 -.7439176 +360 330 .1749272 +379 330 -1 +361 331 .8132019 +380 331 -1 +362 332 .669619 +381 332 -1 +87 333 6.318058 +93 333 1.008121 +88 334 2.101652 +94 334 .98325 +89 335 10.21634 +95 335 .971122 +90 336 4.771533 +96 336 1.016897 +91 337 1.544553 +97 337 .965152 +92 338 7.403258 +98 338 .9399198 +99 339 276.7648 +105 339 .8877067 +100 340 192.1904 +106 340 1.262063 +101 341 465.2974 +107 341 .8373005 +102 342 402.6353 +108 342 .9327234 +103 343 243.9516 +109 343 1.157006 +104 344 694.4257 +110 344 .9025243 +111 345 2.147169 +117 345 .7331041 +112 346 1.830354 +118 346 .7707069 +113 347 5.419496 +119 347 .9106797 +114 348 1.59346 +120 348 .4447283 +115 349 1.519359 +121 349 .5229601 +116 350 5.927271 +122 350 .8141707 +123 351 8.601323 +129 351 .5817151 +124 352 6.197485 +130 352 .5322072 +125 353 37.67033 +131 353 1.16833 +126 354 10.95535 +132 354 .7493993 +127 355 8.286427 +133 355 .7197383 +128 356 35.09293 +134 356 1.100849 +135 357 .4338919 +138 357 2.279713 +136 358 .1137572 +139 358 .6069821 +137 359 .4035473 +140 359 .008004989 +141 360 1.157704 +144 360 4.042228 +142 361 .4193259 +145 361 2.449611 +143 362 .1024647 +146 362 .3647518 +151 363 172.5745 +154 363 14.91761 +152 364 161.5845 +155 364 12.0938 +153 365 145.3145 +156 365 5.740096 +157 366 .004501889 +158 366 .9526359 +159 366 -1 +158 367 .9448816 +163 367 19.88206 +164 367 15.9987 +165 367 7.769499 +159 368 1.00814 +163 368 98.82898 +164 368 147.5558 +165 368 130.8437 +160 369 1 +163 369 1.801198 +161 370 1 +164 370 2.196221 +162 371 1 +165 371 2.060738 +163 372 19.88206 +166 372 .9740453 +164 373 15.9987 +167 373 .9313378 +165 374 7.769499 +168 374 .827714 +169 375 -1 +172 375 -1 +175 375 .05635791 +176 375 .05635791 +177 375 .05635791 +170 376 -1 +173 376 -1 +175 376 .7395973 +176 376 .7395973 +177 376 .7395973 +171 377 -1 +174 377 -1 +175 377 .2040448 +176 377 .2040448 +177 377 .2040448 +172 378 1 +175 378 -1 +173 379 1 +176 379 -1 +174 380 1 +177 380 -1 +175 381 1 +178 381 1 +176 382 1 +179 382 1 +177 383 1 +180 383 1 +102 384 -19.45389 +418 384 1 +424 384 -.09530421 +103 385 -22.09031 +419 385 1 +424 385 -.08819762 +104 386 -49.56359 +420 386 1 +424 386 -.0001069042 +421 387 179.7345 +422 387 -2.59574 +422 388 1 +423 388 -.09621652 +102 389 -1 +103 389 -1 +104 389 -1 +423 389 .09621652 +424 389 -.008893728 +126 390 .9308279 +127 390 .9308279 +128 390 .9308279 +425 390 -1.138352 +436 390 .09534178 +126 391 .8176979 +426 391 -.5508352 +436 391 .04613478 +127 392 .8176979 +427 392 -.4489223 +436 392 .03759915 +128 393 6.806865 +428 393 -.002018842 +436 393 .0001690866 +429 394 -.6031359 +430 394 -1.191426 +431 394 -.2090101 +432 394 -.2323664 +126 395 -1.588199 +430 395 1 +436 395 -.08960673 +127 396 -1.789478 +431 396 1 +436 396 -.08228325 +128 397 -4.012804 +432 397 1 +436 397 -9.968042e-5 +433 398 1.186874 +435 398 -.8713432 +434 399 .9918601 +435 399 -.7341495 +126 400 -1 +127 400 -1 +128 400 -1 +435 400 .8978249 +436 400 -.1024269 +185 401 263.3025 +188 401 16.71023 +186 402 252.3125 +189 402 15.09138 +187 403 236.0425 +190 403 11.44029 +191 404 .006842933 +192 404 .9622744 +193 404 -1 +192 405 .9548602 +197 405 35.04212 +198 405 28.16719 +199 405 13.66854 +193 406 1.007705 +197 406 85.84676 +198 406 144.1621 +199 406 130.4054 +194 407 1 +197 407 1.658905 +195 408 1 +198 408 2.106497 +196 409 1 +199 409 2.000909 +197 410 35.04212 +200 410 1 +198 411 28.16719 +201 411 .9778269 +199 412 13.66854 +202 412 .9162394 +203 413 -1 +206 413 -1 +209 413 .00343519 +210 413 .00343519 +211 413 .00343519 +204 414 -1 +207 414 -1 +209 414 .04301697 +210 414 .04301697 +211 414 .04301697 +205 415 -1 +208 415 -1 +209 415 .9535478 +210 415 .9535478 +211 415 .9535478 +206 416 1 +209 416 -1 +207 417 1 +210 417 -1 +208 418 1 +211 418 -1 +209 419 1 +212 419 1 +210 420 1 +213 420 1 +211 421 1 +214 421 1 +90 422 -.04761313 +461 422 1 +467 422 -2.33347e-5 +91 423 -.05746435 +462 423 1 +467 423 -.0003526656 +92 424 -.1295604 +463 424 1 +467 424 -.01762542 +464 425 270.4625 +465 425 -2.800067 +465 426 1 +466 426 -1.685693 +90 427 -1 +91 427 -1 +92 427 -1 +466 427 1.685693 +467 427 -.1426674 +114 428 .1214974 +115 428 .1214974 +116 428 .1214974 +468 428 -1.138352 +479 428 .02123052 +114 429 .6055575 +469 429 -.01949018 +479 429 .0003634963 +115 430 .3357927 +470 430 -.1353383 +479 430 .00252409 +116 431 .1067309 +471 431 -.9535478 +479 431 .01778388 +472 432 -.9075922 +473 432 -5.711822 +474 432 -.9356841 +475 432 -1.034655 +114 433 -.04324093 +473 433 1 +479 433 -2.595611e-5 +115 434 -.05217491 +474 434 1 +479 434 -.000392189 +116 435 -.1176314 +475 435 1 +479 435 -.01960016 +476 436 5.314078 +478 436 -1.002184 +477 437 .3448372 +478 437 -.2647862 +114 438 -1 +115 438 -1 +116 438 -1 +478 438 1.766971 +479 438 -.1747406 +438 439 99.14973 +444 439 -1 +447 439 -3.311398e-5 +439 440 172.6396 +445 440 -1 +448 440 -4.110812e-5 +440 441 356.6398 +446 441 -1 +449 441 -8.458718e-5 +441 442 200.0008 +444 442 2.5 +447 442 3.311398e-5 +442 443 349.0033 +445 443 2.5 +448 443 4.110812e-5 +443 444 722.0678 +446 444 2.5 +449 444 8.458718e-5 +444 445 1 +453 445 -1.448565e-6 +445 446 1 +453 446 -.0005113292 +446 447 1 +453 447 -.9543887 +447 448 1.005025 +454 448 -.00343519 +448 449 1.002874 +454 449 -.04301697 +449 450 1.001387 +454 450 -.9535478 +450 451 1 +453 451 -4.94556e-5 +451 452 1 +453 452 -.002177557 +452 453 1 +453 453 -.04287153 +453 454 1 +455 454 -1 +466 454 1.5 +478 454 .5 +454 455 1 +456 455 -316220 +459 455 -12132.58 +460 455 -1 +466 455 -20451.81 +477 455 9152.751 +455 456 9.146357 +457 456 .003773492 +456 457 2248.706 +457 457 -.1250533 +457 458 .06758838 +458 458 65.08712 +466 458 -.1885904 +458 459 -9152.751 +460 459 .7543943 +476 459 -9152.751 +477 459 -6904.783 +459 460 1 +466 460 .1856929 +478 460 -.5 +460 461 .0001916794 +466 461 2.668452 +478 461 .889484 +266 462 .523971 +269 462 2.590273 +267 463 .1209036 +270 463 1 +268 464 .3660773 +271 464 .01832333 +288 465 .5307083 +291 465 2.612032 +289 466 .1214381 +292 466 1 +290 467 .3632748 +293 467 .01939848 +310 468 .5311485 +313 468 2.613447 +311 469 .1214731 +314 469 1 +312 470 .3630917 +315 470 .01947045 +332 471 .5313162 +335 471 2.613986 +333 472 .1214864 +336 472 1 +334 473 .3630219 +337 473 .01949793 +354 474 .5375334 +357 474 2.63388 +355 475 .1219796 +358 475 1 +356 476 .3604358 +359 476 .02053846 +376 477 .7109283 +379 477 3.130467 +377 478 .1357358 +380 478 1 +378 479 .2883091 +381 479 .07148988 diff --git a/doc/tutorial/examples/nesdis_example.py b/doc/tutorial/examples/nesdis_example.py new file mode 100644 index 00000000..c9ba19ca --- /dev/null +++ b/doc/tutorial/examples/nesdis_example.py @@ -0,0 +1,87 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: nesdis_example.py +# Created: 2025-08-22 11:04 +# ============================================================================= + +"""An example of using CHOLMOD to compute the Cholesky factorization of a +sparse matrix with various orderings. +""" + +from pathlib import Path + +import matplotlib.pyplot as plt +from scipy.io import mmread +from scipy.sparse.linalg import splu + +from sksparse.amd import amd +from sksparse.cholmod import nesdis + +# Load the west0479 matrix (downloaded from the SuiteSparse Matrix Collection: +# ) +filepath = Path("data") / "west0479.mtx" +A = mmread(filepath, spmatrix=False).tocsc() # read the matrix + +# Compute the AMD permutation +p = amd(A) +PAPT = A[p][:, p] + +# Compute the nested dissection A.T @ A permutation +q = nesdis(A) +QAQT = A[q][:, q] + +# Compute the LU decompositions +lu = splu(A) +lup = splu(PAPT) +luq = splu(QAQT) + +LU = lu.L + lu.U +LUp = lup.L + lup.U +LUq = luq.L + luq.U + +# Plot the original and permuted matrices +plt.rcParams.update({"font.size": 10}) + +fig, axs = plt.subplots(num=1, nrows=3, ncols=2, clear=True) +fig.set_size_inches((6, 10), forward=True) +fig.set_constrained_layout(True) +fig.suptitle("CHOLMOD Example: Cholesky Factor of west0479") + +ax = axs[0, 0] +ax.spy(A, markersize=1) +ax.set_title(r"Original Matrix $A$") + +ax = axs[1, 0] +ax.spy(PAPT, markersize=1) +ax.set_title(r"AMD-Ordered Matrix $PAP^{\top}$") + +ax = axs[0, 1] +ax.spy(LU, markersize=1) +ax.set_title(r"LU Factors $LU = A$") +ax.set_xlabel(f"{LU.nnz:,} non-zeros") + +ax = axs[1, 1] +ax.spy(LUp, markersize=1) +ax.set_title(r"AMD Factors $LU = PAP^{\top}$") +ax.set_xlabel(f"{LUp.nnz:,} non-zeros") + +ax = axs[2, 0] +ax.spy(QAQT, markersize=1) +ax.set_title(r"Nested Dissection Matrix $QAQ^{\top}$") + +ax = axs[2, 1] +ax.spy(LUq, markersize=1) +ax.set_title(r"Nesdis Factors $LU = QAQ^{\top}$") +ax.set_xlabel(f"{LUq.nnz:,} non-zeros") + +for ax in axs.flat: + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + +plt.show() +fig.savefig("nesdis_example.svg") diff --git a/doc/tutorial/examples/nesdis_example.svg b/doc/tutorial/examples/nesdis_example.svg new file mode 100644 index 00000000..da27f323 --- /dev/null +++ b/doc/tutorial/examples/nesdis_example.svg @@ -0,0 +1,24815 @@ + + + + + + + + 2025-09-10T11:29:57.755490 + image/svg+xml + + + Matplotlib v3.10.5, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst new file mode 100644 index 00000000..eec81fae --- /dev/null +++ b/doc/tutorial/index.rst @@ -0,0 +1,52 @@ +.. Part of the scikit-sparse project. +.. Copyright (C) 2008-2025 The scikit-sparse developers. All rights reserved. +.. See pyproject.toml for full author list and LICENSE.txt for license details. +.. SPDX-License-Identifier: BSD-2-Clause + +.. _user_guide: + +************************ +Scikit-Sparse User Guide +************************ + +.. currentmodule:: sksparse + +.. sectionauthor:: Bernard T. Roesler + +Scikit-sparse is a collection of sparse matrix algorithms and convenience +functions built to work with SciPySparse_ arrays. It is largely an interface to +the parts of the SuiteSparse_ library by Timothy A. Davis that have a GPL +license and are not suitable for inclusion in SciPy proper. + +.. _SciPySparse: https://docs.scipy.org/doc/scipy/tutorial/sparse.html +.. _SuiteSparse: http://faculty.cse.tamu.edu/davis/suitesparse.html + + +Subpackages and User Guides +--------------------------- + +Scikit-sparse is organized into submodules corresponding to the submodules of +SuiteSparse. These are summarized in the following table: + +================== ======================================== +Subpackage Description and User Guide +================== ======================================== +``amd`` :doc:`./amd` +``btf`` :doc:`./btf` +``camd`` :doc:`./camd` +``ccolamd`` :doc:`./ccolamd` +``cholmod`` :doc:`./cholmod` +``colamd`` :doc:`./colamd` +================== ======================================== + +.. toctree:: + :caption: User Guide + :maxdepth: 1 + :hidden: + + amd + btf + camd + ccolamd + cholmod + colamd diff --git a/pyproject.toml b/pyproject.toml index e0a415bf..03d70dd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,37 +1,126 @@ -[tool.black] -line-length = 120 -target_version = ['py38'] -include = '\.pyi?$' -exclude = ''' - -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | _build - | buck-out - | build - | dist - | doc - | venv - )/ -) -''' [build-system] -requires = [ - "setuptools>=40.8.0", - "wheel", - "Cython>=0.22", - 'numpy>=1.13.3; python_version=="3.6"', - 'numpy>=1.14.5; python_version=="3.7"', - 'numpy>=1.17.3; python_version=="3.8"', - 'numpy>=2.0; python_version=="3.9"', - 'numpy>=2.0; python_version=="3.10"', - 'numpy>=2.0; python_version>="3.11"', - 'numpy>=2.0; python_version>="3.12"', - 'numpy>=2.0; python_version>="3.13"', -] +# setuptools>=61 for PEP 621 support +requires = ["setuptools>=61.0", "wheel", "Cython>=3.0", "numpy>=2.0"] build-backend = "setuptools.build_meta" + +[project] +name = "scikit-sparse-dev" +version = "0.5.0.dev3" +description = "Scikit-sparse is a Python wrapper for the SuiteSparse sparse matrix library." +readme = "README.rst" +requires-python = ">=3.10" +license = "BSD-2-Clause" +license-files = ["LICENSE.txt"] + +authors = [ + { name = "David Cournapeau", email = "cournape@gmail.com" }, + { name = "Nathaniel Smith", email = "njs@pobox.com" }, + { name = "Dag Sverre Seljebotn", email = "dagss@student.matnat.uio.no" }, + { name = "Leon Barrett", email = "lbarrett@climate.com" }, + { name = "Yuri", email = "yuri@tsoft.com" }, + { name = "Antony Lee", email = "anntzer.lee@gmail.com" }, + { name = "Alex Grigorievskiy", email = "alex.grigorievskiy@gmail.com" }, + { name = "Joscha Reimer", email = "jor@informatik.uni-kiel.de" }, + { name = "Justin Ellis", email = "justin.ellis18@gmail.com" }, + { name = "Aaron Johnson", email = "aaron9035@gmail.com" }, + { name = "Bernard Roesler", email = "bernard.roesler@gmail.com" }, +] +maintainers = [ + { name = "Bernard Roesler", email = "bernard.roesler@gmail.com" }, +] + +keywords = ["sparse", "matrix", "linear algebra", "scipy", "numpy"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", + "Programming Language :: Cython", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "numpy>2.0", + "scipy>=1.14", +] + +[project.optional-dependencies] +dev = [ + "build", + "pre-commit", + "pytest", + "ruff", + "sphinx", + "furo", + "sphinx-copybutton", +] + +[project.urls] +"Homepage" = "https://github.com/broesler/scikit-sparse" +"Bug Tracker" = "https://github.com/broesler/scikit-sparse/issues" + +# This data is excluded from the wheel package, but not necessarily from the +# sdist (for sdist, see: MANIFEST.in) +[tool.setuptools.exclude-package-data] +sksparse = ["*.c", "*.pyx"] + +# [tool.setuptools.packages.find] +# exclude = ["cython_debug"] + +[tool.cython] +language_level = "3" +# gdb_debug = true + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] +extend-select = [ + 'B', # flake8-bugbear + 'C4', # flake8-comprehensions + 'D', # docstring errors + 'E', # pycodestyle errors + 'F', # pyflakes errors + 'I', # isort errors (import sorting) + 'ISC', # flake8-implicit-str-concat + 'NPY', # numpy-specific checks + 'PD', # pandas-vet + 'PIE', # flake8-pie + 'PL', # pylint errors + 'PTH', # use Path vs os + 'UP', # pyupgrade + 'W', # pycodestyle warnings (whitespace) +] + +ignore = [ + 'B028', # No explicit `stacklevel` keyword argument found + 'B904', # Within an `except` clause distinguish raised exceptions from errors in exception handling + 'B905', # `zip()` without an explicit `strict=` parameter + 'D104', # Missing docstring in __init__ + 'D105', # Missing docstring in magic method + 'D205', # 1 blank line required between summary line and description + 'E741', # ambiguous variable name (l, O, I) + 'PIE790', # Unnecessary `pass` statement + 'PLC2401', # non-ASCII characters + 'PLR0912', # too many branches (max-branches) + 'PLR0913', # too many arguments (max-args) + 'PLR0915', # too many statements (max-statements) + 'PLR2004', # magic value used in comparison +] + +[tool.ruff.lint.isort] +known-first-party = ["sksparse"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ['D', 'I001', 'F403', 'F405'] # ignore import sorting, wildcard imports, and undefined names +"test_*.py" = ['D101', 'D102', 'D103', 'E501'] # ignore docstring checks in test files +"setup.py" = ['D100', 'D101', 'D102', 'D103', 'D104'] # ignore docstrings + +[tool.ruff.lint.pydocstyle] +convention = "numpy" diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index 7d9e2237..00000000 --- a/readthedocs.yml +++ /dev/null @@ -1,5 +0,0 @@ -conda: - file: environment.yml -python: - version: 3 - pip_install: true diff --git a/setup.py b/setup.py index e9e42124..9b8df33b 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008-2017 The scikit-sparse developers: +# Copyright (C) 2008-2025 The scikit-sparse developers: # # 2008 David Cournapeau # 2009-2015 Nathaniel Smith @@ -10,113 +10,102 @@ # 2016-2017 Joscha Reimer # 2021- Justin Ellis # 2022- Aaron Johnson - -"""Sparse matrix tools. - -This is a home for sparse matrix code in Python that plays well with -scipy.sparse, but that is somehow unsuitable for inclusion in scipy -proper. Usually this will be because it is released under the GPL. - -So far we have a wrapper for the CHOLMOD library for sparse Cholesky -decomposition. Further contributions are welcome! -""" +# 2025- Bernard Roesler import os import subprocess import sys +from pathlib import Path -import numpy as np -from Cython.Build import cythonize -from setuptools import Extension, find_packages, setup - -DISTNAME = "scikit-sparse" -DESCRIPTION = "Scikit sparse matrix package" -LONG_DESCRIPTION = __doc__ -MAINTAINER = "Aaron Johnson" -MAINTAINER_EMAIL = "aaron9035@gmail.com" -URL = "https://github.com/scikit-sparse/scikit-sparse" -LICENSE = "BSD" - -INCLUDE_DIRS = [ - np.get_include(), - sys.prefix + "/include", - # Debian's suitesparse-dev installs to - "/usr/include/suitesparse", -] +from setuptools import Extension, setup + + +def get_numpy_include(): + """Get the include directory for NumPy.""" + try: + import numpy as np # noqa: PLC0415 + + return np.get_include() + except ImportError: + return [] + + +INCLUDE_DIRS = [] LIBRARY_DIRS = [] -# check if suitesparse is installed via homebrew -homebrew_suitesparse_dir = ( - subprocess.run( - "readlink -f $(brew --prefix suitesparse)", - shell=True, - stdout=subprocess.PIPE, - ) - .stdout.decode() - .strip() -) -if homebrew_suitesparse_dir: # empty string if not found (because error is printed to stderr) - INCLUDE_DIRS.append( - # Include directory for homebrew-installed suitesparse - homebrew_suitesparse_dir - + "/include/suitesparse/", - ) - LIBRARY_DIRS.append( - # Library directory for homebrew-installed suitesparse - homebrew_suitesparse_dir - + "/lib" - ) +numpy_include = get_numpy_include() +if numpy_include: + INCLUDE_DIRS.append(numpy_include) +# Check user SuiteSparse directories first user_include_dir = os.getenv("SUITESPARSE_INCLUDE_DIR") user_library_dir = os.getenv("SUITESPARSE_LIBRARY_DIR") + if user_include_dir: INCLUDE_DIRS.append(user_include_dir) if user_library_dir: LIBRARY_DIRS.append(user_library_dir) -setup( - install_requires=["numpy>=1.13.3", "scipy>=0.19"], - python_requires=">=3.6", - packages=find_packages(), - package_data={ - "": ["test_data/*.mtx.gz"], - }, - name=DISTNAME, - version="0.4.16", # remember to update __init__.py - maintainer=MAINTAINER, - maintainer_email=MAINTAINER_EMAIL, - description=DESCRIPTION, - license=LICENSE, - url=URL, - long_description=LONG_DESCRIPTION, - classifiers=[ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Programming Language :: Cython", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Mathematics", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - # You may specify the directory where CHOLMOD is installed using the - # library_dirs and include_dirs keywords in the lines below. - ext_modules=cythonize( - Extension( - "sksparse.cholmod", - ["sksparse/cholmod.pyx"], - include_dirs=INCLUDE_DIRS, - library_dirs=LIBRARY_DIRS, - libraries=["cholmod"], +# Check if suitesparse is installed via conda +conda_prefix = os.getenv("CONDA_PREFIX") + +if conda_prefix: + conda_include = Path(conda_prefix) / "include" / "suitesparse" + conda_lib = Path(conda_prefix) / "lib" + if conda_include.is_dir(): + INCLUDE_DIRS.append(str(conda_include)) + if conda_lib.is_dir(): + LIBRARY_DIRS.append(str(conda_lib)) + +# Check if suitesparse is installed via homebrew +try: + homebrew_prefix = ( + subprocess.run( + "readlink -f $(brew --prefix suitesparse)", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=True, # raise an error if command fails ) - ), -) + .stdout.decode() + .strip() + ) + brew_include = Path(homebrew_prefix) / "include" / "suitesparse" + brew_lib = Path(homebrew_prefix) / "lib" + if brew_include.is_dir(): + INCLUDE_DIRS.append(str(brew_include)) + if brew_lib.is_dir(): + LIBRARY_DIRS.append(str(brew_lib)) +except Exception: + pass + +# Check system-wide directories +INCLUDE_DIRS.append(str(Path(sys.prefix) / "include")) +INCLUDE_DIRS.append("/usr/include/suitesparse") # Linux default path + +extension_names = [ + "cholmod", + "amd", + "btf", + "camd", + "colamd", + "ccolamd", + "klu", + "spqr", + "umfpack", +] + +extensions = [ + Extension( + f"sksparse.{name}", + [f"src/sksparse/{name}.pyx"], + include_dirs=INCLUDE_DIRS, + library_dirs=LIBRARY_DIRS, + libraries=[name], + ) + for name in extension_names +] + +# No need to call "cythonize" here. Rely on pyproject.toml. +setup(ext_modules=extensions) diff --git a/sksparse/__init__.py b/sksparse/__init__.py deleted file mode 100644 index 6ff6db18..00000000 --- a/sksparse/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.4.16" diff --git a/sksparse/cholmod.pyx b/sksparse/cholmod.pyx deleted file mode 100644 index 45c82dd4..00000000 --- a/sksparse/cholmod.pyx +++ /dev/null @@ -1,1220 +0,0 @@ -# CHOLMOD wrapper for scikits.sparse - -# Copyright (C) 2008-2017 The scikit-sparse developers: -# -# 2008 David Cournapeau -# 2009-2015 Nathaniel Smith -# 2010 Dag Sverre Seljebotn -# 2014 Leon Barrett -# 2015 Yuri -# 2016-2017 Antony Lee -# 2016 Alex Grigorievskiy -# 2016-2018 Joscha Reimer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. - -#cython: binding = True -#cython: language_level = 3 -#cython: legacy_implicit_noexcept = True - -cimport numpy as np - -import warnings -import numpy as np -from scipy import sparse - -np.import_array() - -cdef extern from "numpy/arrayobject.h": - void PyArray_ENABLEFLAGS(np.ndarray arr, int flags) - -cdef extern from "cholmod_backward_compatible.h": - cdef enum: - CHOLMOD_INT, CHOLMOD_INTLONG, CHOLMOD_LONG - CHOLMOD_PATTERN, CHOLMOD_REAL, CHOLMOD_COMPLEX - CHOLMOD_DOUBLE - CHOLMOD_AUTO, CHOLMOD_SIMPLICIAL, CHOLMOD_SUPERNODAL - CHOLMOD_OK, CHOLMOD_NOT_POSDEF - CHOLMOD_NOT_INSTALLED, CHOLMOD_OUT_OF_MEMORY, CHOLMOD_TOO_LARGE, CHOLMOD_INVALID, CHOLMOD_GPU_PROBLEM - CHOLMOD_A, CHOLMOD_LDLt, CHOLMOD_LD, CHOLMOD_DLt, CHOLMOD_L - CHOLMOD_Lt, CHOLMOD_D, CHOLMOD_P, CHOLMOD_Pt - CHOLMOD_NATURAL, CHOLMOD_GIVEN, CHOLMOD_AMD, CHOLMOD_METIS, CHOLMOD_NESDIS, CHOLMOD_COLAMD, CHOLMOD_POSTORDERED - - ctypedef int SuiteSparse_long - - ctypedef struct cholmod_method_struct: - int ordering - - ctypedef struct cholmod_common: - int supernodal - int status - int itype, dtype - int print - int nmethods, postorder - cholmod_method_struct * method - void (*error_handler)(int status, const char * file, int line, const char * msg) - - ctypedef struct cholmod_sparse: - size_t nrow, ncol, nzmax - void * p # column pointers - void * i # row indices - void * x - int stype # 0 = regular, >0 = upper triangular, <0 = lower triangular - int itype, dtype, xtype - int sorted - int packed - - ctypedef struct cholmod_dense: - size_t nrow, ncol, nzmax, d - void * x - int dtype, xtype - - ctypedef struct cholmod_factor: - size_t n - size_t minor - void * Perm - int itype, xtype - int is_ll, is_super, is_monotonic - size_t xsize, nzmax, nsuper - void * x - void * p - void * super_ "super" - void * pi - void * px - - int cholmod_start(cholmod_common *) except * - int cholmod_l_start(cholmod_common *) except * - - int cholmod_finish(cholmod_common *) except * - int cholmod_l_finish(cholmod_common *) except * - - int cholmod_check_common(cholmod_common *) except * - int cholmod_l_check_common(cholmod_common *) except * - - int cholmod_print_common(const char *, cholmod_common *) except * - int cholmod_l_print_common(const char *, cholmod_common *) except * - - int cholmod_free_sparse(cholmod_sparse **, cholmod_common *) except * - int cholmod_l_free_sparse(cholmod_sparse **, cholmod_common *) except * - - int cholmod_check_sparse(cholmod_sparse *, cholmod_common *) except * - int cholmod_l_check_sparse(cholmod_sparse *, cholmod_common *) except * - - int cholmod_print_sparse(cholmod_sparse *, const char *, cholmod_common *) except * - int cholmod_l_print_sparse(cholmod_sparse *, const char *, cholmod_common *) except * - - int cholmod_free_dense(cholmod_dense **, cholmod_common *) except * - int cholmod_l_free_dense(cholmod_dense **, cholmod_common *) except * - - int cholmod_check_dense(cholmod_dense *, cholmod_common *) except * - int cholmod_l_check_dense(cholmod_dense *, cholmod_common *) except * - - int cholmod_print_dense(cholmod_dense *, const char *, cholmod_common *) except * - int cholmod_l_print_dense(cholmod_dense *, const char *, cholmod_common *) except * - - int cholmod_free_factor(cholmod_factor **, cholmod_common *) except * - int cholmod_l_free_factor(cholmod_factor **, cholmod_common *) except * - - int cholmod_check_factor(cholmod_factor *, cholmod_common *) except * - int cholmod_l_check_factor(cholmod_factor *, cholmod_common *) except * - - int cholmod_print_factor(cholmod_factor *, const char *, cholmod_common *) except * - int cholmod_l_print_factor(cholmod_factor *, const char *, cholmod_common *) except * - - cholmod_factor * cholmod_copy_factor(cholmod_factor *, cholmod_common *) except? NULL - cholmod_factor * cholmod_l_copy_factor(cholmod_factor *, cholmod_common *) except? NULL - - cholmod_factor * cholmod_analyze(cholmod_sparse *, cholmod_common *) except? NULL - cholmod_factor * cholmod_l_analyze(cholmod_sparse *, cholmod_common *) except? NULL - - int cholmod_factorize_p(cholmod_sparse *, double beta[2], - int * fset, size_t fsize, - cholmod_factor *, - cholmod_common *) except * - int cholmod_l_factorize_p(cholmod_sparse *, double beta[2], - SuiteSparse_long * fset, size_t fsize, - cholmod_factor *, - cholmod_common *) except * - - cholmod_sparse * cholmod_submatrix(cholmod_sparse *, - int * rset, int rsize, - int * cset, int csize, - int values, int sorted, - cholmod_common *) except? NULL - cholmod_sparse * cholmod_l_submatrix(cholmod_sparse *, - SuiteSparse_long * rset, SuiteSparse_long rsize, - SuiteSparse_long * cset, SuiteSparse_long csize, - int values, int sorted, - cholmod_common *) except? NULL - - int cholmod_updown(int update, cholmod_sparse *, cholmod_factor *, - cholmod_common *) except * - int cholmod_l_updown(int update, cholmod_sparse *, cholmod_factor *, - cholmod_common *) except * - - cholmod_dense * cholmod_solve(int, cholmod_factor *, - cholmod_dense *, cholmod_common *) except? NULL - cholmod_dense * cholmod_l_solve(int, cholmod_factor *, - cholmod_dense *, cholmod_common *) except? NULL - - cholmod_sparse * cholmod_spsolve(int, cholmod_factor *, - cholmod_sparse *, cholmod_common *) except? NULL - cholmod_sparse * cholmod_l_spsolve(int, cholmod_factor *, - cholmod_sparse *, cholmod_common *) except? NULL - - int cholmod_change_factor(int to_xtype, int to_ll, int to_super, - int to_packed, int to_monotonic, - cholmod_factor *, cholmod_common *) except * - int cholmod_l_change_factor(int to_xtype, int to_ll, int to_super, - int to_packed, int to_monotonic, - cholmod_factor *, cholmod_common *) except * - - cholmod_sparse * cholmod_factor_to_sparse(cholmod_factor *, - cholmod_common *) except? NULL - cholmod_sparse * cholmod_l_factor_to_sparse(cholmod_factor *, - cholmod_common *) except? NULL - -cdef class Common -cdef class Factor - -class CholmodError(Exception): - pass - -class CholmodNotPositiveDefiniteError(CholmodError): - def __init__(self, message, column=None, factor=None): - super().__init__(message) - self.column = column - self.factor = factor - -class CholmodNotInstalledError(CholmodError): - pass - -class CholmodOutOfMemoryError(CholmodError): - pass - -class CholmodTooLargeError(CholmodError): - pass - -class CholmodInvalidError(CholmodError): - pass - -class CholmodGpuProblemError(CholmodError): - pass - -class CholmodWarning(UserWarning): - pass - -class CholmodTypeConversionWarning( - CholmodWarning, sparse.SparseEfficiencyWarning): - pass - -cdef int _integer_typenum = np.NPY_INT32 -cdef object _integer_py_dtype = np.dtype(np.int32) -assert sizeof(int) == _integer_py_dtype.itemsize == 4 - -cdef int _long_typenum = np.NPY_INT64 -cdef object _long_py_dtype = np.dtype(np.int64) -assert sizeof(SuiteSparse_long) == _long_py_dtype.itemsize == 8 - -cdef int _real_typenum = np.NPY_FLOAT64 -cdef object _real_py_dtype = np.dtype(np.float64) -assert sizeof(double) == _real_py_dtype.itemsize == 8 - -cdef int _complex_typenum = np.NPY_COMPLEX128 -cdef object _complex_py_dtype = np.dtype(np.complex128) -assert 2 * sizeof(double) == _complex_py_dtype.itemsize == 16 - -cdef _require_1d_integer(a, dtype): - dtype = np.dtype(dtype) - if a.dtype.itemsize != dtype.itemsize: - warnings.warn("array contains %s bit integers; " - "but %s bit integers are needed; " - "slowing down due to converting" - % (a.dtype.itemsize * 8, - dtype.itemsize * 8), - CholmodTypeConversionWarning) - a = np.ascontiguousarray(a, dtype=dtype) - assert a.dtype == dtype - assert a.ndim == 1 - return a - -########## -# Cholmod -> Python conversion: -########## - -cdef int _np_typenum_for_data(int xtype): - if xtype == CHOLMOD_COMPLEX: - return _complex_typenum - elif xtype == CHOLMOD_REAL: - return _real_typenum - else: - raise CholmodError("cholmod->numpy type conversion failed") - -cdef type _np_dtype_for_data(int xtype): - return np.PyArray_TypeObjectFromType(_np_typenum_for_data(xtype)) - -cdef int _np_typenum_for_index(int itype): - if itype == CHOLMOD_INT: - return _integer_typenum - elif itype == CHOLMOD_LONG: - return _long_typenum - else: - raise CholmodError("cholmod->numpy type conversion failed") - -cdef type _np_dtype_for_index(int itype): - return np.PyArray_TypeObjectFromType(_np_typenum_for_index(itype)) - -cdef class _CholmodSparseDestructor: - """This is a destructor for NumPy arrays based on sparse data of Cholmod. - Use this only once for each Cholmod sparse array. Otherwise memory will be - freed multiple times.""" - cdef cholmod_sparse* _sparse - cdef Common _common - - cdef init(self, cholmod_sparse* m, Common common): - assert m is not NULL - assert common is not None - self._sparse = m - self._common = common - - def __dealloc__(self): - if self._common._use_long: - cholmod_c_free_sparse = cholmod_l_free_sparse - else: - cholmod_c_free_sparse = cholmod_free_sparse - cholmod_c_free_sparse(&self._sparse, &self._common._common) - -cdef _cholmod_sparse_to_scipy_sparse(cholmod_sparse * m, Common common): - """Build a scipy.sparse.csc_matrix that's a view onto m, with a 'base' with - appropriate destructor. 'm' must have been allocated by cholmod.""" - - # This is a little tricky: We build 3 arrays, views on each part of the - # cholmod_dense object. They all have the same _CholmodSparseDestructor - # object as base. So none of them will be deallocated until they have all - # become unused. Then those are built into a csc_matrix. - - # init destructor for cholmod data - cdef _CholmodSparseDestructor base = _CholmodSparseDestructor() - base.init(m, common) - - # convert to NumPy arrays - assert m.itype == common._common.itype - cdef np.ndarray indptr = np.PyArray_SimpleNewFromData( - 1, [m.ncol + 1], _np_typenum_for_index(m.itype), m.p) - cdef np.ndarray indices = np.PyArray_SimpleNewFromData( - 1, [m.nzmax], _np_typenum_for_index(m.itype), m.i) - cdef np.ndarray data = np.PyArray_SimpleNewFromData( - 1, [m.nzmax], _np_typenum_for_data(m.xtype), m.x) - - # set destructor and check if writeable - for array in (indptr, indices, data): - np.set_array_base(array, base) - assert np.PyArray_ISWRITEABLE(indptr) - - # return sparse matrix - return sparse.csc_matrix((data, indices, indptr), shape=(m.nrow, m.ncol)) - -cdef class _CholmodDenseDestructor: - """This is a destructor for NumPy arrays based on dense data of Cholmod. - Use this only once for each Cholmod dense array. Otherwise memory will be - freed multiple times.""" - cdef cholmod_dense* _dense - cdef Common _common - - cdef init(self, cholmod_dense* m, Common common): - assert m is not NULL - assert common is not None - self._dense = m - self._common = common - - def __dealloc__(self): - if self._common._use_long: - cholmod_c_free_dense = cholmod_l_free_dense - else: - cholmod_c_free_dense = cholmod_free_dense - cholmod_c_free_dense(&self._dense, &self._common._common) - -cdef _cholmod_dense_to_numpy_array(cholmod_dense* m, Common common): - """Converts Cholmod dense array to NumPy array. - Use this only once for each Cholmod dense array. Otherwise memory will be - freed multiple times.""" - # init destructor for cholmod data - cdef _CholmodDenseDestructor base = _CholmodDenseDestructor() - base.init(m, common) - # convert cholmod array to numpy array - cdef np.ndarray array = np.PyArray_SimpleNewFromData( - 1, [m.ncol * m.nrow], _np_typenum_for_data(m.xtype), m.x) - # set destructor that frees the memory as base - np.set_array_base(array, base) - # reshape array - array = array.reshape((m.ncol, m.nrow)).T - # check if array is writeable - assert np.PyArray_ISWRITEABLE(array) - # return array - return array - -cdef void _error_handler( - int status, const char * file, int line, const char * msg) except * with gil: - if status == CHOLMOD_OK: - return - full_msg = "{}:{:d}: {} (code {:d})".format(file.decode(), line, msg.decode(), status) - # known errors - if status == CHOLMOD_NOT_POSDEF: - raise CholmodNotPositiveDefiniteError(full_msg) - elif status == CHOLMOD_NOT_INSTALLED: - raise CholmodNotInstalledError(full_msg) - elif status == CHOLMOD_OUT_OF_MEMORY: - raise CholmodOutOfMemoryError(full_msg) - elif status == CHOLMOD_TOO_LARGE: - raise CholmodTooLargeError(full_msg) - elif status == CHOLMOD_INVALID: - raise CholmodInvalidError(full_msg) - elif status == CHOLMOD_GPU_PROBLEM: - raise CholmodGpuProblemError(full_msg) - # unknown errors - if status < 0: - raise CholmodError(full_msg) - # warnings - else: - warnings.warn(full_msg, CholmodWarning) - -def _check_for_csc(m): - if not (sparse.issparse(m) and m.format == "csc"): - warnings.warn("converting matrix of class %s to CSC format" - % (m.__class__.__name__,), - CholmodTypeConversionWarning) - m = m.tocsc() - assert sparse.issparse(m) and m.format == "csc" - return m - -cdef class Common: - cdef cholmod_common _common - cdef int _complex - cdef int _xtype - cdef int _use_long - - def __cinit__(self, _complex, _use_long): - self._complex = _complex - if self._complex: - self._xtype = CHOLMOD_COMPLEX - else: - self._xtype = CHOLMOD_REAL - self._use_long = _use_long - if self._use_long: - cholmod_c_start = cholmod_l_start - else: - cholmod_c_start = cholmod_start - cholmod_c_start(&self._common) - assert (_use_long == 0 and self._common.itype == CHOLMOD_INT) \ - or (_use_long == 1 and self._common.itype == CHOLMOD_LONG) - self._common.print = 0 - self._common.error_handler = ( - _error_handler) - - def __dealloc__(self): - if self._use_long: - cholmod_c_finish = cholmod_l_finish - else: - cholmod_c_finish = cholmod_finish - cholmod_c_finish(&self._common) - - # Debugging: - def _print(self): - if self._use_long: - cholmod_c_check_common = cholmod_l_check_common - cholmod_c_print_common = cholmod_l_print_common - else: - cholmod_c_check_common = cholmod_check_common - cholmod_c_print_common = cholmod_print_common - - print(cholmod_c_check_common(&self._common)) - name = repr(self).encode() - return cholmod_c_print_common(name, &self._common) - - def _print_sparse(self, name, symmetric, matrix): - if self._use_long: - cholmod_c_check_sparse = cholmod_l_check_sparse - cholmod_c_print_sparse = cholmod_l_print_sparse - else: - cholmod_c_check_sparse = cholmod_check_sparse - cholmod_c_print_sparse = cholmod_print_sparse - cdef cholmod_sparse m - cdef object ref = self._init_view_sparse(&m, matrix, symmetric) - print(cholmod_c_check_sparse(&m, &self._common)) - return cholmod_c_print_sparse(&m, name, &self._common) - - def _print_dense(self, name, matrix): - if self._use_long: - cholmod_c_check_dense = cholmod_l_check_dense - cholmod_c_print_dense = cholmod_l_print_dense - else: - cholmod_c_check_dense = cholmod_check_dense - cholmod_c_print_dense = cholmod_print_dense - cdef cholmod_dense m - cdef object ref = self._init_view_dense(&m, matrix) - print(cholmod_c_check_dense(&m, &self._common)) - return cholmod_c_print_dense(&m, name, &self._common) - - ########## - # Python -> Cholmod conversion: - ########## - cdef np.ndarray _cast(self, np.ndarray arr): - if not issubclass(arr.dtype.type, np.number): - raise CholmodError("non-numeric dtype %s" % (arr.dtype,)) - if self._complex: - # All numeric types can be upcast to complex: - return np.asfortranarray(arr, dtype=_complex_py_dtype) - else: - # Refuse to downcast complex types to real: - if issubclass(arr.dtype.type, np.complexfloating): - raise CholmodError("inconsistent use of complex array") - else: - return np.asfortranarray(arr, dtype=_real_py_dtype) - - # Some memory allocated for the init'd sparse matrix is refcounted by the - # returned object; do not let it be garbage collected as long as you want - # to use the matrix. - cdef object _init_view_sparse(self, cholmod_sparse *out, m, symmetric): - if symmetric and m.shape[0] != m.shape[1]: - raise CholmodError("supposedly symmetric matrix is not square!") - m = _check_for_csc(m) - m.sort_indices() - cdef np.ndarray indptr = _require_1d_integer(m.indptr, _np_dtype_for_index(self._common.itype)) - cdef np.ndarray indices = _require_1d_integer(m.indices, _np_dtype_for_index(self._common.itype)) - cdef np.ndarray data = self._cast(m.data) - out.nrow, out.ncol = m.shape - out.nzmax = m.nnz - out.p = indptr.data - out.i = indices.data - out.x = data.data - if symmetric: - out.stype = -1 - else: - out.stype = 0 - out.itype = self._common.itype - out.dtype = CHOLMOD_DOUBLE - out.xtype = self._xtype - out.sorted = 1 - out.packed = 1 - return m, indptr, indices, data - - # Some memory allocated for the init'd dense matrix is refcounted by the - # returned object; do not let it be garbage collected as long as you want - # to use the matrix. - cdef object _init_view_dense(self, cholmod_dense *out, np.ndarray m): - if m.ndim != 2: - raise CholmodError("array has %s dimensions (expected 2)" % m.ndim) - m = self._cast(m) - # The leading dimension is equal to m.shape[0] because `_cast` ensures - # that m is Fortran-contiguous. It is not necessarily equal to - # `m.strides[1] // m.itemsize` when relaxed stride checking is on. - out.nrow = out.d = m.shape[0] - out.ncol = m.shape[1] - out.nzmax = m.size - out.x = m.data - out.dtype = CHOLMOD_DOUBLE - out.xtype = self._xtype - return m - -cdef object factor_secret_handshake = object() - -cdef class Factor: - """This class represents a Cholesky decomposition with a particular - fill-reducing permutation. It cannot be instantiated directly; see - :func:`analyze` and :func:`cholesky`, both of which return objects of type - Factor. - """ - - cdef readonly Common _common - cdef cholmod_factor * _factor - - def __init__(self, handshake): - if handshake is not factor_secret_handshake: - raise CholmodError("Factor may not be constructed directly; use analyze()") - - def __dealloc__(self): - if self._common._use_long: - cholmod_c_free_factor = cholmod_l_free_factor - else: - cholmod_c_free_factor = cholmod_free_factor - cholmod_c_free_factor(&self._factor, &self._common._common) - - def _print(self): - if self._common._use_long: - cholmod_c_check_factor = cholmod_l_check_factor - cholmod_c_print_factor = cholmod_l_print_factor - else: - cholmod_c_check_factor = cholmod_check_factor - cholmod_c_print_factor = cholmod_print_factor - print(cholmod_c_check_factor(self._factor, &self._common._common)) - name = repr(self).encode() - return cholmod_c_print_factor(self._factor, name, &self._common._common) - - def cholesky_inplace(self, A, beta=0): - """Updates this Factor so that it represents the Cholesky - decomposition of :math:`A + \\beta I`, rather than whatever it - contained before. - - :math:`A` must have the same pattern of non-zeros as the matrix used - to create this factor originally.""" - return self._cholesky_inplace(A, True, beta=beta) - - def cholesky_AAt_inplace(self, A, beta=0): - """The same as :meth:`cholesky_inplace`, except it factors :math:`AA' - + \\beta I` instead of :math:`A + \\beta I`.""" - return self._cholesky_inplace(A, False, beta=beta) - - def _cholesky_inplace(self, A, symmetric, beta=0, **kwargs): - cdef cholmod_sparse c_A - cdef object ref = self._common._init_view_sparse(&c_A, A, symmetric) - try: - if self._common._use_long: - cholmod_l_factorize_p(&c_A, [beta, 0], NULL, 0, - self._factor, &self._common._common) - else: - cholmod_factorize_p(&c_A, [beta, 0], NULL, 0, - self._factor, &self._common._common) - except CholmodNotPositiveDefiniteError as e: - e.factor = self - e.column = self._factor.minor - raise - assert self._common._common.status == CHOLMOD_OK - - def copy(self): - """Copies the current :class:`Factor`. - - :returns: A new :class:`Factor` object.""" - if self._common._use_long: - cholmod_c_copy_factor = cholmod_l_copy_factor - else: - cholmod_c_copy_factor = cholmod_copy_factor - - cdef cholmod_factor * c_clone = cholmod_c_copy_factor(self._factor, - &self._common._common) - assert c_clone - cdef Factor clone = Factor(factor_secret_handshake) - clone._common = self._common - clone._factor = c_clone - assert self._factor.itype == clone._factor.itype == self._common._common.itype - assert self._factor.xtype == clone._factor.xtype - return clone - - def cholesky(self, A, beta=0): - """The same as :meth:`cholesky_inplace` except that it first creates - a copy of the current :class:`Factor` and modifies the copy. - - :returns: The new :class:`Factor` object.""" - clone = self.copy() - clone.cholesky_inplace(A, beta=beta) - return clone - - def cholesky_AAt(self, A, beta=0): - """The same as :meth:`cholesky_AAt_inplace` except that it first - creates a copy of the current :class:`Factor` and modifies the copy. - - :returns: The new :class:`Factor` object.""" - clone = self.copy() - clone.cholesky_AAt_inplace(A, beta=beta) - return clone - - def update_inplace(self, C, bint subtract=False): - """Incremental building of :math:`AA'` decompositions. - - Updates this factor so that instead of representing the decomposition - of :math:`A` (:math:`AA'`), it computes the decomposition of - :math:`A + CC'` (:math:`AA' + CC'`) for ``subtract=False`` which is the - default, or :math:`A - CC'` (:math:`AA' - CC'`) for - ``subtract=True``. This method does not require that the - :class:`Factor` was created with :func:`cholesky_AAt`, though that - is the common case. - - The usual use for this is to factor AA' when A has a large number of - columns, or those columns become available incrementally. Instead of - loading all of A into memory, one can load in 'strips' of columns and - pass them to this method one at a time. - - Note that no fill-reduction analysis is done; whatever permutation was - chosen by the initial call to :func:`analyze` will be used regardless - of the pattern of non-zeros in C.""" - # permute C - if self._common._use_long: - cholmod_c_updown = cholmod_l_updown - cholmod_c_free_sparse = cholmod_l_free_sparse - else: - cholmod_c_updown = cholmod_updown - cholmod_c_free_sparse = cholmod_free_sparse - - cdef cholmod_sparse c_C - cdef object ref = self._common._init_view_sparse(&c_C, C, False) - cdef cholmod_sparse * C_perm - - if self._common._use_long: - C_perm = cholmod_l_submatrix( - &c_C, self._factor.Perm, self._factor.n, NULL, -1, - True, True, &self._common._common) - else: - C_perm = cholmod_submatrix( - &c_C, self._factor.Perm, self._factor.n, NULL, -1, - True, True, &self._common._common) - assert C_perm - try: - cholmod_c_updown(not subtract, C_perm, self._factor, - &self._common._common) - finally: - cholmod_c_free_sparse(&C_perm, &self._common._common) - - # Everything below here will fail for matrices that were only analyzed, - # not factorized. - def P(self): - """Returns the fill-reducing permutation P, as a vector of indices. - - The decomposition :math:`LL'` or :math:`LDL'` is of:: - - A[P[:, np.newaxis], P[np.newaxis, :]] - - (or similar for AA').""" - if self._factor.Perm is NULL: - raise CholmodError("you must analyze a matrix first") - assert self._factor.itype == self._common._common.itype - - cdef np.ndarray out = np.PyArray_SimpleNewFromData( - 1, [self._factor.n], _np_typenum_for_index(self._factor.itype), self._factor.Perm) - np.set_array_base(out, self) - - return out - - def _ensure_L_or_LD_inplace(self, want_L): - # In CHOLMOD, supernodal factorizations are always LL'. If we request - # to change to a supernodal LDL' factorization, cholmod_change_factor - # will silently do nothing! So we can only stay supernodal when LL' is - # requested: - if self._common._use_long: - cholmod_c_change_factor = cholmod_l_change_factor - else: - cholmod_c_change_factor = cholmod_change_factor - want_super = self._factor.is_super and want_L - cholmod_c_change_factor(self._factor.xtype, - want_L, # to_ll - want_super, - True, # to_packed - self._factor.is_monotonic, - self._factor, - &self._common._common) - assert bool(self._factor.is_ll) == want_L - - def _L_or_LD(self, want_L): - if self._common._use_long: - cholmod_c_factor_to_sparse = cholmod_l_factor_to_sparse - else: - cholmod_c_factor_to_sparse = cholmod_factor_to_sparse - cdef Factor f = self.copy() - cdef cholmod_sparse * l - f._ensure_L_or_LD_inplace(want_L) - l = cholmod_c_factor_to_sparse(f._factor, - &f._common._common) - assert l - return _cholmod_sparse_to_scipy_sparse(l, self._common) - - def D(self): - """Converts this factorization to the style - - .. math:: LDL' = PAP' - - or - - .. math:: LDL' = PAA'P' - - and then returns the diagonal matrix D *as a 1d vector*. - - .. note:: This method uses an efficient implementation that extracts - the diagonal D directly from CHOLMOD's internal - representation. It never makes a copy of the factor matrices, or - actually converts a full `LL'` factorization into an `LDL'` - factorization just to extract `D`. - - """ - - if self._factor.xtype == CHOLMOD_PATTERN: - raise CholmodError("cannot extract diagonal from a symbolic " - "factor; call a cholesky*() method first.") - - cdef np.ndarray x = np.PyArray_SimpleNewFromData( - 1, [self._factor.xsize if self._factor.is_super else self._factor.nzmax], - _np_typenum_for_data(self._factor.xtype), self._factor.x) - - cdef size_t i - cdef np.npy_intp n - cdef SuiteSparse_long * l_super_ = self._factor.super_ - cdef SuiteSparse_long * l_pi = self._factor.pi - cdef SuiteSparse_long * l_px = self._factor.px - cdef int * i_super_ = self._factor.super_ - cdef int * i_pi = self._factor.pi - cdef int * i_px = self._factor.px - - if self._factor.is_super: - # This is a supernodal factorization, which is stored as a bunch - # of dense, lower-triangular, column-major arrays packed into the - # x vector. This is not documented in the CHOLMOD user-guide, or - # anywhere else as far as I can tell; I got the details from - # CVXOPT's C/cholmod.c. - d = np.empty(self._factor.n, dtype=_np_dtype_for_data(self._factor.xtype)) - filled = 0 - for i in xrange(self._factor.nsuper): - if self._common._use_long: - ncols = l_super_[i + 1] - l_super_[i] - nrows = l_pi[i + 1] - l_pi[i] - px_i = l_px[i] - else: - ncols = i_super_[i + 1] - i_super_[i] - nrows = i_pi[i + 1] - i_pi[i] - px_i = i_px[i] - d[filled:filled + ncols] = x[px_i - :px_i + nrows * ncols - :nrows + 1] - filled += ncols - else: - # This is a simplicial factorization, which is simply stored as a - # sparse CSC matrix in x, p, i. We want the diagonal, which is - # just the first entry in each column; p gives the offsets in x to - # the beginning of each column. - - # The ->p array actually has n+1 entries, but only the first n - # entries actually point to real columns (the last entry is a - # sentinel), so we just create a view onto those: - assert self._factor.itype == self._common._common.itype - p = np.PyArray_SimpleNewFromData( - 1, [self._factor.n], _np_typenum_for_index(self._factor.itype), self._factor.p) - - d = x[p] - if self._factor.is_ll: - return d ** 2 - else: - return d - - def L(self): - """If necessary, converts this factorization to the style - - .. math:: LL' = PAP' - - or - - .. math:: LL' = PAA'P' - - and then returns the sparse lower-triangular matrix L. - - .. warning:: The L matrix returned by this method and the one returned - by :meth:`L_D` are different! - """ - return self._L_or_LD(True) - - def LD(self): - """If necessary, converts this factorization to the style - - .. math:: LDL' = PAP' - - or - - .. math:: LDL' = PAA'P' - - and then returns a sparse lower-triangular matrix "LD", which contains - the D matrix on its diagonal, plus the below-diagonal part of L (the - actual diagonal of L is all-ones). - - See :meth:`L_D` for a more convenient interface.""" - return self._L_or_LD(False) - - def L_D(self): - """If necessary, converts this factorization to the style - - .. math:: LDL' = PAP' - - or - - .. math:: LDL' = PAA'P' - - and then returns the pair (L, D) where L is a sparse lower-triangular - matrix and D is a sparse diagonal matrix. - - .. warning:: The L matrix returned by this method and the one returned - by :meth:`L` are different! - """ - ld = self.LD() - l = sparse.tril(ld, -1) + sparse.eye(*ld.shape) - d = sparse.dia_matrix((ld.diagonal(), [0]), shape=ld.shape) - return (l, d) - - def solve_A(self, b): - """ Solves a linear system. - - :param b: right-hand-side - - :returns: math:`x`, where :math:`Ax = b` (or :math:`AA'x = b`, if - you used :func:`cholesky_AAt`). - - :meth:`__call__` is an alias for this function, i.e., you can simply - call the :class:`Factor` object like a function to solve :math:`Ax = - b`.""" - return self._solve(b, CHOLMOD_A) - - def __call__(self, b): - "Alias for :meth:`solve_A`." - return self.solve_A(b) - - def solve_LDLt(self, b): - """ Solves a linear system. - - :param b: right-hand-side - - :returns: math:`x`, where :math:`LDL'x = b`. - - (This is different from :meth:`solve_A` because it does not correct - for the fill-reducing permutation.)""" - return self._solve(b, CHOLMOD_LDLt) - - def solve_LD(self, b): - """ Solves a linear system. - - :param b: right-hand-side - - :returns: math:`x`, where :math:`LDx = b`.""" - self._ensure_L_or_LD_inplace(False) - return self._solve(b, CHOLMOD_LD) - - def solve_DLt(self, b): - """ Solves a linear system. - - :param b: right-hand-side - - :returns: math:`x`, where :math:`DL'x = b`.""" - - self._ensure_L_or_LD_inplace(False) - return self._solve(b, CHOLMOD_DLt) - - def solve_L(self, b, use_LDLt_decomposition=True): - """ Solves a linear system. - - :param b: right-hand-side - - :param use_LDLt_decomposition: If True, use the `L` of the `LDL'` - decomposition. If False, use the `L` of the `LL'` decomposition. - - :returns: math:`x`, where :math:`Lx = b`.""" - self._ensure_L_or_LD_inplace(not use_LDLt_decomposition) - return self._solve(b, CHOLMOD_L) - - def solve_Lt(self, b, use_LDLt_decomposition=True): - """ Solves a linear system. - - :param b: right-hand-side - - :param use_LDLt_decomposition: If True, use the `L` of the `LDL'` - decomposition. If False, use the `L` of the `LL'` decomposition. - - :returns: math:`x`, where :math:`L'x = b`.""" - self._ensure_L_or_LD_inplace(not use_LDLt_decomposition) - return self._solve(b, CHOLMOD_Lt) - - def solve_D(self, b): - "Returns :math:`x`, where :math:`Dx = b`." - return self._solve(b, CHOLMOD_D) - - # CHOLMOD API is quite confusing here -- unlike all the other solve - # magic constants, CHOLMOD_P and CHOLMOD_Pt actually apply the matrix to b - # rather than performing a matrix solve. Basically their names are - # backwards... therefore let's call the functions `apply_P` and `apply_Pt`. - def apply_P(self, b): - "Returns :math:`x`, where :math:`x = Pb`." - return self._solve(b, CHOLMOD_P) - - def apply_Pt(self, b): - "Returns :math:`x`, where :math:`x = P'b`." - return self._solve(b, CHOLMOD_Pt) - - def _solve(self, b, system): - if sparse.issparse(b): - return self._solve_sparse(b, system) - else: - return self._solve_dense(b, system) - - def _solve_sparse(self, b, system): - if self._common._use_long: - cholmod_c_spsolve = cholmod_l_spsolve - else: - cholmod_c_spsolve = cholmod_spsolve - cdef cholmod_sparse c_b - cdef object ref = self._common._init_view_sparse(&c_b, b, False) - cdef cholmod_sparse *out = cholmod_c_spsolve( - system, self._factor, &c_b, &self._common._common) - return _cholmod_sparse_to_scipy_sparse(out, self._common) - - def _solve_dense(self, b, system): - if self._common._use_long: - cholmod_c_solve = cholmod_l_solve - else: - cholmod_c_solve = cholmod_solve - - b = np.asarray(b) - ndim = b.ndim - if b.ndim == 1: - b = b[:, np.newaxis] - cdef cholmod_dense c_b - cdef object ref = self._common._init_view_dense(&c_b, b) - cdef cholmod_dense *out = cholmod_c_solve( - system, self._factor, &c_b, &self._common._common) - py_out = _cholmod_dense_to_numpy_array(out, self._common) - if ndim == 1: - py_out = py_out[:, 0] - return py_out - - def slogdet(self): - """Computes the log-determinant of the matrix A, with the same API as - :meth:`numpy.linalg.slogdet`. - - This returns a tuple `(sign, logdet)`, where `sign` is always the - number 1.0 (because the determinant of a positive-definite matrix is - always a positive real number), and `logdet` is the (natural) - logarithm of the determinant of the matrix A. - - .. versionadded:: 0.2 - """ - return (1.0, self.logdet()) - - def logdet(self): - """Computes the (natural) log of the determinant of the matrix A. - - If `f` is a factor, then `f.logdet()` is equivalent to - `np.sum(np.log(f.D()))`. - - .. versionadded:: 0.2 - """ - return np.sum(np.log(self.D())) - - def det(self): - """Computes the determinant of the matrix A. - - Consider using :meth:`logdet` instead, for improved numerical - stability. (In particular, determinants are often prone to problems - with underflow or overflow.) - - .. versionadded:: 0.2 - """ - return np.exp(self.logdet()) - - def inv(self): - """Returns the inverse of the matrix A, as a sparse (CSC) matrix. - - .. warning:: For most purposes, it is better to use :meth:`solve` - instead of computing the inverse explicitly. That is, the - following two pieces of code produce identical results:: - - x = f.solve(b) - x = f.inv() * b # DON'T DO THIS! - - But the first line is both faster and produces more accurate - results. - - Sometimes, though, you really do need the inverse explicitly (e.g., - for calculating standard errors in least squares regression), so if - that's your situation, here you go. - - .. versionadded:: 0.2 - """ - return self(sparse.eye(self._factor.n, self._factor.n, - dtype=_np_dtype_for_data(self._factor.xtype), - format="csc")) - -def analyze(A, mode="auto", ordering_method="default", use_long=None): - """Computes the optimal fill-reducing permutation for the symmetric matrix - A, but does *not* factor it (i.e., it performs a "symbolic Cholesky - decomposition"). This function ignores the actual contents of the matrix - A. All it cares about are (1) which entries are non-zero, and (2) whether - A has real or complex type. - - :param A: The matrix to be analyzed. - - :param mode: Specifies which algorithm should be used to (eventually) - compute the Cholesky decomposition -- one of "simplicial", "supernodal", - or "auto". See the CHOLMOD documentation for details on how "auto" chooses - the algorithm to be used. - - :param ordering_method: Specifies which ordering algorithm should be used to - (eventually) order the matrix A -- one of "natural", "amd", "metis", - "nesdis", "colamd", "default" and "best". "natural" means no permutation. - See the CHOLMOD documentation for more details. - - :param use_long: Specifies if the long type (64 bit) or the int type - (32 bit) should be used for the indices of the sparse matrices. If - use_long is None try to estimate if long type is needed. - - :returns: A :class:`Factor` object representing the analysis. Many - operations on this object will fail, because it does not yet hold a full - decomposition. Use :meth:`Factor.cholesky_inplace` (or similar) to - actually factor a matrix. - """ - return _analyze(A, True, mode=mode, ordering_method=ordering_method, use_long=use_long) - -def analyze_AAt(A, mode="auto", ordering_method="default", use_long=None): - """Computes the optimal fill-reducing permutation for the symmetric matrix - :math:`AA'`, but does *not* factor it (i.e., it performs a "symbolic - Cholesky decomposition"). This function ignores the actual contents of the - matrix A. All it cares about are (1) which entries are non-zero, and (2) - whether A has real or complex type. - - :param A: The matrix to be analyzed. - - :param mode: Specifies which algorithm should be used to (eventually) - compute the Cholesky decomposition -- one of "simplicial", "supernodal", - or "auto". See the CHOLMOD documentation for details on how "auto" chooses - the algorithm to be used. - - :param ordering_method: Specifies which ordering algorithm should be used to - (eventually) order the matrix A -- one of "natural", "amd", "metis", - "nesdis", "colamd", "default" and "best". "natural" means no permutation. - See the CHOLMOD documentation for more details. - - :param use_long: Specifies if the long type (64 bit) or the int type - (32 bit) should be used for the indices of the sparse matrices. If - use_long is None try to estimate if long type is needed. - - :returns: A :class:`Factor` object representing the analysis. Many - operations on this object will fail, because it does not yet hold a full - decomposition. Use :meth:`Factor.cholesky_AAt_inplace` (or similar) to - actually factor a matrix. - """ - return _analyze(A, False, mode=mode, ordering_method=ordering_method, use_long=use_long) - -_modes = { - "simplicial": CHOLMOD_SIMPLICIAL, - "supernodal": CHOLMOD_SUPERNODAL, - "auto": CHOLMOD_AUTO, - } -_ordering_methods = { - "natural": CHOLMOD_NATURAL, - "amd": CHOLMOD_AMD, - "metis": CHOLMOD_METIS, - "nesdis": CHOLMOD_NESDIS, - "colamd": CHOLMOD_COLAMD, - "default": None, - "best": None, -} - -def _analyze(A, symmetric, mode, ordering_method="default", use_long=None): - A = _check_for_csc(A) - if use_long is None: - assert A.indices.dtype == A.indptr.dtype - use_long = A.indices.dtype == np.int64 - elif not use_long: - INT_MAX = np.iinfo(np.int32).max - if (A.nnz > INT_MAX or np.any(np.array(A.shape) > INT_MAX)): - warnings.warn('Problem to large for int, switching to long.') - use_long = True - cdef Common common = Common(issubclass(A.dtype.type, np.complexfloating), use_long) - cdef cholmod_sparse c_A - cdef object ref = common._init_view_sparse(&c_A, A, symmetric) - if mode in _modes: - common._common.supernodal = _modes[mode] - else: - raise CholmodError("Unknown mode '%s', must be one of %s" % - (mode, ", ".join(_modes.keys()))) - - if ordering_method in _ordering_methods: - if ordering_method == "default": - common._common.nmethods = 0 - elif ordering_method == "best": - common._common.nmethods = 9 - else: - common._common.nmethods = 1 - common._common.method [0].ordering = _ordering_methods[ordering_method] - common._common.postorder = ordering_method != "natural" - elif ordering_method is not None: - raise CholmodError, ("Unknown ordering method '%s', must be one of %s" - % (ordering_method, ", ".join(_ordering_methods.keys()))) - - if common._use_long: - cholmod_c_analyze = cholmod_l_analyze - else: - cholmod_c_analyze = cholmod_analyze - cdef cholmod_factor *c_f = cholmod_c_analyze(&c_A, &common._common) - if c_f is NULL: - raise CholmodError("Error in cholmod_analyze") - - cdef Factor f = Factor(factor_secret_handshake) - f._common = common - f._factor = c_f - return f - -def cholesky(A, beta=0, mode="auto", ordering_method="default", use_long=None): - """Computes the fill-reducing Cholesky decomposition of - - .. math:: A + \\beta I - - where ``A`` is a sparse, symmetric, positive-definite matrix, preferably - in CSC format, and ``beta`` is any real scalar (usually 0 or 1). (And - :math:`I` denotes the identity matrix.) - - Only the lower triangular part of ``A`` is used. - - ``mode`` is passed to :func:`analyze`. - - ``ordering_method`` is passed to :func:`analyze`. - - ``use_long`` is passed to :func:`analyze`. - - :returns: A :class:`Factor` object represented the decomposition. - """ - return _cholesky(A, True, beta=beta, mode=mode, ordering_method=ordering_method, use_long=use_long) - -def cholesky_AAt(A, beta=0, mode="auto", ordering_method="default", use_long=None): - """Computes the fill-reducing Cholesky decomposition of - - .. math:: AA' + \\beta I - - where ``A`` is a sparse matrix, preferably in CSC format, and ``beta`` is - any real scalar (usually 0 or 1). (And :math:`I` denotes the identity - matrix.) - - Note that if you are solving a conventional least-squares problem, you - will need to transpose your matrix before calling this function, and - therefore it will be somewhat more efficient to construct your matrix in - CSR format (so that its transpose will be in CSC format). - - ``mode`` is passed to :func:`analyze_AAt`. - - ``ordering_method`` is passed to :func:`analyze_AAt`. - - ``use_long`` is passed to :func:`analyze_AAt`. - - :returns: A :class:`Factor` object represented the decomposition. - """ - return _cholesky(A, False, beta=beta, mode=mode, ordering_method=ordering_method, use_long=use_long) - -def _cholesky(A, symmetric, beta, mode, ordering_method="default", use_long=None): - f = _analyze(A, symmetric, mode=mode, ordering_method=ordering_method, use_long=use_long) - f._cholesky_inplace(A, symmetric, beta=beta) - return f - -__all__ = ["analyze", "analyze_AAt", "cholesky", "cholesky_AAt"] diff --git a/sksparse/cholmod_backward_compatible.h b/sksparse/cholmod_backward_compatible.h deleted file mode 100644 index 3a258bde..00000000 --- a/sksparse/cholmod_backward_compatible.h +++ /dev/null @@ -1,22 +0,0 @@ -#include "cholmod.h" - -// CHOLMOD_GPU_PROBLEM is only defined since version 2.0 of cholmod, -// so we need to define it here for backward compatibility -#ifndef CHOLMOD_GPU_PROBLEM - #define CHOLMOD_GPU_PROBLEM -5 -#endif - -// SuiteSparse_long is only defined since version 4.0 of cholmod, -// previously its name was UF_long, -// so we need to define it here for backward compatibility -#ifndef SuiteSparse_long - #ifdef UF_long - #define SuiteSparse_long UF_long - #else - #ifdef _WIN64 - #define SuiteSparse_long __int64 - #else - #define SuiteSparse_long long - #endif - #endif -#endif diff --git a/sksparse/test_cholmod.py b/sksparse/test_cholmod.py deleted file mode 100644 index 0b445319..00000000 --- a/sksparse/test_cholmod.py +++ /dev/null @@ -1,290 +0,0 @@ -# Test code for the scikits.sparse CHOLMOD wrapper. - -# Copyright (C) 2008-2017 The scikit-sparse developers: -# -# 2008 David Cournapeau -# 2009-2015 Nathaniel Smith -# 2010 Dag Sverre Seljebotn -# 2014 Leon Barrett -# 2015 Yuri -# 2016-2017 Antony Lee -# 2016 Alex Grigorievskiy -# 2016-2017 Joscha Reimer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. - -from functools import partial -import os.path - -from pytest import raises as assert_raises -import numpy as np -from numpy.testing import assert_allclose, assert_array_equal -from scipy import sparse -from sksparse.cholmod import ( - cholesky, - cholesky_AAt, - analyze, - analyze_AAt, - CholmodError, - CholmodNotPositiveDefiniteError, - _modes, - _ordering_methods, -) - -modes = tuple(_modes.keys()) -ordering_methods = tuple(_ordering_methods.keys()) - -# Match defaults of np.allclose, which were used before (and are needed). -assert_allclose = partial(assert_allclose, rtol=1e-5, atol=1e-8) - - -def test_cholesky_smoke_test(): - f = cholesky(sparse.eye(10, 10)) - d = np.arange(20).reshape(10, 2) - print("dense") - assert_allclose(f(d), d) - print("sparse") - s_csc = sparse.csc_matrix(np.eye(10)[:, :2]) - assert sparse.issparse(f(s_csc)) - assert_allclose(f(s_csc).todense(), s_csc.todense()) - print("csc_array") - sa_csc = sparse.csc_array(s_csc) - assert sparse.issparse(f(sa_csc)) - assert_allclose(f(sa_csc).todense(), sa_csc.todense()) - print("csr") - s_csr = s_csc.tocsr() - assert sparse.issparse(f(s_csr)) - assert_allclose(f(s_csr).todense(), s_csr.todense()) - print("extract") - assert np.all(f.P() == np.arange(10)) - - -def test_writeability(): - t = cholesky(sparse.eye(10, 10))(np.arange(10)) - assert t.flags["WRITEABLE"] - - -def real_matrix(): - return sparse.csc_matrix([[10, 0, 3, 0], [0, 5, 0, -2], [3, 0, 5, 0], [0, -2, 0, 2]]) - - -def complex_matrix(): - return sparse.csc_matrix([[10, 0, 3 - 1j, 0], [0, 5, 0, -2], [3 + 1j, 0, 5, 0], [0, -2, 0, 2]]) - - -def factor_of(factor, matrix): - return np.allclose( - (factor.L() * factor.L().T.conjugate()).todense(), matrix.todense()[factor.P()[:, np.newaxis], factor.P()[np.newaxis, :]] - ) - - -def convert_matrix_indices_to_long_indices(matrix): - matrix.indices = np.asarray(matrix.indices, dtype=np.int64) - matrix.indptr = np.asarray(matrix.indptr, dtype=np.int64) - return matrix - - -def test_complex(): - c = complex_matrix() - fc = cholesky(c) - r = real_matrix() - fr = cholesky(r) - - assert factor_of(fc, c) - - assert_allclose(fc(np.arange(4))[:, None], c.todense().I * np.arange(4)[:, None]) - assert_allclose(fc(np.arange(4) * 1j)[:, None], c.todense().I * (np.arange(4) * 1j)[:, None]) - assert_allclose(fr(np.arange(4))[:, None], r.todense().I * np.arange(4)[:, None]) - # If we did a real factorization, we can't do solves on complex arrays: - assert_raises(CholmodError, fr, np.arange(4) * 1j) - - -def test_beta(): - for matrix in [real_matrix(), complex_matrix()]: - for beta in [0, 1, 3.4]: - matrix_plus_beta = matrix + beta * sparse.eye(*matrix.shape) - for use_long in [False, True]: - if use_long: - matrix_plus_beta = convert_matrix_indices_to_long_indices(matrix_plus_beta) - for ordering_method in ordering_methods: - for mode in modes: - f = cholesky(matrix, beta=beta, mode=mode, ordering_method=ordering_method) - L = f.L() - assert factor_of(f, matrix_plus_beta) - - -def test_update_downdate(): - m = real_matrix() - f = cholesky(m) - L = f.L()[f.P(), :] - assert factor_of(f, m) - f.update_inplace(L) - assert factor_of(f, 2 * m) - f.update_inplace(L) - assert factor_of(f, 3 * m) - f.update_inplace(L, subtract=True) - assert factor_of(f, 2 * m) - f.update_inplace(L, subtract=True) - assert factor_of(f, m) - - -def test_solve_edge_cases(): - m = real_matrix() - f = cholesky(m) - # sparse matrices give a sparse back: - assert sparse.issparse(f(sparse.eye(*m.shape).tocsc())) - # dense matrices give a dense back: - assert not sparse.issparse(f(np.eye(*m.shape))) - # 1d dense matrices are accepted and a 1d vector is returned (this matches - # the behavior of np.dot): - assert f(np.arange(m.shape[0])).shape == (m.shape[0],) - # 2d dense matrices are also accepted: - assert f(np.arange(m.shape[0])[:, np.newaxis]).shape == (m.shape[0], 1) - # But not if the dimensions are wrong...: - assert_raises(CholmodError, f, np.arange(m.shape[0] + 1)[:, np.newaxis]) - assert_raises(CholmodError, f, np.arange(m.shape[0])[np.newaxis, :]) - assert_raises(CholmodError, f, np.arange(m.shape[0])[:, np.newaxis, np.newaxis]) - # And ditto for the sparse version: - assert_raises(CholmodError, f, sparse.eye(m.shape[0] + 1, m.shape[1]).tocsc()) - - -def mm_matrix(name): - from scipy.io import mmread - - # Supposedly, it is better to use resource_stream and pass the resulting - # open file object to mmread()... but for some reason this fails? - from pkg_resources import resource_filename - - filename = resource_filename(__name__, "test_data/%s.mtx.gz" % name) - matrix = mmread(filename) - if sparse.issparse(matrix): - matrix = matrix.tocsc() - return matrix - - -def test_cholesky_matrix_market(): - for problem in ("well1033", "illc1033", "well1850", "illc1850"): - X = mm_matrix(problem) - y = mm_matrix(problem + "_rhs1") - answer = np.linalg.lstsq(X.todense(), y)[0] - XtX = (X.T * X).tocsc() - Xty = X.T * y - for mode in modes: - assert_allclose(cholesky(XtX, mode=mode)(Xty), answer) - assert_allclose(cholesky_AAt(X.T, mode=mode)(Xty), answer) - assert_allclose(cholesky(XtX, mode=mode).solve_A(Xty), answer) - assert_allclose(cholesky_AAt(X.T, mode=mode).solve_A(Xty), answer) - - f1 = analyze(XtX, mode=mode) - f2 = f1.cholesky(XtX) - assert_allclose(f2(Xty), answer) - assert_raises(CholmodError, f1, Xty) - assert_raises(CholmodError, f1.solve_A, Xty) - assert_raises(CholmodError, f1.solve_LDLt, Xty) - assert_raises(CholmodError, f1.solve_LD, Xty) - assert_raises(CholmodError, f1.solve_DLt, Xty) - assert_raises(CholmodError, f1.solve_L, Xty) - assert_raises(CholmodError, f1.solve_D, Xty) - assert_raises(CholmodError, f1.apply_P, Xty) - assert_raises(CholmodError, f1.apply_Pt, Xty) - f1.P() - assert_raises(CholmodError, f1.L) - assert_raises(CholmodError, f1.LD) - assert_raises(CholmodError, f1.L_D) - assert_raises(CholmodError, f1.L_D) - f1.cholesky_inplace(XtX) - assert_allclose(f1(Xty), answer) - - f3 = analyze_AAt(X.T, mode=mode) - f4 = f3.cholesky(XtX) - assert_allclose(f4(Xty), answer) - assert_raises(CholmodError, f3, Xty) - f3.cholesky_AAt_inplace(X.T) - assert_allclose(f3(Xty), answer) - - print(problem, mode) - for f in (f1, f2, f3, f4): - pXtX = XtX.todense()[f.P()[:, np.newaxis], f.P()[np.newaxis, :]] - assert_allclose(np.prod(f.D()), np.linalg.det(XtX.todense())) - assert_allclose((f.L() * f.L().T).todense(), pXtX) - L, D = f.L_D() - assert_allclose((L * D * L.T).todense(), pXtX) - - b = np.arange(XtX.shape[0])[:, np.newaxis] - assert_allclose(f.solve_A(b), np.dot(XtX.todense().I, b)) - assert_allclose(f(b), np.dot(XtX.todense().I, b)) - assert_allclose(f.solve_LDLt(b), np.dot((L * D * L.T).todense().I, b)) - assert_allclose(f.solve_LD(b), np.dot((L * D).todense().I, b)) - assert_allclose(f.solve_DLt(b), np.dot((D * L.T).todense().I, b)) - assert_allclose(f.solve_L(b), np.dot(L.todense().I, b)) - assert_allclose(f.solve_Lt(b), np.dot(L.T.todense().I, b)) - assert_allclose(f.solve_D(b), np.dot(D.todense().I, b)) - - assert_allclose(f.apply_P(b), b[f.P(), :]) - assert_allclose(f.apply_P(b), b[f.P(), :]) - # Pt is the inverse of P, and argsort inverts permutation - # vectors: - assert_allclose(f.apply_Pt(b), b[np.argsort(f.P()), :]) - assert_allclose(f.apply_Pt(b), b[np.argsort(f.P()), :]) - - -def test_convenience(): - A_dense_seed = np.array([[10, 0, 3, 0], [0, 5, 0, -2], [3, 0, 5, 0], [0, -2, 0, 2]]) - for dtype in (float, complex): - A_dense = np.array(A_dense_seed, dtype=dtype) - A_sp = sparse.csc_matrix(A_dense) - for use_long in [False, True]: - if use_long: - A_sp = convert_matrix_indices_to_long_indices(A_sp) - for ordering_method in ordering_methods: - for mode in modes: - print("----") - print(dtype) - print(A_sp.indices.dtype) - print(use_long) - print(ordering_method) - print(mode) - print("----") - f = cholesky(A_sp, mode=mode, ordering_method=ordering_method) - print(f.D()) - assert_allclose(f.det(), np.linalg.det(A_dense)) - assert_allclose(f.logdet(), np.log(np.linalg.det(A_dense))) - assert_allclose(f.slogdet(), [1, np.log(np.linalg.det(A_dense))]) - assert_allclose((f.inv() * A_sp).todense(), np.eye(4)) - - -def test_CholmodNotPositiveDefiniteError(): - A = -sparse.eye(4).tocsc() - f = cholesky(A) - assert_raises(CholmodNotPositiveDefiniteError, f.L) - - -def test_natural_ordering_method(): - A = real_matrix() - f = cholesky(A, ordering_method="natural") - p = f.P() - assert_array_equal(p, np.arange(len(p))) diff --git a/sksparse/.gitignore b/src/sksparse/.gitignore similarity index 100% rename from sksparse/.gitignore rename to src/sksparse/.gitignore diff --git a/src/sksparse/__init__.py b/src/sksparse/__init__.py new file mode 100644 index 00000000..ba600fd3 --- /dev/null +++ b/src/sksparse/__init__.py @@ -0,0 +1,91 @@ +# Copyright (C) 2008-2025 The scikit-sparse developers: +# +# 2008 David Cournapeau +# 2009-2015 Nathaniel Smith +# 2010 Dag Sverre Seljebotn +# 2014 Leon Barrett +# 2015 Yuri +# 2016-2017 Antony Lee +# 2016 Alex Grigorievskiy +# 2016-2017 Joscha Reimer +# 2021- Justin Ellis +# 2022- Aaron Johnson +# 2025- Bernard Roesler + +""" +=================================== +Scikit Sparse API (:mod:`sksparse`) +=================================== + +.. currentmodule:: sksparse + +.. toctree:: + :maxdepth: 1 + :hidden: + :titlesonly: + + sksparse.amd + sksparse.btf + sksparse.camd + sksparse.ccolamd + sksparse.cholmod + sksparse.colamd + sksparse.klu + sksparse.spqr + sksparse.umfpack + +Provides sparse matrix algorithms not found in SciPy, for use with SciPy's +sparse matrix classes in :mod:`scipy.sparse`. + + +Submodules +========== + +.. autosummary:: + + amd + btf + camd + ccolamd + cholmod + colamd + klu + spqr + umfpack + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse GitHub `_ +""" + +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("scikit-sparse-dev") +except PackageNotFoundError: + # package is not installed, so we set a default version + __version__ = "0.0.0.dev0" + +from . import amd +from . import btf +from . import camd +from . import ccolamd +from . import cholmod +from . import colamd +from . import klu +from . import spqr +from . import umfpack + +__all__ = [ + "amd", + "btf", + "camd", + "ccolamd", + "cholmod", + "colamd", + "klu", + "spqr", + "umfpack", +] diff --git a/src/sksparse/amd.pxd b/src/sksparse/amd.pxd new file mode 100644 index 00000000..48976846 --- /dev/null +++ b/src/sksparse/amd.pxd @@ -0,0 +1,66 @@ +# Cython AMD header interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: amd.pxd +# Created: 2025-07-28 10:27 +# ============================================================================= +# distutils: language = c +# cython: language_level=3 + +from libc.stdint cimport int32_t, int64_t + + +cdef extern from "amd.h": + # sizes of Control and Info + int AMD_CONTROL + int AMD_INFO + + # indices of Control + int AMD_DENSE + int AMD_AGGRESSIVE + + # indices of Info + int AMD_STATUS # return value of amd_order and amd_l_order + int AMD_N # A is n-by-n + int AMD_NZ # number of nonzeros in A + int AMD_SYMMETRY # symmetry of pattern ( is sym., is unsym.) + int AMD_NZDIAG # number of entries on diagonal + int AMD_NZ_A_PLUS_AT # nz in A+A' + int AMD_NDENSE # number of "dense" rows/columns in A + int AMD_MEMORY # amount of memory used by AMD + int AMD_NCMPA # number of garbage collections in AMD + int AMD_LNZ # approx. nz in L, excluding the diagonal + int AMD_NDIV # number of fl. point divides for LU and LDL' + int AMD_NMULTSUBS_LDL # number of fl. point (*,-) pairs for LDL' + int AMD_NMULTSUBS_LU # number of fl. point (*,-) pairs for LU + int AMD_DMAX # max nz. in any column of L, incl. diagonal + + # return values of amd_order and amd_l_order + int AMD_OUT_OF_MEMORY + int AMD_INVALID + + # 32-bit AMD interface + int amd_order( + int32_t n, + const int32_t Ap[], + const int32_t Ai[], + int32_t P[], + double Control[], + double Info[] + ) + void amd_defaults(double Control[]) + + # 64-bit AMD interface + int amd_l_order( + int64_t n, + const int64_t Ap[], + const int64_t Ai[], + int64_t P[], + double Control[], + double Info[] + ) diff --git a/src/sksparse/amd.pyx b/src/sksparse/amd.pyx new file mode 100644 index 00000000..92baf28d --- /dev/null +++ b/src/sksparse/amd.pyx @@ -0,0 +1,442 @@ +# Cython AMD public Python interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: amd.pyx +# Created: 2025-07-28 11:12 +# ============================================================================= + +""" +=============================================================== +Approximate Minimum Degree (AMD) Ordering (:mod:`sksparse.amd`) +=============================================================== + +.. currentmodule:: sksparse.amd + +.. versionadded:: 0.5.0 + +Python interface to the `Approximate Minimum Degree (AMD) +`_ ordering +algorithm. + + +.. _amd-interface: + +Interface +--------- + +.. autosummary:: + :toctree: generated/ + + AMDInfo - Dataclass to hold information statistics returned by the AMD algorithm. + amd - Main function to compute the AMD ordering. + amd_default_control - Get the default control parameters for AMD. + + +.. _amd-exceptions: + +Exceptions and Warnings +----------------------- + +.. autosummary:: + :toctree: generated/ + + AMDError - Base class for AMD-related errors. + AMDInvalidMatrixError - Raised when the input matrix is invalid for AMD. + AMDMemoryError - Raised when AMD runs out of memory. + + +References +---------- + +* SuiteSparse homepage: + https://people.engr.tamu.edu/davis/suitesparse.html +* SuiteSparse AMD: + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/AMD +* AMD Algorithm Publication: + Amestoy, P. R., Davis, T. A., & Duff, I. S. (1996). An approximate minimum + degree ordering algorithm. *SIAM Journal on Matrix Analysis and + Applications*, 17(4), 886-905. +""" + +cimport cython + +import numpy as np + +from dataclasses import dataclass + +from .utils import validate_csc_input + +__all__ = [ + "AMDError", + "AMDInvalidMatrixError", + "AMDMemoryError", + "AMDInfo", + "amd", + "amd_default_control" +] + + +ctypedef fused index_t: + int32_t + int64_t + + +class AMDError(Exception): + """Base class for AMD-related errors.""" + pass + + +class AMDInvalidMatrixError(AMDError, ValueError): + """Raised when the input matrix is invalid for AMD.""" + pass + + +class AMDMemoryError(AMDError, MemoryError): + """Raised when AMD runs out of memory.""" + pass + + +@dataclass(frozen=True) +class AMDInfo: + """Information statistics returned by the AMD algorithm. + + This class wraps the contents of the ``Info`` array output by + ``amd_order()`` into a Python dataclass. + + Attributes + ---------- + status : int + Return status: + * 0 = OK, + * 1 = OK but jumbled, + * -1 = out of memory, + * -2 = invalid matrix. + N : int + Number of rows and columns of the input matrix ``A``. + nz : int + Number of nonzeros in the input matrix ``A``. + symmetry : :class:`float` :math:`\in [0, 1]` + Symmetry of pattern of ``A``. The symmetry is the number of "matched" + off-diagonal entries divided by the total number of off-diagonal + entries. An entry ``A[i, j]`` is matched if ``A[j, i]`` is also an + entry, for any pair ``[i, j]`` where ``i != j``. In python code: + + .. code:: python + + S = A.astype(bool) + B = sparse.tril(S, -1) + sparse.triu(S, 1) + symmetry = (B * B.T).nnz / B.nnz + + nzdiag : int + Number of entries on the diagonal of ``A``. + nz_A_plus_AT : int + Number of nonzeros in ``A + A.T`` (excluding diagonal). + If ``A`` is perfectly symmetric (``symmetry = 1``), with a fully + non-zero diagonal, then ``nz_A_plus_AT = nz - N`` (the smallest + possible value). + If ``A`` is perfectly unsymmetric (``symmetry = 0``, for an upper + triangular matrix, *e.g.*) with no diagonal, + then ``nz_A_plus_AT = 2 * nz`` (the largest possible value). + Ndense : int + Number of dense rows/columns ignored during ordering. These + rows/columns are placed last in the output order ``p``. + memory : float + Memory used, in bytes. This is equal to: + ``(1.2 * nz_A_plus_AT + 9 * N) * sizeof(int)``. This coefficient is at + most ``2.4 * nz + 9 * N``. This accounting excludes the size of the + input arguments ``Ap``, ``Ai``, and ``p``, which have a total size of + ``nz + 2 * N + 1`` integers. + Ncmpa : int + Number of components in the matrix (excluding dense rows/columns). + Lnz : int + Number of nonzeros in the Cholesky factor ``L`` of ``A``, excluding + the diagonal. This is a slight upper bound because of the approximate + degree algorithm. It is a rough upper bound if there are many dense + rows/columns. The remaining statistics are also slight or rough upper + bounds for the same reason. + Ndiv : int + Number of division operations for LU or Cholesky factorization of the + permuted matrix ``A[p][:, p]``. + Nmultsubs_LDL : int + Number of multiply-subtract pairs for ``LDL.T`` factorization. + Nmultsubs_LU : int + Number of multiply-subtract pairs for LU factorization, assuming that + no numerical pivoting is required. + dmax : int + Maximum number of nonzeros in any column of ``L``, including the + diagonal. + + Notes + ----- + Field descriptions are adapted from SuiteSparse ``amd.h`` [#amd_h]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#amd_h] ``amd.h`` - SuiteSparse AMD header file. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/AMD/Include/amd.h + """ + status: int + N: int + nz: int + symmetry: float + nzdiag: int + nz_A_plus_AT: int + Ndense: int + memory: float + Ncmpa: int + Lnz: int + Ndiv: int + Nmultsubs_LDL: int + Nmultsubs_LU: int + dmax: int + + @classmethod + def from_array(cls, info: "np.ndarray") -> "AMDInfo": + return cls( + status=int(info[AMD_STATUS]), + N=int(info[AMD_N]), + nz=int(info[AMD_NZ]), + symmetry=float(info[AMD_SYMMETRY]), + nzdiag=int(info[AMD_NZDIAG]), + nz_A_plus_AT=int(info[AMD_NZ_A_PLUS_AT]), + Ndense=int(info[AMD_NDENSE]), + memory=float(info[AMD_MEMORY]), + Ncmpa=int(info[AMD_NCMPA]), + Lnz=int(info[AMD_LNZ]), + Ndiv=int(info[AMD_NDIV]), + Nmultsubs_LDL=int(info[AMD_NMULTSUBS_LDL]), + Nmultsubs_LU=int(info[AMD_NMULTSUBS_LU]), + dmax=int(info[AMD_DMAX]), + ) + + +def amd(A, dense_thresh=None, aggressive=None, return_info=False): + """Compute the approximate minimum degree ordering of a sparse matrix. + + Adapted from the SuiteSparse `amd.h` documentation [0]_: + + AMD finds a fill-reducing ordering of a sparse matrix ``A``, + using the approximate minimum degree algorithm. The output is + a permutation vector ``p`` such that the Cholesky factor of + ``A[p][:, p]`` has fewer nonzeros than the Cholesky factor of ``A``. + If ``A`` is not symmetric, the algorithm computes an ordering of + ``A + A.T``. + + For more details on the entire package, see the SuiteSparse homepage [1]_ + and Github repository [2]_. + + Parameters + ---------- + A : (N, N) array_like or sparse matrix + A square matrix in CSC format or convertible to CSC. + dense_thresh : float, optional + Threshold number of entries for considering a row/column dense. If + None, use the default value from AMD. The default value is 10. + + Adapted from the SuiteSparse `amd.h` documentation [0]_: + + A dense row/column in ``A + A.T`` can cause AMD to spend a lot of + time in ordering the matrix. If ``dense_thresh >= 0``, rows/columns + with more than ``max(dense_thresh * sqrt(N), 16)`` entries are + ignored during the ordering, and placed last in the output order. + The default value of ``dense_thresh`` is 10. If negative, no + rows/columns are treated as "dense". Rows/columns with 16 or fewer + off-diagonal entries are never considered "dense". + + aggressive : bool, optional + If True, use aggressive absorption. If None, uses the default value + from AMD. The default value is True. + + Adapted from the SuiteSparse `amd.h` documentation [0]_: + + Controls whether or not to use aggressive absorption, in which + a prior element is absorbed into the current element if is a subset + of the current element, even if it is not adjacent to the current + pivot element (refer to Amestoy, Davis, & Duff, 1996, for more + details). The default value is ``True``, which means to perform + aggressive absorption. This nearly always leads to a better + ordering (because the approximate degrees are more accurate) and + a lower execution time. There are cases where it can lead to + a slightly worse ordering, however. + + return_info : bool, optional + If True, returns additional information about the ordering process. + Default is False. + + Returns + ------- + p : :obj:`~numpy.ndarray` + The permutation vector such that the Cholesky factor of ``A[p][:, p]`` + has fewer nonzeros than the Cholesky factor of ``A``. + info : :obj:`~numpy.ndarray`, optional + Additional information about the ordering process, returned if + ``return_info`` is True. Contains various statistics and status codes. + + Raises + ------ + ~scipy.sparse.SparseEfficiencyWarning + If the input matrix is not in CSC format, a warning is raised and the + matrix is converted to CSC format. + ValueError + If the input matrix is not square or cannot be converted to CSC format. + AMDInvalidMatrixError + If the input matrix is invalid for AMD, such as having unsupported + data types or formats. + AMDMemoryError + If the AMD algorithm runs out of memory during execution. + + See Also + -------- + ~sksparse.camd.camd, ~sksparse.colamd.colamd, ~sksparse.ccolamd.ccolamd + + Notes + ----- + This function wraps the AMD (Approximate Minimum Degree) algorithm from + the SuiteSparse by Timothy A. Davis. For details, see the SuiteSparse + repository [2]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [0] `amd.h` - Source header file from SuiteSparse. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/AMD/Include/amd.h + .. [1] SuiteSparse homepage. + https://people.engr.tamu.edu/davis/suitesparse.html + .. [2] SuiteSparse GitHub repository. + https://github.com/DrTimothyAldenDavis/SuiteSparse + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import coo_array + >>> from sksparse.amd import amd + >>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) + >>> N = 11 + >>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + >>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + >>> rng = np.random.default_rng(565656) + >>> vals = rng.random(len(rows), dtype=np.float64) + >>> L = coo_array((vals, (rows, cols)), shape=(N, N)) + >>> A = L + L.T # make it symmetric + >>> A.setdiag(N) # make it strongly positive definite + >>> A = A.tocsc() + >>> p, info = amd(A, return_info=True) + >>> p + array([ 1, 4, 8, 6, 0, 3, 5, 2, 9, 10, 7]) + >>> info + AMDInfo(status=0, N=11, nz=43, symmetry=1.0, nzdiag=11, nz_A_plus_AT=32, + Ndense=0, memory=1096.0, Ncmpa=0, Lnz=19, Ndiv=19, Nmultsubs_LDL=29, + Nmultsubs_LU=39, dmax=4) + """ + + A, _, out_itype = validate_csc_input(A, require_square=True) + + N = A.shape[0] + + if N == 0: + return np.empty(0, dtype=out_itype) + + if A.nnz == 0: + return np.arange(N, dtype=out_itype) + + # Prepare control parameters + ctrl = np.empty(AMD_CONTROL, dtype=np.double) + cdef double[::1] ctrl_view = ctrl + + amd_defaults(&ctrl_view[0]) + + # Update the defaults with user control parameters + if dense_thresh is not None: + ctrl_view[AMD_DENSE] = dense_thresh + + if aggressive is not None: + ctrl_view[AMD_AGGRESSIVE] = 1.0 if aggressive else 0.0 + + info = np.zeros(AMD_INFO, dtype=np.double) + + # Prepare output permutation array + p = np.empty(N, dtype=out_itype) + + # Compute the ordering + _amd_order(N, A.indptr, A.indices, p, ctrl_view, info) + + if return_info: + return p, AMDInfo.from_array(info) + else: + return p + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _amd_order( + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + index_t[::1] p, + double[::1] ctrl, + double[::1] info +): + """Internal Cython wrapper for amd_order and amd_l_order. + + Parameters + ---------- + N : int + Number of rows and columns of the input matrix. + Ap : array_like + Column pointer array of the CSC matrix. + Ai : array_like + Row indices array of the CSC matrix. + p : array_like + Output permutation array. + ctrl : array_like + Control parameters array. + info : array_like + Output information array. + """ + cdef int status + + # AMD ordering + if index_t is int32_t: + status = amd_order(N, &Ap[0], &Ai[0], &p[0], &ctrl[0], &info[0]) + else: + status = amd_l_order(N, &Ap[0], &Ai[0], &p[0], &ctrl[0], &info[0]) + + if status == AMD_OUT_OF_MEMORY: + raise AMDMemoryError("amd: out of memory") + elif status == AMD_INVALID: + dump_info = AMDInfo.from_array(info) + raise AMDInvalidMatrixError(f"amd: input matrix A is invalid:\n{dump_info}") + + +def amd_default_control(): + """Get the default control parameters for AMD. + + Returns + ------- + control : dict + A dictionary containing the default control parameters for AMD. + + The keys are: + + * 'dense_thresh': Threshold for considering a row/column dense. Rows or + columns with more than ``max(dense_thresh * sqrt(N), 16)`` entries + are permuted to the end of the matrix. + * 'aggressive': Whether to use aggressive absorption. + + + .. versionadded:: 0.5.0 + """ + cdef double[::1] ctrl_view = np.empty(AMD_CONTROL, dtype=np.float64) + amd_defaults(&ctrl_view[0]) + return dict( + dense_thresh=ctrl_view[AMD_DENSE], + aggressive=bool(ctrl_view[AMD_AGGRESSIVE]), + ) diff --git a/src/sksparse/btf.pxd b/src/sksparse/btf.pxd new file mode 100644 index 00000000..bd81c95d --- /dev/null +++ b/src/sksparse/btf.pxd @@ -0,0 +1,83 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: btf.pxd +# Created: 2025-08-04 20:12 +# ============================================================================= +# distutils: language = c +# cython: language_level=3 + +from libc.stdint cimport int32_t, int64_t + + +cdef extern from "btf.h": + int32_t btf_maxtrans( + int32_t nrow, + int32_t ncol, + int32_t Ap[], + int32_t Ai[], + double maxwork, + double *work, + int32_t Match[], + int32_t Work[] + ) + + int64_t btf_l_maxtrans( + int64_t nrow, + int64_t ncol, + int64_t Ap[], + int64_t Ai[], + double maxwork, + double *work, + int64_t Match[], + int64_t Work[] + ) + + int32_t btf_strongcomp( + int32_t n, + int32_t Ap[], + int32_t Ai[], + int32_t Q[], + int32_t P[], + int32_t R[], + int32_t Work[] + ) + + int64_t btf_l_strongcomp( + int64_t n, + int64_t Ap[], + int64_t Ai[], + int64_t Q[], + int64_t P[], + int64_t R[], + int64_t Work[] + ) + + int32_t btf_order( + int32_t n, + int32_t Ap[], + int32_t Ai[], + double maxwork, + double *work, + int32_t P[], + int32_t Q[], + int32_t R[], + int32_t *nmatch, + int32_t Work[] + ) + + int64_t btf_l_order( + int64_t n, + int64_t Ap[], + int64_t Ai[], + double maxwork, + double *work, + int64_t P[], + int64_t Q[], + int64_t R[], + int64_t *nmatch, + int64_t Work[] + ) diff --git a/src/sksparse/btf.pyx b/src/sksparse/btf.pyx new file mode 100644 index 00000000..0a92a3d5 --- /dev/null +++ b/src/sksparse/btf.pyx @@ -0,0 +1,500 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: btf.pyx +# Created: 2025-08-04 20:22 +# ============================================================================= + +""" +================================================= +Block Triangular Form (BTF) (:mod:`sksparse.btf`) +================================================= + +.. currentmodule:: sksparse.btf + +.. versionadded:: 0.5.0 + +Python interface to the `Block Triangular Format (BTF) +`_ library. + + +Interface +--------- + +.. autosummary:: + :toctree: generated/ + + maxtrans - Maximum transversal of a sparse matrix. + strongcomp - Strongly connected components of a directed graph. + btf - Permutation into Block Triangular Form (BTF). + btf_q_permutation - Convert raw BTF column permutation to valid permutation. + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse BTF `_ +* Duff, Iain. "On Algorithms for Obtaining a Maximum Transversal", *ACM Trans. + Mathematical Software*, vol 7, no. 1, pp. 315-330. +* "Algorithm 575: Permutations for a Zero-Free Diagonal", *ACM Trans. + Mathematical Software*, vol 7, no. 1, pp. 387-390. Algorithm 575 is MC21A in + the Harwell Subroutine Library. +""" + +cimport cython + +import numpy as np + +from .utils import validate_csc_input + +__all__ = ['maxtrans', 'strongcomp', 'btf', 'btf_q_permutation'] + + +ctypedef fused index_t: + int32_t + int64_t + + +def maxtrans(A): + """Compute the maximum transversal of a sparse matrix. + + This function finds a permutation of the columns of a sparse matrix + so that it has a zero-free diagonal, if possible [#maxtrans_h]_. + + Parameters + ---------- + A : (M, N) {array-like, sparse array} + An array convertible to a sparse matrix in Compressed Sparse Column + (CSC) format. + + Returns + ------- + jmatch : (M,) ndarray + Array containing the maximum transversal. + + Adapted from the BTF maxtrans documentation [#maxtrans_h]_: + + The output is an array ``jmatch`` of size ``N``. If row ``i`` is + matched with column ``j``, then ``A[i, j]`` is nonzero, and then + ``jmatch[i] = j``. If the matrix is structurally nonsingular, all + entries in the ``jmatch`` array are unique, and ``jmatch`` can be + viewed as a column permutation if `A` is square. That is, column + `k` of the original matrix becomes column ``jmatch[k]`` of the + permuted matrix. + + If row ``i`` is not matched with any column, + then ``jmatch[i] = -1``. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#maxtrans_h] BTF maxtrans header file: + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/BTF/Include/btf.h + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import random_array + >>> from sksparse.btf import maxtrans + >>> # Create a non-symmetric matrix + >>> N = 11 + >>> rng = np.random.default_rng(56) + >>> A = random_array((N, N - 3), density=0.5, format='csc', rng=rng) + >>> jmatch = maxtrans(A) + >>> jmatch + array([ 0, 2, 1, 3, 4, 5, 7, -1, 6, -1, -1], dtype=int32) + """ + A, _, out_dtype = validate_csc_input(A) + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if M == 0 or N == 0: + return np.empty(0, dtype=out_dtype) + + if A.nnz == 0: + return np.full(M, -1, dtype=out_dtype) + + # Allocate output array + jmatch = np.zeros(M, dtype=out_dtype) + + cdef double maxwork = 0 # TODO default value? + + _maxtrans(M, N, A.indptr, A.indices, maxwork, jmatch) + + return jmatch + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _maxtrans( + Py_ssize_t M, + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + double maxwork, + index_t[::1] jmatch, +): + cdef index_t nnz_diag + cdef double work + + # Allocate workspace + itype = np.int32 if index_t is int32_t else np.int64 + cdef index_t[::1] workspace = np.zeros(5 * N, dtype=itype) + + if index_t is int32_t: + nnz_diag = btf_maxtrans( + M, + N, + &Ap[0], + &Ai[0], + maxwork, + &work, + &jmatch[0], + &workspace[0] + ) + else: + nnz_diag = btf_l_maxtrans( + M, + N, + &Ap[0], + &Ai[0], + maxwork, + &work, + &jmatch[0], + &workspace[0] + ) + + if nnz_diag < 0: + raise ValueError(f"BTF maxtrans failed with error code: {nnz_diag}") + + +def strongcomp(A, q=None): + """Compute the strongly connected components of a directed graph. + + This function finds a symmetric permutation of a sparse matrix so that + ``A[p][:, p]`` is block upper triangular form [#strongcomp_h]_. + + Parameters + ---------- + A : (N, N) {array-like, sparse array} + An array convertible to a sparse matrix in Compressed Sparse Column + (CSC) format. Must be square. + q : (N,) ndarray of int, optional + A permutation vector. If provided, find the strongly connected + components of ``A[:, qin]``. + + Returns + ------- + p : (N,) ndarray of int + The permutation vector such that ``A[p][:, p]`` is in block upper + triangular form, unless ``q`` is provided (see below). + q : (N,) ndarray of int, optional + If ``q`` is provided on input, ``A[p][:, q]`` is in block upper + triangular form. + r : (Nb+1,) ndarray of int + The array of indices of the start of each block in the permuted matrix. + Block ``b`` is in rows/columns ``r[b]`` to ``r[b+1] - 1``. + The number of blocks is ``len(r) - 1``. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#strongcomp_h] BTF strongcomp header file: + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/BTF/Include/btf.h + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import random_array, block_diag + >>> from sksparse.btf import strongcomp + >>> # Create a matrix with at least 2 strongly connected components + >>> M, N = 4, 7 + >>> rng = np.random.default_rng(56) + >>> A0 = random_array((M, M), density=0.5, rng=rng) + >>> A1 = random_array((N, N), density=0.5, rng=rng) + >>> A = block_diag((A0, A1), format='csc') + >>> # The first M rows/columns are ordered together, then the last N + >>> p, r = strongcomp(A) + array([ 0, 1, 3, 2, 10, 4, 5, 6, 7, 8, 9], dtype=int32) + >>> r + array([ 0, 3, 4, 5, 11], dtype=int32) + """ + A, _, out_dtype = validate_csc_input(A, require_square=True) + + cdef Py_ssize_t N = A.shape[0] + + if N == 0: + p = np.empty(0, dtype=out_dtype) + r = np.zeros(1, dtype=out_dtype) # no blocks + if q is not None: + q = np.empty(0, dtype=out_dtype) + return p, q, r + else: + return p, r + + if A.nnz == 0: + p = np.arange(N, dtype=out_dtype) + r = np.zeros(N + 1, dtype=out_dtype) + r[-1] = N # N blocks of size 1 + if q is not None: + q = np.arange(N, dtype=out_dtype) + return p, q, r + else: + return p, r + + if q is not None: + try: + q = np.ascontiguousarray(q, dtype=out_dtype) + except TypeError: + raise TypeError("qin must be an integer array.") + + # Allocate output arrays + p = np.zeros(N, dtype=out_dtype) + r = np.zeros(N + 1, dtype=out_dtype) + + nblocks = _strongcomp(N, A.indptr, A.indices, q, p, r) + + # Take only the first nblocks of r + r = np.asarray(r[:nblocks + 1]) + + if q is not None: + return p, q, r + else: + return p, r + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _strongcomp( + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + index_t[::1] q, + index_t[::1] p, + index_t[::1] r, +): + cdef index_t nblocks + cdef index_t* q_ptr = NULL + + if q is not None: + if len(q) != N: + raise ValueError("qin must have the same length" + "as the number of columns in A.") + + q_ptr = &q[0] + + # Assign memory for the input/output arrays + itype = np.int32 if index_t is int32_t else np.int64 + cdef index_t[::1] workspace = np.zeros(4 * N, dtype=itype) + + if index_t is int32_t: + nblocks = btf_strongcomp(N, &Ap[0], &Ai[0], q_ptr, &p[0], &r[0], &workspace[0]) + else: + nblocks = btf_l_strongcomp(N, &Ap[0], &Ai[0], q_ptr, &p[0], &r[0], &workspace[0]) + + if nblocks < 0: + raise ValueError(f"BTF strongcomp failed with error code: {nblocks}") + + return nblocks + + +def btf(A): + """Permute the square sparse matrix into Block Triangular Form (BTF). + + This function finds a permutation of a sparse matrix so that + `PAQ` (``A[p][:, q]``) is block upper triangular form with a zero-free + diagonal, or with a maximum number of nonzeros on the diagonal if + a zero-free permutation does not exist [#btf_h]_. + + Parameters + ---------- + A : (N, N) {array-like, sparse array} + An array convertible to a sparse matrix in Compressed Sparse Column + (CSC) format. Must be square. + + Returns + ------- + p : (N,) ndarray of int + The row permutation vector such that ``A[p][:, q]`` is in block upper + triangular form. + q : (N,) ndarray of int + The column permutation vector. If ``A`` is structurally nonsingular, + ``A[p][:, q]`` has a zero-free diagonal. If ``A`` is structurally + singular, ``q`` will contain negative entries. The permuted matrix + is ``A[p][:, abs(q)]``. If ``q[k] < 0``, then ``PAQ[k, k]`` is zero. + r : (Nb+1,) ndarray of int + The array of indices of the start of each block in the permuted matrix. + Block ``b`` is in rows/columns ``r[b]`` to ``r[b+1] - 1``. + The number of blocks is ``len(r) - 1``. + + Notes + ----- + Adapted from the BTF documentation [#btf_h]_: + + The function finds a maximum matching (or perhaps a limited matching if + the work is limited), via the :func:`.maxtrans` function. If a complete + matching is not found, :func:`.btf` completes the permutation, but + flags the columns of ``A[p][:, q]`` to denote which columns are not + matched. If the matrix is structurally rank deficient, some of the + entries on the diagonal of the permuted matrix will be zero. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#btf_h] BTF header file: + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/BTF/Include/btf.h + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import random_array, block_diag + >>> from sksparse.btf import btf + >>> # Create a matrix with at least 2 strongly connected components + >>> M, N = 4, 7 + >>> rng = np.random.default_rng(56) + >>> A0 = random_array((M, M), density=0.5, rng=rng) + >>> A1 = random_array((N, N), density=0.5, rng=rng) + >>> A = block_diag((A0, A1), format='csc') + >>> # The first M rows/columns are ordered together, then the last N + >>> p, q, r = btf(A) + >>> p + array([ 0, 3, 1, 2, 10, 4, 5, 6, 7, 8, 9], dtype=int32) + >>> q + array([ 0, 1, 2, -5, 10, 6, 5, 7, 8, 4, 9], dtype=int32) + >>> r + array([ 0, 2, 3, 4, 5, 11, 9, 10, 0, 0, 0, 0], dtype=int32) + """ + A, _, out_dtype = validate_csc_input(A, require_square=True) + + cdef Py_ssize_t N = A.shape[0] + cdef Py_ssize_t M = A.shape[1] + + if N == 0: + p = np.empty(0, dtype=out_dtype) + q = np.empty(0, dtype=out_dtype) + r = np.zeros(1, dtype=out_dtype) # no blocks + return p, q, r + + if A.nnz == 0: + # p = [0, 1, ..., N - 1] + p = np.arange(N, dtype=out_dtype) + # q = [-2, -3, ..., -(N + 1)] + q = -np.arange(N, dtype=out_dtype) - 2 # flag all columns + r = np.arange(N + 1, dtype=out_dtype) # N blocks of size 1 + return p, q, r + + # Assign memory for the input/output arrays + p = np.zeros(N, dtype=out_dtype) + q = np.zeros(N, dtype=out_dtype) + r = np.zeros(N + 1, dtype=out_dtype) + + cdef double maxwork = 0 # TODO default value? + + _btf(N, A.indptr, A.indices, maxwork, p, q, r) + + return p, q, r + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _btf( + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + double maxwork, + index_t[::1] p, + index_t[::1] q, + index_t[::1] r, +): + cdef: + index_t nblocks + double work + index_t nmatch + + # Define workspace + itype = np.int32 if index_t is int32_t else np.int64 + cdef index_t[::1] workspace = np.zeros(5 * N, dtype=itype) + + if index_t is int32_t: + nblocks = btf_order( + N, + &Ap[0], + &Ai[0], + maxwork, + &work, + &p[0], + &q[0], + &r[0], + &nmatch, + &workspace[0] + ) + else: + nblocks = btf_l_order( + N, + &Ap[0], + &Ai[0], + maxwork, + &work, + &p[0], + &q[0], + &r[0], + &nmatch, + &workspace[0] + ) + + if nblocks < 0: + raise ValueError(f"BTF failed with error code: {nblocks}") + + +def btf_q_permutation(q): + """Convert a raw BTF column permutation vector to a valid permutation. + + Parameters + ---------- + q : (N,) ndarray of int + The raw BTF column permutation vector. Contains negative entries for + unmatched columns. + + Returns + ------- + q_perm : (N,) ndarray of int + The valid BTF column permutation vector. Contains only non-negative + entries, where unmatched columns are replaced with their shifted + absolute values. + + Notes + ----- + In C, the values of ``q`` are converted using ``j = BTF_UNFLIP(Q[k])``, + which is a macro for: + + .. code:: C + + j = (Q[k] < 0) ? -Q[k] - 2 : Q[k] + + This function is a Python equivalent of that macro. + + .. versionadded:: 0.5.0 + + Examples + -------- + >>> import numpy as np + >>> from sksparse.btf import btf_q_permutation + >>> q = np.array([0, 1, 2, -5, 10, 6, 5, 7, 8, 4, 9], dtype=np.int32) + >>> btf_q_permutation(q) + array([ 0, 1, 2, 3, 10, 6, 5, 7, 8, 4, 9], dtype=int32) + """ + q = np.asarray(q) + + if q.ndim != 1: + raise ValueError("Input must be a 1D array.") + + idx = q < 0 + q[idx] = -q[idx] - 2 # flip negative values + return q diff --git a/src/sksparse/camd.pxd b/src/sksparse/camd.pxd new file mode 100644 index 00000000..5ea44b7e --- /dev/null +++ b/src/sksparse/camd.pxd @@ -0,0 +1,68 @@ +# Cython CAMD header interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: camd.pxd +# Created: 2025-08-01 13:02 +# ============================================================================= +# distutils: language = c +# cython: language_level=3 + +from libc.stdint cimport int32_t, int64_t + + +cdef extern from "camd.h": + # sizes of Control and Info + int CAMD_CONTROL + int CAMD_INFO + + # indices of Control + int CAMD_DENSE + int CAMD_AGGRESSIVE + + # indices of Info + int CAMD_STATUS # return value of camd_order and camd_l_order + int CAMD_N # A is n-by-n + int CAMD_NZ # number of nonzeros in A + int CAMD_SYMMETRY # symmetry of pattern ( is sym., is unsym.) + int CAMD_NZDIAG # number of entries on diagonal + int CAMD_NZ_A_PLUS_AT # nz in A+A' + int CAMD_NDENSE # number of "dense" rows/columns in A + int CAMD_MEMORY # amount of memory used by CAMD + int CAMD_NCMPA # number of garbage collections in CAMD + int CAMD_LNZ # approx. nz in L, excluding the diagonal + int CAMD_NDIV # number of fl. point divides for LU and LDL' + int CAMD_NMULTSUBS_LDL # number of fl. point (*,-) pairs for LDL' + int CAMD_NMULTSUBS_LU # number of fl. point (*,-) pairs for LU + int CAMD_DMAX # max nz. in any column of L, incl. diagonal + + # return values of camd_order and camd_l_order + int CAMD_OUT_OF_MEMORY + int CAMD_INVALID + + # 32-bit CAMD interface + int camd_order( + int32_t n, + const int32_t Ap[], + const int32_t Ai[], + int32_t P[], + double Control[], + double Info[], + const int32_t C[] + ) + void camd_defaults(double Control[]) + + # 64-bit CAMD interface + int camd_l_order( + int64_t n, + const int64_t Ap[], + const int64_t Ai[], + int64_t P[], + double Control[], + double Info[], + const int64_t C[] + ) diff --git a/src/sksparse/camd.pyx b/src/sksparse/camd.pyx new file mode 100644 index 00000000..cfdd161c --- /dev/null +++ b/src/sksparse/camd.pyx @@ -0,0 +1,481 @@ +# Cython CAMD public Python interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: camd.pyx +# Created: 2025-08-01 13:04 +# ============================================================================= + +""" +============================================================================= +Constrained Approximate Minimum Degree (CAMD) Ordering (:mod:`sksparse.camd`) +============================================================================= + +.. versionadded:: 0.5.0 + +Python interface to the `Constrained Approximate Minimum Degree (CAMD) +`_ ordering +algorithm. + + +.. _camd-interface: + +Interface +--------- + +.. autosummary:: + :toctree: generated/ + + CAMDInfo - Dataclass to hold information statistics returned by the CAMD algorithm. + camd - Main function to compute the CAMD ordering. + camd_default_control - Get the default control parameters for CAMD. + + +.. _camd-exceptions: + +Exceptions and Warnings +----------------------- + +.. autosummary:: + :toctree: generated/ + + CAMDError - Base class for CAMD-related errors. + CAMDInvalidMatrixError - Raised when the input matrix is invalid for CAMD. + CAMDMemoryError - Raised when CAMD runs out of memory. + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse CAMD `_ +* AMD Algorithm Publication: + Amestoy, P. R., Davis, T. A., & Duff, I. S. (1996). An approximate minimum + degree ordering algorithm. *SIAM Journal on Matrix Analysis and Applications*, + 17(4), 886-905. +""" + +cimport cython + +import numpy as np + +from dataclasses import dataclass + +from .utils import validate_csc_input + +__all__ = [ + "CAMDError", + "CAMDInvalidMatrixError", + "CAMDMemoryError", + "CAMDInfo", + "camd", + "camd_default_control" +] + + +ctypedef fused index_t: + int32_t + int64_t + + +class CAMDError(Exception): + """Base class for CAMD-related errors.""" + pass + + +class CAMDInvalidMatrixError(CAMDError, ValueError): + """Raised when the input matrix is invalid for CAMD.""" + pass + + +class CAMDMemoryError(CAMDError, MemoryError): + """Raised when CAMD runs out of memory.""" + pass + + +@dataclass(frozen=True) +class CAMDInfo: + """Information statistics returned by the CAMD algorithm. + + This class wraps the contents of the ``Info`` array output by + ``camd_order()`` into a Python dataclass. + + Attributes + ---------- + status : int + Return status: + * 0 = OK, + * 1 = OK but jumbled, + * -1 = out of memory, + * -2 = invalid matrix. + N : int + Number of rows and columns of the input matrix ``A``. + nz : int + Number of nonzeros in the input matrix ``A``. + symmetry : :class:`float` :math:`\in [0, 1]` + Symmetry of pattern of ``A``. The symmetry is the number of "matched" + off-diagonal entries divided by the total number of off-diagonal + entries. An entry ``A[i, j]`` is matched if ``A[j, i]`` is also an + entry, for any pair ``[i, j]`` where ``i != j``. In python code: + + .. code:: python + + S = A.astype(bool) + B = sparse.tril(S, -1) + sparse.triu(S, 1) + symmetry = (B * B.T).nnz / B.nnz + + nzdiag : int + Number of entries on the diagonal of ``A``. + nz_A_plus_AT : int + Number of nonzeros in ``A + A.T`` (excluding diagonal). + If ``A`` is perfectly symmetric (``symmetry = 1``), with a fully + non-zero diagonal, then ``nz_A_plus_AT = nz - N`` (the smallest + possible value). + If ``A`` is perfectly unsymmetric (``symmetry = 0``, for an upper + triangular matrix, *e.g.*) with no diagonal, + then ``nz_A_plus_AT = 2 * nz`` (the largest possible value). + Ndense : int + Number of dense rows/columns ignored during ordering. These + rows/columns are placed last in the output order ``p``. + memory : float + Memory used, in bytes. This is equal to: + ``(1.2 * nz_A_plus_AT + 9 * N) * sizeof(int)``. This coefficient is at + most ``2.4 * nz + 9 * N``. This accounting excludes the size of the + input arguments ``Ap``, ``Ai``, and ``p``, which have a total size of + ``nz + 2 * N + 1`` integers. + Ncmpa : int + Number of components in the matrix (excluding dense rows/columns). + Lnz : int + Number of nonzeros in the Cholesky factor ``L`` of ``A``, excluding + the diagonal. This is a slight upper bound because of the approximate + degree algorithm. It is a rough upper bound if there are many dense + rows/columns. The remaining statistics are also slight or rough upper + bounds for the same reason. + Ndiv : int + Number of division operations for LU or Cholesky factorization of the + permuted matrix ``A[p][:, p]``. + Nmultsubs_LDL : int + Number of multiply-subtract pairs for ``LDL.T`` factorization. + Nmultsubs_LU : int + Number of multiply-subtract pairs for LU factorization, assuming that + no numerical pivoting is required. + dmax : int + Maximum number of nonzeros in any column of ``L``, including the + diagonal. + + Notes + ----- + Field descriptions are adapted from SuiteSparse ``camd.h`` [#camd_h]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#camd_h] ``camd.h`` - SuiteSparse CAMD header file. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CAMD/Include/camd.h + """ + status: int + N: int + nz: int + symmetry: float + nzdiag: int + nz_A_plus_AT: int + Ndense: int + memory: float + Ncmpa: int + Lnz: int + Ndiv: int + Nmultsubs_LDL: int + Nmultsubs_LU: int + dmax: int + + @classmethod + def from_array(cls, info: "np.ndarray") -> "CAMDInfo": + return cls( + status=int(info[CAMD_STATUS]), + N=int(info[CAMD_N]), + nz=int(info[CAMD_NZ]), + symmetry=float(info[CAMD_SYMMETRY]), + nzdiag=int(info[CAMD_NZDIAG]), + nz_A_plus_AT=int(info[CAMD_NZ_A_PLUS_AT]), + Ndense=int(info[CAMD_NDENSE]), + memory=float(info[CAMD_MEMORY]), + Ncmpa=int(info[CAMD_NCMPA]), + Lnz=int(info[CAMD_LNZ]), + Ndiv=int(info[CAMD_NDIV]), + Nmultsubs_LDL=int(info[CAMD_NMULTSUBS_LDL]), + Nmultsubs_LU=int(info[CAMD_NMULTSUBS_LU]), + dmax=int(info[CAMD_DMAX]), + ) + + +def camd(A, constraints=None, dense_thresh=None, aggressive=None, return_info=False): + """Compute the approximate minimum degree ordering of a sparse matrix. + + Adapted from the SuiteSparse `camd.h` documentation [0]_: + + CAMD finds a fill-reducing ordering of a sparse matrix ``A``, + using the approximate minimum degree algorithm. The output is + a permutation vector ``p`` such that the Cholesky factor of + ``A[p][:, p]`` has fewer nonzeros than the Cholesky factor of ``A``. + If ``A`` is not symmetric, the algorithm computes an ordering of + ``A + A.T``. + + For more details on the entire package, see the SuiteSparse homepage [1]_ + and Github repository [2]_. + + Parameters + ---------- + A : (N, N) array_like or sparse matrix + A square matrix in CSC format or convertible to CSC. + constraints : (N,) array_like, optional + A 1D array of constraints for the ordering. Each node `i` in the graph + of `A` has a constraint, ``constraints[i]``, in the range [0, N-1]. All + nodes with ``constraints[i] = 0`` are ordered first, followed by nodes + with `C(i) = 1`, and so on. Thus, ``constraints[p]`` is monotonically + non-decreasing. If None, no constraints are applied, and the ordering + will be similar to :func:`~sksparse.amd.amd`, except that the + post-ordering is different. + dense_thresh : float, optional + Threshold number of entries for considering a row/column dense. If + None, use the default value from CAMD. The default value is 10. + + Adapted from the SuiteSparse `camd.h` documentation [0]_: + + A dense row/column in ``A + A.T`` can cause CAMD to spend a lot of + time in ordering the matrix. If ``dense_thresh >= 0``, rows/columns + with more than ``max(dense_thresh * sqrt(N), 16)`` entries are + ignored during the ordering, and placed last in the output order. + The default value of ``dense_thresh`` is 10. If negative, no + rows/columns are treated as "dense". Rows/columns with 16 or fewer + off-diagonal entries are never considered "dense". + + aggressive : bool, optional + If True, use aggressive absorption. If None, uses the default value + from CAMD. The default value is True. + + Adapted from the SuiteSparse `camd.h` documentation [0]_: + + Controls whether or not to use aggressive absorption, in which + a prior element is absorbed into the current element if is a subset + of the current element, even if it is not adjacent to the current + pivot element (refer to Amestoy, Davis, & Duff, 1996, for more + details). The default value is ``True``, which means to perform + aggressive absorption. This nearly always leads to a better + ordering (because the approximate degrees are more accurate) and + a lower execution time. There are cases where it can lead to + a slightly worse ordering, however. + + return_info : bool, optional + If True, returns additional information about the ordering process. + Default is False. + + Returns + ------- + p : ndarray + The permutation vector such that the Cholesky factor of ``A[p][:, p]`` + has fewer nonzeros than the Cholesky factor of ``A``. + info : ndarray, optional + Additional information about the ordering process, returned if + ``return_info`` is True. Contains various statistics and status codes. + + Raises + ------ + ~scipy.sparse.SparseEfficiencyWarning + If the input matrix is not in CSC format, a warning is raised and the + matrix is converted to CSC format. + ValueError + If the input matrix is not square or cannot be converted to CSC format. + CAMDInvalidMatrixError + If the input matrix is invalid for CAMD, such as having unsupported + data types or formats. + CAMDMemoryError + If the CAMD algorithm runs out of memory during execution. + + See Also + -------- + ~sksparse.amd.amd, ~sksparse.colamd.colamd, ~sksparse.ccolamd.ccolamd + + Notes + ----- + This function wraps the CAMD (Approximate Minimum Degree) algorithm from + the SuiteSparse by Timothy A. Davis. For details, see the SuiteSparse + repository [2]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [0] `camd.h` - Source header file from SuiteSparse. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CAMD/Include/camd.h + .. [1] SuiteSparse homepage. + https://people.engr.tamu.edu/davis/suitesparse.html + .. [2] SuiteSparse GitHub repository. + https://github.com/DrTimothyAldenDavis/SuiteSparse + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import coo_array + >>> from sksparse.camd import camd + >>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) + >>> N = 11 + >>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + >>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + >>> rng = np.random.default_rng(565656) + >>> vals = rng.random(len(rows), dtype=np.float64) + >>> L = coo_array((vals, (rows, cols)), shape=(N, N)) + >>> A = L + L.T # make it symmetric + >>> A.setdiag(N) # make it strongly positive definite + >>> A = A.tocsc() + >>> # Constrain the first K nodes to be ordered first + >>> K = 4 + >>> C = np.full(N, K) + >>> C[:K] = np.arange(K) # constrained nodes + >>> p, info = camd(A, constraints=C, return_info=True) + >>> p + array([ 0, 1, 2, 3, 8, 5, 6, 9, 4, 10, 7]) + >>> info + CAMDInfo(status=0, N=11, nz=43, symmetry=1.0, nzdiag=11, nz_A_plus_AT=32, + Ndense=0, memory=1248.0, Ncmpa=0, Lnz=19, Ndiv=19, Nmultsubs_LDL=29, + Nmultsubs_LU=39, dmax=4) + """ + A, _, out_itype = validate_csc_input(A, require_square=True) + + cdef Py_ssize_t N = A.shape[0] + + if N == 0: + return np.empty(0, dtype=out_itype) + + if A.nnz == 0: + return np.arange(N, dtype=out_itype) + + # Prepare control parameters + ctrl = np.empty(CAMD_CONTROL, dtype=np.double) + cdef double[::1] ctrl_view = ctrl + + camd_defaults(&ctrl_view[0]) + + # Update the defaults with user control parameters + if dense_thresh is not None: + ctrl_view[CAMD_DENSE] = dense_thresh + + if aggressive is not None: + ctrl_view[CAMD_AGGRESSIVE] = 1.0 if aggressive else 0.0 + + info = np.zeros(CAMD_INFO, dtype=np.double) + + # Prepare output permutation array + p = np.empty(N, dtype=out_itype) + + if constraints is not None: + # Convert the dtype so the user doesn't have to + try: + constraints = np.ascontiguousarray(constraints, dtype=out_itype) + except TypeError: + raise TypeError("Constraints must be an array of integers.") + + _camd_order(N, A.indptr, A.indices, p, ctrl_view, info, constraints) + + if return_info: + return p, CAMDInfo.from_array(info) + else: + return p + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _camd_order( + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + index_t[::1] p, + double[::1] ctrl, + double[::1] info, + index_t[::1] constraints=None, +): + """Internal Cython wrapper for amd_order and amd_l_order. + + Parameters + ---------- + Ap : array_like + Column pointer array of the CSC matrix. + Ai : array_like + Row indices array of the CSC matrix. + p : array_like + Output permutation array. + ctrl : array_like + Control parameters array. + info : array_like + Output information array. + constraints : array_like, optional + Constraints array. + """ + cdef int status + + # Prepare constraints + # Use a raw pointer to pass NULL if no constraints are given + cdef index_t* constraints_ptr = NULL + + if constraints is not None: + if constraints.shape[0] != N: + raise ValueError("Constraints must have the same length as the matrix size.") + + constraints_ptr = &constraints[0] + + # CAMD ordering + if index_t is int32_t: + status = camd_order( + N, + &Ap[0], + &Ai[0], + &p[0], + &ctrl[0], + &info[0], + constraints_ptr + ) + else: + status = camd_l_order( + N, + &Ap[0], + &Ai[0], + &p[0], + &ctrl[0], + &info[0], + constraints_ptr + ) + + if status == CAMD_OUT_OF_MEMORY: + raise CAMDMemoryError("camd: out of memory") + elif status == CAMD_INVALID: + dump_info = CAMDInfo.from_array(info) + raise CAMDInvalidMatrixError(f"camd: input matrix A is invalid:\n{dump_info}") + + +def camd_default_control(): + """Get the default control parameters for CAMD. + + Returns + ------- + control : dict + A dictionary containing the default control parameters for CAMD. + + The keys are: + + * 'dense_thresh': Threshold for considering a row/column dense. Rows or + columns with more than ``max(dense_thresh * sqrt(N), 16)`` entries + are permuted to the end of the matrix. + * 'aggressive': Whether to use aggressive absorption. + + + .. versionadded:: 0.5.0 + """ + cdef double[::1] ctrl_view = np.empty(CAMD_CONTROL, dtype=np.float64) + camd_defaults(&ctrl_view[0]) + return dict( + dense_thresh=ctrl_view[CAMD_DENSE], + aggressive=bool(ctrl_view[CAMD_AGGRESSIVE]), + ) diff --git a/src/sksparse/ccolamd.pxd b/src/sksparse/ccolamd.pxd new file mode 100644 index 00000000..f9b239e1 --- /dev/null +++ b/src/sksparse/ccolamd.pxd @@ -0,0 +1,110 @@ +# Cython CCOLAMD header interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: ccolamd.pxd +# Created: 2025-07-31 09:45 +# ============================================================================= +# distutils: language = c +# cython: language_level=3 + +from libc.stddef cimport size_t +from libc.stdint cimport int32_t, int64_t +from libc.stdlib cimport calloc, free + + +cdef extern from "ccolamd.h": + ctypedef void* (*alloc_func)(size_t, size_t) + ctypedef void (*free_func)(void *) + + # Get all #defined constants + enum: + # sizes of input and output arrays + CCOLAMD_KNOBS + CCOLAMD_STATS + + # indices of knobs + CCOLAMD_DENSE_ROW + CCOLAMD_DENSE_COL + CCOLAMD_AGGRESSIVE + CCOLAMD_LU + + # indices of stats + CCOLAMD_DEFRAG_COUNT + CCOLAMD_STATUS + CCOLAMD_INFO1 + CCOLAMD_INFO2 + CCOLAMD_INFO3 + + # return values of ccolamd + CCOLAMD_OK + CCOLAMD_OK_BUT_JUMBLED + CCOLAMD_ERROR_A_not_present + CCOLAMD_ERROR_p_not_present + CCOLAMD_ERROR_nrow_negative + CCOLAMD_ERROR_ncol_negative + CCOLAMD_ERROR_nnz_negative + CCOLAMD_ERROR_p0_nonzero + CCOLAMD_ERROR_A_too_small + CCOLAMD_ERROR_col_length_negative + CCOLAMD_ERROR_row_index_out_of_bounds + CCOLAMD_ERROR_out_of_memory + CCOLAMD_ERROR_internal_error + + size_t ccolamd_recommended(int32_t nnz, int32_t n_row, int32_t n_col) + size_t ccolamd_l_recommended(int64_t nnz, int64_t n_row, int64_t n_col) + + void ccolamd_set_defaults(double knobs[CCOLAMD_KNOBS]) + void ccolamd_l_set_defaults(double knobs[CCOLAMD_KNOBS]) + + int c_ccolamd "ccolamd"( + int32_t n_row, + int32_t n_col, + int32_t Alen, + int32_t A[], + int32_t p[], + double knobs[CCOLAMD_KNOBS], + int32_t stats[CCOLAMD_STATS], + int32_t cmember[] + ) + + int c_ccolamd_l "ccolamd_l"( + int64_t n_row, + int64_t n_col, + int64_t Alen, + int64_t A[], + int64_t p[], + double knobs[CCOLAMD_KNOBS], + int64_t stats[CCOLAMD_STATS], + int64_t cmember[] + ) + + int c_csymamd "csymamd"( + int32_t n, + int32_t A[], + int32_t p[], + int32_t perm[], + double knobs[CCOLAMD_KNOBS], + int32_t stats[CCOLAMD_STATS], + alloc_func allocate, + free_func release, + int32_t cmember[], + int32_t stype + ) + + int c_csymamd_l "csymamd_l"( + int64_t n, + int64_t A[], + int64_t p[], + int64_t perm[], + double knobs[CCOLAMD_KNOBS], + int64_t stats[CCOLAMD_STATS], + alloc_func allocate, + free_func release, + int64_t cmember[], + int64_t stype + ) diff --git a/src/sksparse/ccolamd.pyx b/src/sksparse/ccolamd.pyx new file mode 100644 index 00000000..6a663969 --- /dev/null +++ b/src/sksparse/ccolamd.pyx @@ -0,0 +1,688 @@ +# Cython CCOLAMD python interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: ccolamd.pyx +# Created: 2025-07-31 10:13 +# ============================================================================= + +""" +========================================================================================== +Constrained Column Approximate Minimum Degree (CCOLAMD) Ordering (:mod:`sksparse.ccolamd`) +========================================================================================== + +.. currentmodule:: sksparse.ccolamd + +.. versionadded:: 0.5.0 + +Python interface to the `Constrained Column Approximate Minimum Degree +(CCOLAMD) +`_ +ordering algorithm. + + +.. _ccolamd-interface: + +Interface +--------- + +.. autosummary:: + :toctree: generated/ + + ccolamd - Function to compute the column ordering of any shape sparse matrix. + csymamd - Function to compute the column ordering of a symmetric sparse matrix. + ccolamd_get_defaults - Function to get the default knobs for CCOLAMD. + + +.. _ccolamd-exceptions: + +Exceptions and Warnings +----------------------- + +.. autosummary:: + :toctree: generated/ + + CCOLAMDError - Base class for CCOLAMD errors. + CCOLAMDValueError - Raised when CCOLAMD encounters a value error. + CCOLAMDMemoryError - Raised when CCOLAMD runs out of memory. + CCOLAMDInternalError - Raised when CCOLAMD encounters an internal error. + CCOLAMDStats - Dataclass containing statistics about the ordering. + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse CCOLAMD `_ +* CCOLAMD Algorithm Publications: + + * T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, An approximate column + minimum degree ordering algorithm, *ACM Transactions on Mathematical + Software*, vol. 30, no. 3., pp. 353-376, 2004. + + * T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, Algorithm 836: CCOLAMD, + an approximate column minimum degree ordering algorithm, *ACM + Transactions on Mathematical Software*, vol. 30, no. 3., pp. 377-380, + 2004. +""" + +cimport cython + +import numpy as np + +from dataclasses import dataclass + +from .utils import validate_csc_input + +__all__ = [ + "CCOLAMDError", + "CCOLAMDValueError", + "CCOLAMDMemoryError", + "CCOLAMDInternalError", + "CCOLAMDStats", + "ccolamd", + "csymamd", + "ccolamd_get_defaults" +] + + +ctypedef fused index_t: + int32_t + int64_t + + +class CCOLAMDError(Exception): + """Base class for CCOLAMD errors.""" + pass + + +class CCOLAMDValueError(CCOLAMDError, ValueError): + """Raised when CCOLAMD encounters a value error.""" + pass + + +class CCOLAMDMemoryError(CCOLAMDError, MemoryError): + """Raised when CCOLAMD runs out of memory.""" + pass + + +class CCOLAMDInternalError(CCOLAMDError, RuntimeError): + """Raised when CCOLAMD encounters an internal error.""" + pass + + +# Define CCOLAMD error codes +cdef dict _CCOLAMD_ERROR_CODES = { + CCOLAMD_OK: "ok", + CCOLAMD_OK_BUT_JUMBLED: "ok but A has unsorted columns or duplicate entries", + CCOLAMD_ERROR_A_not_present: "A is a null pointer", + CCOLAMD_ERROR_p_not_present: "p is a null pointer", + CCOLAMD_ERROR_nrow_negative: "nrow is negative", + CCOLAMD_ERROR_ncol_negative: "ncol is negative", + CCOLAMD_ERROR_nnz_negative: "nnz is negative", + CCOLAMD_ERROR_p0_nonzero: "p[0] is nonzero", + CCOLAMD_ERROR_A_too_small: "A is too small", + CCOLAMD_ERROR_col_length_negative: "column has a negative number of entries", + CCOLAMD_ERROR_row_index_out_of_bounds: "row index out of bounds", + CCOLAMD_ERROR_out_of_memory: "out of memory", + CCOLAMD_ERROR_internal_error: "internal error" +} + + +cdef int _handle_errors(ok, stats) except -1 with gil: + """Handle errors from CCOLAMD.""" + # Check the return status + if ok: + assert stats[CCOLAMD_STATUS] == CCOLAMD_OK, \ + "CCOLAMD returned OK but status is not CCOLAMD_OK." + else: + if stats[CCOLAMD_STATUS] == CCOLAMD_ERROR_out_of_memory: + raise CCOLAMDMemoryError("CCOLAMD ran out of memory.") + elif stats[CCOLAMD_STATUS] == CCOLAMD_ERROR_internal_error: + raise CCOLAMDInternalError("CCOLAMD encountered an internal error.") + else: + raise CCOLAMDValueError( + f"CCOLAMD returned an error:{_CCOLAMD_ERROR_CODES[stats[CCOLAMD_STATUS]]}." + ) + + +@dataclass(frozen=True) +class CCOLAMDStats: + """Information statistics returned by the CCOLAMD algorithm. + + This class wraps the contents of the ``stats`` array returned by + C ``ccolamd()`` into a Python dataclass. + + Attributes + ---------- + N_rows_ignored : int + The number of dense or empty rows ignored in the ordering. + N_cols_ignored : int + The number of dense or empty columns ignored in the ordering. + Ncmpa : int + The number of garbage collections performed. + status : int + Status code indicating the result of the CCOLAMD operation. If non-zero, + ``ccolamd`` will throw an appropriate exception that interprets this + status code. + + The following fields take on different meanings depending on the value of + ``status``: + + info1 : int + Value of ``status``: + + * 0: the highest numbered column that is unsorted or has + duplicate entries. + * -3: the value of ``n_row``. + * -4: the value of ``n_col``. + * -5: the value of ``nnz == p[n_col]``. + * -6: the value of ``p[0]``. + * -7: the required ``Alen`` value. + * -8: the column with negative entries. + * -9: the column with a row index out of bounds. + info2 : int + Value of ``status``: + + * 0: the last seen duplicate or unsorted row index. + * -7: the actual ``Alen`` value. + * -9: the bad row index. + info3 : int + Value of ``status``: + + * 0: the number of duplicates or unsorted row indices. + * -9: ``n_row``. + + Notes + ----- + Field descriptions are adapted from SuiteSparse ``ccolamd.c`` + [#ccolamd_fields]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#ccolamd_fields] ``ccolamd.c`` - SuiteSparse AMD source file. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CCOLAMD/Source/ccolamd.c + """ + N_rows_ignored : int + N_cols_ignored : int + Ncmpa : int + status : int + info1 : int + info2 : int + info3 : int + + @classmethod + def from_array(cls, stats: "np.ndarray") -> "CCOLAMDStats": + """Create a CCOLAMDStats instance from an array.""" + return cls( + N_rows_ignored=int(stats[CCOLAMD_DENSE_ROW]), + N_cols_ignored=int(stats[CCOLAMD_DENSE_COL]), + Ncmpa=int(stats[CCOLAMD_DEFRAG_COUNT]), + status=int(stats[CCOLAMD_STATUS]), + info1=int(stats[CCOLAMD_INFO1]), + info2=int(stats[CCOLAMD_INFO2]), + info3=int(stats[CCOLAMD_INFO3]), + ) + + +def _ccolamd_base( + object A, + *, + object constraints=None, + bint is_symmetric=False, + object dense_row_thresh=None, + object dense_col_thresh=None, + object aggressive=None, + object opt_lu=None, + bint return_info=False, +): + """A common base function for ccolamd and csymamd.""" + A, _, out_dtype = validate_csc_input(A, is_symmetric) + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if M == 0 or N == 0: + return np.empty(0, dtype=out_dtype) + + if A.nnz == 0 or (not is_symmetric and M == 1): + return np.arange(N, dtype=out_dtype) + + if N == 1: + return np.zeros(N, dtype=out_dtype) + + # Set the default knobs + knobs = np.zeros(CCOLAMD_KNOBS, dtype=np.double) + cdef double[::1] knobs_view = knobs + ccolamd_set_defaults(&knobs_view[0]) + + # Override with user knobs if provided + if dense_row_thresh is not None: + knobs_view[CCOLAMD_DENSE_ROW] = dense_row_thresh + + if dense_col_thresh is not None: + knobs_view[CCOLAMD_DENSE_COL] = dense_col_thresh + + if aggressive is not None: + knobs_view[CCOLAMD_AGGRESSIVE] = 1.0 if aggressive else 0.0 + + if opt_lu is not None: + if opt_lu not in ('lu', 'cholesky'): + raise ValueError("opt_lu must be either 'lu' or 'cholesky'.") + knobs_view[CCOLAMD_LU] = 1.0 if opt_lu == 'lu' else 0.0 + + if constraints is not None: + try: + constraints = np.asarray(constraints, dtype=out_dtype, order='C') + except TypeError: + raise TypeError("Constraints must be an array of integers.") + + # Allocate output rrays + perm = np.zeros(N + 1, dtype=out_dtype) + stats = np.zeros(CCOLAMD_STATS, dtype=out_dtype) + + if is_symmetric: + _csymamd(N, A.indptr, A.indices, perm, knobs_view, stats, constraints) + else: + _ccolamd(M, N, A.indptr, A.indices, perm, knobs_view, stats, constraints) + + # Return the permutation array + q = np.asarray(perm[:N]) + + if return_info: + return q, CCOLAMDStats.from_array(stats) + else: + return q + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _ccolamd( + Py_ssize_t M, + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + index_t[::1] perm, + double[::1] knobs, + index_t[::1] stats, + index_t[::1] constraints=None, +): + """Internal Cython wrapper for CCOLAMD. + + Parameters + ---------- + M : int + Number of rows in the matrix. + N : int + Number of columns in the matrix. + Ap : array_like + Column pointer array of size (N + 1,). + Ai : array_like + Row index array of size (nnz,). + perm : array_like + Output permutation array of size (N + 1,). + knobs : array_like + Knobs array of size (CCOLAMD_KNOBS,). + stats : array_like + Stats array of size (CCOLAMD_STATS,). + """ + cdef int ok + + # Get the recommended size for the Alen array + cdef index_t Alen = 0 + cdef Py_ssize_t nnz = Ai.shape[0] + + if index_t is int32_t: + Alen = ccolamd_recommended(nnz, M, N) + else: + Alen = ccolamd_l_recommended(nnz, M, N) + + if Alen == 0: + raise ValueError("Recommended Alen is zero: one of {A.nnz, M, N} is erroneous.") + + assert Alen >= nnz, "Recommended Alen is less than nnz." + + cdef index_t *constraints_ptr = NULL + + if constraints is not None: + if len(constraints) != N: + raise ValueError("Constraints must have the same length as the matrix size.") + + constraints_ptr = &constraints[0] + + # Copy the input arrays, since they are altered in the C function + itype = np.int32 if index_t is int32_t else np.int64 + cdef index_t[::1] Ai_work = np.zeros(Alen, dtype=itype) + Ai_work[:nnz] = Ai + perm[:] = Ap + + if index_t is int32_t: + ok = c_ccolamd(M, N, Alen, &Ai_work[0], &perm[0], &knobs[0], &stats[0], constraints_ptr) + else: + ok = c_ccolamd_l(M, N, Alen, &Ai_work[0], &perm[0], &knobs[0], &stats[0], constraints_ptr) + + _handle_errors(ok, stats) + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _csymamd( + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + index_t[::1] perm, + double[::1] knobs, + index_t[::1] stats, + index_t[::1] constraints=None, +): + """Internal Cython wrapper for CSYMAMD. + + Parameters + ---------- + M : int + Number of rows in the matrix. + N : int + Number of columns in the matrix. + Ap : array_like + Column pointer array of size (N + 1,). + Ai : array_like + Row index array of size (nnz,). + perm : array_like + Output permutation array of size (N + 1,). + knobs : array_like + Knobs array of size (COLAMD_KNOBS,). + stats : array_like + Stats array of size (COLAMD_STATS,). + """ + cdef int ok + cdef index_t stype = -1 # only lower triangular part is used in csymamd + cdef index_t *constraints_ptr = NULL + + if constraints is not None: + if len(constraints) != N: + raise ValueError("Constraints must have the same length as the matrix size.") + + constraints_ptr = &constraints[0] + + # Compute the ordering + if index_t is int32_t: + ok = c_csymamd( + N, + &Ai[0], + &Ap[0], + &perm[0], + &knobs[0], + &stats[0], + calloc, + free, + constraints_ptr, + stype + ) + else: + ok = c_csymamd_l( + N, + &Ai[0], + &Ap[0], + &perm[0], + &knobs[0], + &stats[0], + calloc, + free, + constraints_ptr, + stype + ) + + _handle_errors(ok, stats) + + +def ccolamd( + A, + constraints=None, + dense_row_thresh=None, + dense_col_thresh=None, + aggressive=None, + opt_lu=None, + return_info=False +): + return _ccolamd_base( + A, + constraints=constraints, + is_symmetric=False, + dense_row_thresh=dense_row_thresh, + dense_col_thresh=dense_col_thresh, + aggressive=aggressive, + opt_lu=None, + return_info=return_info + ) + + +def csymamd( + A, + constraints=None, + dense_row_thresh=None, + dense_col_thresh=None, + aggressive=None, + return_info=False +): + return _ccolamd_base( + A, + constraints=constraints, + is_symmetric=True, + dense_row_thresh=dense_row_thresh, + dense_col_thresh=dense_col_thresh, + aggressive=aggressive, + opt_lu=None, + return_info=return_info + ) + + +_CCOLAMD_DOC_TEMPLATE = """ +{intro} +Parameters +---------- +{A_param} +contraints : (N,) array_like, optional + A 1D array of constraints for the ordering. Each column `i` in + `A` has a constraint, ``constraints[i]``, in the range [0, N-1]. All + columns with ``constraints[i] = 0`` are ordered first, followed by nodes + with `C(i) = 1`, and so on. Thus, ``constraints[p]`` is monotonically + non-decreasing. If None, no constraints are applied, and the ordering + will be similar to :func:`~sksparse.colamd.colamd`, except that the default + values of ``dense_row_thresh``, ``dense_col_thresh``, and ``aggressive`` + may differ. +dense_row_thresh, dense_col_thresh : float, optional + Threshold for considering a row/column dense. If + None, use the default value from CCOLAMD. The default value is 10. + The actual number of entries in a row/column is to be considered + "dense" is ``max(dense_row_thresh * sqrt(M), 16)`` where ``M`` is the + number of rows (or ``N`` for columns). Dense rows/columns are ignored + during ordering and moved to the end of the matrix. +aggressive : bool, optional + If True, use aggressive absorption. If None, uses the default value + from CCOLAMD. The default value is True. +{opt_lu_param} + +Returns +------- +q : (N,) :class:`~numpy.ndarray` + The permutation vector. +stats : :class:`CCOLAMDStats`, optional + If ``return_info`` is True, returns an object containing statistics + about the ordering. + +See Also +-------- +{see_also} + + +.. versionadded:: 0.5.0 + +References +---------- +.. {reftag} ``ccolamd.c`` - SuiteSparse AMD source file. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CCOLAMD/Source/ccolamd.c + +Examples +-------- +{example} +""" + +# Define the docstrings +ccolamd_reftag = "[#ccolamd_c]" + +ccolamd_intro = f"""Compute the column approximate minimum degree ordering of +a sparse matrix. + +Adapted from the CCOLAMD documentation {ccolamd_reftag}_: + + This function computes a column ordering for a sparse matrix `A` that + is appropriate for LU factorization of symmetric or unsymmetric + matrices, QR factorization, least squares, interior point methods for + linear programming problems, and other related problems. + + CCOLAMD computes a permutation `Q` such that the Cholesky factorization + of :math:`(AQ)^{{\\top}}(AQ)` has less fill-in and requires fewer floating + point operations than :math:`A^{{\\top}}A`. This also provides a good + ordering for sparse partial pivoting methods, :math:`P(AQ) = LU`, where + `Q` is computed prior to numerical factorization, and `P` is computed + during numerical factorization via conventional partial pivoting with + row interchanges. +""" + +ccolamd_A_param = """A : (M, N) {array_like, sparse matrix} + The input matrix for which to compute the column ordering. + Must be 2D and convertible to CSC format. Need not be square.""" + +ccolamd_opt_lu_param = """opt_lu : {'lu', 'cholesky'}, optional + If 'lu', the ordering is optimized for LU factorization of `A`. If 'cholesky', + the ordering is optimized for Cholesky factorization of :math:`A^{\\top} + A`. If None, uses the default value from CCOLAMD, which is 'cholesky'.""" + +_ccolamd_example = """\ +>>> import numpy as np +>>> from scipy.sparse import random_array +>>> from sksparse.ccolamd import ccolamd +>>> # Create a non-symmetric matrix +>>> N = 11 +>>> rng = np.random.default_rng(56) +>>> A = random_array((N, N - 3), density=0.5, format='csc', rng=rng) +>>> A.setdiag(N) # make the diagonal non-zero +>>> # Constrain the first K nodes to be ordered first +>>> K = 4 +>>> C = np.full(A.shape[1], K) +>>> C[:K] = np.arange(K) # constrained nodes +>>> p, info = ccolamd(A, constraints=C, return_info=True) +>>> p +array([0, 1, 2, 3, 4, 7, 6, 5], dtype=int32) +>>> info +CCOLAMDStats(N_rows_ignored=0, N_cols_ignored=0, Ncmpa=0, status=0, info1=-1, + info2=-1, info3=0) +""" + +ccolamd.__doc__ = _CCOLAMD_DOC_TEMPLATE.format( + intro=ccolamd_intro, + A_param=ccolamd_A_param, + opt_lu_param=ccolamd_opt_lu_param, + see_also="csymamd, ~sksparse.colamd.colamd, ~sksparse.colamd.symamd", + reftag=ccolamd_reftag, + example=_ccolamd_example, +) + + +# Define the docstring for csymamd +csymamd_reftag = "[#csymamd_c]" + +csymamd_intro = f"""Compute the column approximate minimum degree ordering of +a sparse symmetric matrix. + +Adapted from the CCOLAMD documentation {csymamd_reftag}_: + + This function computes an approximate minimum degree ordering for + Cholesky factorization of symmetric matrices. + + Symamd computes a permutation `P` of a symmetric matrix `A` such that + the Cholesky factorization of :math:`PAP^{{\\top}}` has less fill-in and + requires fewer floating point operations than `A`. Symamd constructs + a matrix `M` such that :math:`M^{{\\top}}M` has the same nonzero pattern + of `A`, and then orders the columns of `M` using ccolamd. The column + ordering of `M` is then returned as the row and column ordering `P` of + `A`. +""" + +csymamd_A_param = """A : (N, N) array_like or sparse matrix + The input matrix for which to compute the column ordering. + Must be 2D, square, and convertible to CSC format. + + .. note:: + + This routine only accesses the lower triangular part of ``A``, + which is *assumed* to be symmetric. If it is not, the results may + be incorrect or undefined. + +""" + +_csymamd_example = """\ +>>> import numpy as np +>>> from scipy.sparse import random_array +>>> from sksparse.ccolamd import csymamd +>>> # Create a non-symmetric matrix +>>> N = 11 +>>> rng = np.random.default_rng(56) +>>> A = random_array((N, N - 3), density=0.5, format='csc', rng=rng) +>>> A.setdiag(N) # make the diagonal non-zero +>>> A = (A.T @ A).tocsc() # make A symmetric +>>> # Constrain the first K nodes to be ordered first +>>> K = 4 +>>> C = np.full(A.shape[1], K) +>>> C[:K] = np.arange(K) # constrained nodes +>>> p, info = csymamd(A, constraints=C, return_info=True) +>>> p +array([0, 1, 2, 3, 7, 6, 5, 4], dtype=int32) +>>> info +CCOLAMDStats(N_rows_ignored=0, N_cols_ignored=0, Ncmpa=0, status=0, info1=-1, + info2=-1, info3=0) +""" + +csymamd.__doc__ = _CCOLAMD_DOC_TEMPLATE.format( + intro=csymamd_intro, + A_param=csymamd_A_param, + opt_lu_param='', + see_also="ccolamd, ~sksparse.colamd.colamd, ~sksparse.colamd.symamd", + reftag=csymamd_reftag, + example=_csymamd_example, +) + + +def ccolamd_get_defaults(): + """Get the default knobs for CCOLAMD. + + Returns + ------- + knobs : dict + A dictionary containing the default knobs for CCOLAMD. + + The keys are: + + * 'dense_row_thresh': Threshold for considering a row/column dense. + Rows with more than ``max(dense_row_thresh * sqrt(M), 16)`` entries + are permuted to the end of the matrix. + * 'dense_col_thresh': Like `dense_row_thresh`, but for columns. + * 'aggressive': Default value for the aggressive knob. + + + .. versionadded:: 0.5.0 + """ + knobs = np.zeros(CCOLAMD_KNOBS, dtype=np.double) + cdef double[::1] knobs_view = knobs + ccolamd_set_defaults(&knobs_view[0]) + return dict( + dense_row_thresh=knobs[CCOLAMD_DENSE_ROW], + dense_col_thresh=knobs[CCOLAMD_DENSE_COL], + aggressive=knobs[CCOLAMD_AGGRESSIVE], + opt_lu='lu' if knobs[CCOLAMD_LU] else 'cholesky', + ) diff --git a/src/sksparse/cholmod.pxd b/src/sksparse/cholmod.pxd new file mode 100644 index 00000000..cfd42913 --- /dev/null +++ b/src/sksparse/cholmod.pxd @@ -0,0 +1,552 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2008-2025 The scikit-sparse developers. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: cholmod.pxd +# Created: 2025-08-11 12:59 +# ============================================================================= +# distutils: language = c + +from libc.stdlib cimport malloc +from libc.stdint cimport int32_t, int64_t, uintptr_t +from libc.string cimport memcpy, memset + +cimport numpy as cnp + + +cdef extern from "cholmod.h": + # xtypes + int CHOLMOD_PATTERN + int CHOLMOD_REAL + int CHOLMOD_COMPLEX + int CHOLMOD_ZOMPLEX # only used in old MATLAB interface + + # itypes + int CHOLMOD_INT + int CHOLMOD_LONG + + # dtypes + int CHOLMOD_SINGLE + int CHOLMOD_DOUBLE + + # supernodal types + int CHOLMOD_SIMPLICIAL + int CHOLMOD_AUTO + int CHOLMOD_SUPERNODAL + + # Ordering methods + int CHOLMOD_MAXMETHODS + int CHOLMOD_NATURAL + int CHOLMOD_GIVEN + int CHOLMOD_AMD + int CHOLMOD_METIS + int CHOLMOD_NESDIS + int CHOLMOD_COLAMD + int CHOLMOD_POSTORDERED + + # Output codes + int CHOLMOD_OK + int CHOLMOD_NOT_INSTALLED + int CHOLMOD_OUT_OF_MEMORY + int CHOLMOD_TOO_LARGE + int CHOLMOD_INVALID + int CHOLMOD_GPU_PROBLEM + int CHOLMOD_NOT_POSDEF + int CHOLMOD_DSMALL + + # Solve codes + int CHOLMOD_A + int CHOLMOD_LDLt + int CHOLMOD_LD + int CHOLMOD_DLt + int CHOLMOD_L + int CHOLMOD_Lt + int CHOLMOD_D + int CHOLMOD_P + int CHOLMOD_Pt + + ctypedef struct cholmod_method_struct: + double lnz + double fl + double prune_dense + double prune_dense2 + double nd_oksep + size_t nd_small + int aggressive + int order_for_lu + int nd_compress + int nd_camd + int nd_components + int ordering + + ctypedef struct cholmod_common: + int supernodal + int final_asis + int final_super + int final_ll + int final_pack + int final_monotonic + int final_resymbol + int quick_return_if_not_posdef + int nmethods + int current + int selected + cholmod_method_struct method[] + int postorder + int itype + int status + double fl + double lnz + double anz + double modfl + size_t malloc_count + size_t memory_usage + size_t memory_inuse + double nrealloc_col + double nrealloc_factor + double ndbounds_hit + double nsbounds_hit + double rowfacfl + double aatfl + int called_nd + int blas_ok + + double SPQR_grain + double SPQR_small + int SPQR_shrink + int SPQR_nthreads + + double SPQR_flopcount + double SPQR_analyze_time + double SPQR_factorize_time + double SPQR_solve_time + double SPQR_flopcount_bound + double SPQR_tol_used + double SPQR_norm_E_fro + int64_t SPQR_istat[8] + + ctypedef struct cholmod_factor: + size_t n + size_t minor + void *Perm + void *ColCount + size_t nzmax + void *p + void *i + void *x + void *z + void *nz + void *next + void *prev + int ordering + int is_ll + int is_super + int is_monotonic + int itype + int xtype + int dtype + + ctypedef struct cholmod_sparse: + size_t nrow + size_t ncol + size_t nzmax + void *p + void *i + void *x + void *z + int stype + int itype + int xtype + int dtype + int sorted + int packed + + ctypedef struct cholmod_dense: + size_t nrow + size_t ncol + size_t nzmax + size_t d + void *x + void *z + int xtype + int dtype + + int cholmod_start(cholmod_common *Common) + int cholmod_l_start(cholmod_common *Common) + + int cholmod_finish(cholmod_common *Common) + int cholmod_l_finish(cholmod_common *Common) + + cholmod_factor* cholmod_analyze(cholmod_sparse *A, cholmod_common *Common) + cholmod_factor* cholmod_l_analyze(cholmod_sparse *A, cholmod_common *Common) + + int cholmod_factorize_p( + cholmod_sparse *A, + double beta[2], + int32_t *fset, + size_t fsize, + cholmod_factor *L, + cholmod_common *Common + ) + int cholmod_l_factorize_p( + cholmod_sparse *A, + double beta[2], + int64_t *fset, + size_t fsize, + cholmod_factor *L, + cholmod_common *Common + ) + + cholmod_sparse *cholmod_spsolve( + int sys, + cholmod_factor *L, + cholmod_sparse *B, + cholmod_common *Common + ) + cholmod_sparse *cholmod_l_spsolve( + int sys, + cholmod_factor *L, + cholmod_sparse *B, + cholmod_common *Common + ) + + cholmod_dense *cholmod_solve( + int sys, + cholmod_factor *L, + cholmod_dense *B, + cholmod_common *Common + ) + cholmod_dense *cholmod_l_solve( + int sys, + cholmod_factor *L, + cholmod_dense *B, + cholmod_common *Common + ) + + double cholmod_rcond(cholmod_factor *L, cholmod_common *Common) + double cholmod_l_rcond(cholmod_factor *L, cholmod_common *Common) + + int cholmod_updown( + int update, + cholmod_sparse *C, + cholmod_factor *L, + cholmod_common *Common + ) + int cholmod_l_updown( + int update, + cholmod_sparse *C, + cholmod_factor *L, + cholmod_common *Common + ) + + int cholmod_rowadd( + size_t k, + cholmod_sparse *R, + cholmod_factor *L, + cholmod_common *Common + ) + int cholmod_l_rowadd( + size_t k, + cholmod_sparse *R, + cholmod_factor *L, + cholmod_common *Common + ) + + int cholmod_rowdel( + size_t k, + cholmod_sparse *R, + cholmod_factor *L, + cholmod_common *Common + ) + int cholmod_l_rowdel( + size_t k, + cholmod_sparse *R, + cholmod_factor *L, + cholmod_common *Common + ) + + int cholmod_resymbol( + cholmod_sparse *A, + int *fset, + size_t fsize, + int pack, + cholmod_factor *L, + cholmod_common *Common + ) + int cholmod_l_resymbol( + cholmod_sparse *A, + int *fset, + size_t fsize, + int pack, + cholmod_factor *L, + cholmod_common *Common + ) + + int cholmod_resymbol_noperm( + cholmod_sparse *A, + int *fset, + size_t fsize, + int pack, + cholmod_factor *L, + cholmod_common *Common + ) + int cholmod_l_resymbol_noperm( + cholmod_sparse *A, + int *fset, + size_t fsize, + int pack, + cholmod_factor *L, + cholmod_common *Common + ) + + cholmod_sparse* cholmod_transpose( + cholmod_sparse *A, + int mode, + cholmod_common *Common + ) + cholmod_sparse* cholmod_l_transpose( + cholmod_sparse *A, + int mode, + cholmod_common *Common + ) + + cholmod_sparse *cholmod_submatrix( + cholmod_sparse *A, + int32_t *rset, + int64_t rsize, + int32_t *cset, + int64_t csize, + int mode, + int sorted, + cholmod_common *Common + ) + cholmod_sparse *cholmod_l_submatrix( + cholmod_sparse *A, + int64_t *rset, + int64_t rsize, + int64_t *cset, + int64_t csize, + int mode, + int sorted, + cholmod_common *Common + ) + + cholmod_sparse *cholmod_allocate_sparse( + size_t nrow, + size_t ncol, + size_t nzmax, + int sorted, + int packed, + int stype, + int xdtype, + cholmod_common *Common + ) + cholmod_sparse *cholmod_l_allocate_sparse( + size_t nrow, + size_t ncol, + size_t nzmax, + int sorted, + int packed, + int stype, + int xdtype, + cholmod_common *Common + ) + + void *cholmod_malloc(size_t n, size_t size, cholmod_common *Common) + void *cholmod_l_malloc(size_t n, size_t size, cholmod_common *Common) + + int cholmod_change_factor( + int to_xtype, + int to_ll, + int to_super, + int to_packed, + int to_monotonic, + cholmod_factor *L, + cholmod_common *Common + ) + int cholmod_l_change_factor( + int to_xtype, + int to_ll, + int to_super, + int to_packed, + int to_monotonic, + cholmod_factor *L, + cholmod_common *Common + ) + + cholmod_factor *cholmod_copy_factor( + cholmod_factor *L, + cholmod_common *Common + ) + cholmod_factor *cholmod_l_copy_factor( + cholmod_factor *L, + cholmod_common *Common + ) + + int cholmod_etree(cholmod_sparse *A, int32_t *Parent, cholmod_common *Common) + int cholmod_l_etree(cholmod_sparse *A, int64_t *Parent, cholmod_common *Common) + + int32_t cholmod_postorder( + int32_t *Parent, + size_t n, + int32_t *Weight, + int32_t *Post, + cholmod_common *Common + ) + int64_t cholmod_l_postorder( + int64_t *Parent, + size_t n, + int64_t *Weight, + int64_t *Post, + cholmod_common *Common + ) + + int cholmod_rowcolcounts( + cholmod_sparse *A, + int32_t *fset, + size_t fsize, + int32_t *Parent, + int32_t *Post, + int32_t *RowCount, + int32_t *ColCount, + int32_t *First, + int32_t *Level, + cholmod_common *Common + ) + int cholmod_l_rowcolcounts( + cholmod_sparse *A, + int64_t *fset, + size_t fsize, + int64_t *Parent, + int64_t *Post, + int64_t *RowCount, + int64_t *ColCount, + int64_t *First, + int64_t *Level, + cholmod_common *Common + ) + + int cholmod_row_subtree( + cholmod_sparse *A, + cholmod_sparse *F, + size_t krow, + int32_t *Parent, + cholmod_sparse *R, + cholmod_common *Common + ) + int cholmod_l_row_subtree( + cholmod_sparse *A, + cholmod_sparse *F, + size_t krow, + int64_t *Parent, + cholmod_sparse *R, + cholmod_common *Common + ) + + int64_t cholmod_bisect( + cholmod_sparse *A, + int32_t *fset, + size_t fsize, + int compress, + int32_t *Partition, + cholmod_common *Common + ) + int64_t cholmod_l_bisect( + cholmod_sparse *A, + int64_t *fset, + size_t fsize, + int compress, + int64_t *Partition, + cholmod_common *Common + ) + + int64_t cholmod_nested_dissection( + cholmod_sparse *A, + int32_t *fset, + size_t fsize, + int32_t *Perm, + int32_t *CParent, + int32_t *Cmember, + cholmod_common *Common + ) + int64_t cholmod_l_nested_dissection( + cholmod_sparse *A, + int64_t *fset, + size_t fsize, + int64_t *Perm, + int64_t *CParent, + int64_t *Cmember, + cholmod_common *Common + ) + + int cholmod_metis( + cholmod_sparse *A, + int32_t *fset, + size_t fsize, + int postorder, + int32_t *Perm, + cholmod_common *Common + ) + int cholmod_l_metis( + cholmod_sparse *A, + int64_t *fset, + size_t fsize, + int postorder, + int64_t *Perm, + cholmod_common *Common + ) + + int64_t cholmod_collapse_septree( + size_t n, + size_t ncomponents, + double nd_oksep, + size_t nd_small, + int32_t *CParent, + int32_t *Cmember, + cholmod_common *Common + ) + int64_t cholmod_l_collapse_septree( + size_t n, + size_t ncomponents, + double nd_oksep, + size_t nd_small, + int64_t *CParent, + int64_t *Cmember, + cholmod_common *Common + ) + + void *cholmod_free(size_t n, size_t size, void *p, cholmod_common *Common) + void *cholmod_l_free(size_t n, size_t size, void *p, cholmod_common *Common) + + int cholmod_free_sparse(cholmod_sparse **A, cholmod_common *Common) + int cholmod_l_free_sparse(cholmod_sparse **A, cholmod_common *Common) + + int cholmod_free_dense(cholmod_dense **A, cholmod_common *Common) + int cholmod_l_free_dense(cholmod_dense **A, cholmod_common *Common) + + int cholmod_free_factor(cholmod_factor **L, cholmod_common *Common) + int cholmod_l_free_factor(cholmod_factor **L, cholmod_common *Common) + + +# ------------------------------------------------------------------------------------- +# Interface Declarations +# ------------------------------------------------------------------------------------- +ctypedef fused index_t: + int32_t + int64_t + +ctypedef fused floating_t: + float + double + float complex + double complex + +cdef object _csc_from_cholmod_sparse(cholmod_sparse* A, cholmod_common* common) +cdef void _cholmod_dense_from_ndarray(floating_t[::1, :] Xd, cholmod_dense *X_static) +cdef cnp.ndarray _ndarray_from_cholmod_dense( + cholmod_dense* X, bint use_int32, cholmod_common* common +) +cdef cnp.ndarray _ndarray_copy_from_intptr(void* ptr, size_t N, bint use_int32) +cdef void _copy_cholmod_common(cholmod_common* dest, cholmod_common* src) diff --git a/src/sksparse/cholmod.pyx b/src/sksparse/cholmod.pyx new file mode 100644 index 00000000..8464f986 --- /dev/null +++ b/src/sksparse/cholmod.pyx @@ -0,0 +1,3968 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2008-2025 The scikit-sparse developers. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: cholmod.pyx +# Created: 2025-08-11 14:49 +# ============================================================================= + +""" +================================================ +Cholesky Decomposition (:mod:`sksparse.cholmod`) +================================================ + +.. currentmodule:: sksparse.cholmod + +.. versionadded:: 0.1.0 + +.. versionchanged:: 0.5.0 + Major API updates to more closely resemble the :func:`scipy.linalg.cholesky` + dense interface, and incorporate more functions from the CHOLMOD MATLAB + interface. + + +An interface to the SuiteSparse `CHOLMOD +`_ +package, which computes basic linear algebra operations for sparse, symmetric, +positive-definite matrices. + + +Function Interface +------------------ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + cholesky - Computes the Cholesky factorization of a sparse matrix. + ldl - Computes the LDL.T factorization of a sparse matrix. + + +Object Interface +---------------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + cho_factor - Computes the Cholesky factorization of a sparse matrix. + ldl_factor - Computes the LDL.T factorization of a sparse matrix. + CholeskyFactor - Class representing a Cholesky factorization. + + +Symbolic Analysis +----------------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + symbfact - Computes the symbolic factorization of a sparse matrix. + etree - Computes the elimination tree of a sparse matrix. + + +Graph Partitioning +------------------ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + bisect - Bisects a graph using nested dissection. + metis - Computes a fill-reducing ordering using METIS. + nesdis - Computes a fill-reducing ordering using NESDIS. + SeparatorTree - Class representing a separator tree. + + +.. _cholmod-exceptions: + +Exceptions and Warnings +----------------------- + +.. autosummary:: + :toctree: generated/ + + CholmodWarning + CholmodSmallDiagonalWarning + + CholmodError + CholmodNotPositiveDefiniteError + CholmodNotInstalledError + CholmodOutOfMemoryError + CholmodOverflowError + CholmodInvalidInputError + CholmodGpuProblemError + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse CHOLMOD `_ +""" + +from cpython.ref cimport Py_INCREF +cimport cython +cimport numpy as cnp + +import numpy as np + +from scipy.sparse import csc_array, diags_array, eye_array, issparse +import warnings + +from .utils import validate_csc_input + +__all__ = [ + "CholeskyFactor", + "CholmodError", + "CholmodGpuProblemError", + "CholmodInvalidInputError", + "CholmodNotInstalledError", + "CholmodNotPositiveDefiniteError", + "CholmodOutOfMemoryError", + "CholmodOverflowError", + "CholmodSmallDiagonalWarning", + "CholmodWarning", + "SeparatorTree", + "bisect", + "cho_factor", + "cholesky", + "etree", + "ldl", + "ldl_factor", + "metis", + "nesdis", + "symbfact", +] + + +# Define constants for the mode of cholmod_transpose (see cholmod.h) +cdef int CHOLMOD_TRANS_PATTERN = 0 # transpose only the pattern +cdef int CHOLMOD_TRANS_NOCONJ = 1 # numeric (no conjugate) +cdef int CHOLMOD_TRANS_CONJ = 2 # numeric (conjugate transpose) + + +# ----------------------------------------------------------------------------- +# Error Handling +# ----------------------------------------------------------------------------- +class CholmodError(Exception): + """Base class for CHOLMOD-related errors.""" + pass + + +class CholmodNotPositiveDefiniteError(CholmodError): + """Raised when the input matrix is not positive definite.""" + pass + + +class CholmodNotInstalledError(CholmodError): + """Raised when the CHOLMOD library is not installed.""" + pass + + +class CholmodOutOfMemoryError(CholmodError): + """Raised when CHOLMOD runs out of memory.""" + pass + + +class CholmodOverflowError(CholmodError): + """Raised when CHOLMOD encounters an integer overflow.""" + pass + + +class CholmodInvalidInputError(CholmodError): + """Raised when CHOLMOD receives invalid input.""" + pass + + +class CholmodGpuProblemError(CholmodError): + """Raised when CHOLMOD encounters a problem with CUDA.""" + pass + + +class CholmodWarning(Warning): + """Base class for CHOLMOD-related warnings.""" + pass + + +class CholmodSmallDiagonalWarning(CholmodWarning): + """Warning for small diagonal entries.""" + pass + + +# Known Errors +cdef dict _ERROR_MAP = { + CHOLMOD_NOT_INSTALLED: ( + CholmodNotInstalledError, + "CHOLMOD library is not installed or not found." + ), + CHOLMOD_OUT_OF_MEMORY: ( + CholmodOutOfMemoryError, + "CHOLMOD ran out of memory." + ), + CHOLMOD_TOO_LARGE: ( + CholmodOverflowError, + "CHOLMOD encountered an integer overflow." + ), + CHOLMOD_INVALID: ( + CholmodInvalidInputError, + "CHOLMOD received invalid input." + ), + CHOLMOD_GPU_PROBLEM: ( + CholmodGpuProblemError, + "CHOLMOD encountered a problem with CUDA." + ), + CHOLMOD_NOT_POSDEF: ( + CholmodNotPositiveDefiniteError, + "Input matrix is not positive definite." + ), + CHOLMOD_DSMALL: ( + CholmodSmallDiagonalWarning, + "A diagonal entry is very small, which may lead to numerical instability." + ), +} + + +cdef int _handle_errors(int status, minor=None) except -1 with gil: + """Handle CHOLMOD errors by raising Python exceptions or warnings. + + This function should be called with cholmod_common->status after any + CHOLMOD C function that may fail. + + .. note:: + + It is not a safe practice to pass a function like this as the + "error_handler" member of the cholmod_common struct, because CHOLMOD + may call it from C code that does not hold the Python GIL. + + Parameters + ---------- + status : int + The CHOLMOD status code, from the cholmod_common.status field. + minor : int, optional + The column index that caused the error, if applicable. + + Returns + ------- + None + + Raises + ------ + :exc:`CholmodWarning` + Raises a warning for non-critical issues. + :exc:`CholmodError` or subclass + Raises an appropriate Python exception based on the CHOLMOD status code. + """ + if status == CHOLMOD_OK: + return 0 + + status_msg = f"(code {status:d})" + + # Fallback to generic error for unknown codes + exc_class, msg = _ERROR_MAP.get(status, CholmodError) + full_msg = msg + " " + status_msg + + if minor is not None: + full_msg += f" Failed at column {minor}." + + if issubclass(exc_class, Warning): + warnings.warn(full_msg, exc_class, stacklevel=2) + else: + raise exc_class(full_msg) + + +# ----------------------------------------------------------------------------- +# CSC <==> CHOLMOD Sparse +# ----------------------------------------------------------------------------- +cdef inline int _single_or_double(floating_t _=0) noexcept: + """Return the CHOLMOD dtype number for a given NumPy dtype.""" + if floating_t is float or floating_t is cython.floatcomplex: + return CHOLMOD_SINGLE + else: + return CHOLMOD_DOUBLE + + +cdef inline int _real_or_complex(floating_t _=0) noexcept: + """Return the CHOLMOD xtype number for a given NumPy dtype.""" + if floating_t is float or floating_t is double: + return CHOLMOD_REAL + else: + return CHOLMOD_COMPLEX + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _cholmod_sparse_from_csc( + tuple shape not None, + index_t[::1] indptr not None, + index_t[::1] indices not None, + floating_t[::1] data not None, + int stype, + uintptr_t A_static, # cholmod_sparse* as an int +): + """Create a CHOLMOD sparse matrix from a scipy.sparse.csc_array. + + See the CHOLMOD MATLAB interface for details [#sputil_get_sparse]_. + + Parameters + ---------- + shape : (2,) tuple of int + The shape of the CSC matrix. + indptr : (N+1,) array of index_t + The column pointer array of the CSC matrix. + indices : (nnz,) array of index_t + The row indices of the non-zero entries of the matrix. + data : (nnz,) array of floating_t + The numerical values of the non-zero entries of the matrix. + stype : int + The assumed symmetry type of ``A``: + * -1: lower triangular, + * 0: unsymmetric, + * 1: upper triangular. + A_static : cholmod_sparse* + Pointer to a preallocated CHOLMOD sparse matrix structure. Contents + need not be initialized. Contains the CHOLMOD sparse matrix on output. + + Returns + ------- + res : csc_array + A reference to ``A``. There is no use for the output of this + function, except to keep the underlying data from being garbage + collected until the cholmod_sparse object is freed. + + References + ---------- + .. [#sputil_get_sparse] ``sputil2.c`` - CHOLMOD MATLAB utilities + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/sputil2.c + """ + # Initialize the CHOLMOD sparse matrix + cdef cholmod_sparse* A = A_static + memset(A, 0, sizeof(cholmod_sparse)) + + assert len(shape) == 2 + + # Set the matrix dimensions and properties + A.nrow = shape[0] + A.ncol = shape[1] + A.nzmax = indices.shape[0] + A.packed = True + A.sorted = True # NOTE requires input indices to be sorted + A.itype = CHOLMOD_INT if index_t is int32_t else CHOLMOD_LONG + A.stype = -1 if stype < 0 else (0 if stype == 0 else 1) + A.dtype = _single_or_double[floating_t]() + A.z = NULL + + # Declare dummy variables to avoid empty array issues + cdef index_t dummy_index + cdef floating_t dummy_value + + # Create the index arrays + A.p = &indptr[0] # always has size > 0 for a valid csc_array + A.i = &indices[0] if indices.size > 0 else &dummy_index + + # Get the numerical values of A + A.xtype = _real_or_complex[floating_t]() + A.x = &data[0] if data.size > 0 else &dummy_value + + +cdef class _CholmodSparseDestructor: + """A destructor for CHOLMOD sparse matrices. + + This class is used as a base for NumPy arrays that are views on CHOLMOD + sparse matrices. It ensures that the CHOLMOD sparse matrix is properly + freed when the NumPy array is no longer in use. + + Attributes + ---------- + _sparse : cholmod_sparse* + The CHOLMOD sparse matrix to be freed. + _common : cholmod_common* + The CHOLMOD common structure used for memory management. + """ + + cdef cholmod_sparse* _sparse + cdef cholmod_common* _common + + # NOTE __cinit__ is a *Python*-level constructor, cannot take C pointers + cdef void init(self, cholmod_sparse* A, cholmod_common* common): + assert A is not NULL + assert common is not NULL + self._sparse = A + self._common = common + + def __dealloc__(self): + if self._sparse.itype == CHOLMOD_INT: + cholmod_free_sparse(&self._sparse, self._common) + else: + cholmod_l_free_sparse(&self._sparse, self._common) + + +cdef inline int _np_itypenum_from_cholmod(int itype) noexcept: + """Get the NumPy typenum corresponding to the given CHOLMOD itype. + + Parameters + ---------- + itype : int + The CHOLMOD itype of the matrix. + + Returns + ------- + np_itypenum : int + The corresponding NumPy typenum. + """ + return cnp.NPY_INT32 if itype == CHOLMOD_INT else cnp.NPY_INT64 + + +cdef inline int _np_dtypenum_from_cholmod(int xtype, int dtype) noexcept: + """Get the NumPy typenum corresponding to the given CHOLMOD xtype and dtype. + + Parameters + ---------- + xtype : int + The CHOLMOD xtype of the matrix. + dtype : int + The CHOLMOD dtype of the matrix. + + Returns + ------- + np_dtypenum : int + The corresponding NumPy typenum. + """ + if xtype == CHOLMOD_REAL: + return cnp.NPY_FLOAT32 if dtype == CHOLMOD_SINGLE else cnp.NPY_FLOAT64 + elif xtype == CHOLMOD_COMPLEX: + return cnp.NPY_COMPLEX64 if dtype == CHOLMOD_SINGLE else cnp.NPY_COMPLEX128 + elif xtype == CHOLMOD_PATTERN: + return cnp.NPY_BOOL + else: + return cnp.NPY_OBJECT + + +cdef object _csc_from_cholmod_sparse(cholmod_sparse* A, cholmod_common* common): + """Create a csc_array that is a view onto a cholmod_sparse object. + + Parameters + ---------- + A : cholmod_sparse* + A pointer to the CHOLMOD sparse matrix to convert to a csc_array. + common : cholmod_common* + A pointer to the CHOLMOD common structure used for memory management. + + Returns + ------- + res : csc_array + A scipy.sparse.csc_array that is a view onto the CHOLMOD sparse matrix. + The array has a base with a destructor that frees the CHOLMOD sparse + matrix when the array is no longer in use. + """ + cdef int np_itypenum = _np_itypenum_from_cholmod(A.itype) + cdef int np_dtypenum = _np_dtypenum_from_cholmod(A.xtype, A.dtype) + + # convert to NumPy arrays + indptr = cnp.PyArray_SimpleNewFromData(1, [A.ncol + 1], np_itypenum, A.p) + indices = cnp.PyArray_SimpleNewFromData(1, [A.nzmax], np_itypenum, A.i) + data = cnp.PyArray_SimpleNewFromData(1, [A.nzmax], np_dtypenum, A.x) + + # Take ownership of the data + cdef _CholmodSparseDestructor base = _CholmodSparseDestructor() + base.init(A, common) + + # NOTE need to increment reference count of base manually for each array, + # since the PyArray_SetBaseObject steals a reference. + for arr in (indptr, indices, data): + Py_INCREF(base) + if cnp.PyArray_SetBaseObject(arr, base) < 0: + raise MemoryError("Failed to set base object for array.") + + return csc_array((data, indices, indptr), shape=(A.nrow, A.ncol)) + + +cdef object _csc_view_from_cholmod_factor(CholeskyFactor py_factor, object ldl=None): + """Create a sparse matrix from a CHOLMOD factor. + + This function is similar to _csc_from_cholmod_sparse, but builds the matrix + directly from the factor, without the intermediate cholmod_factor_to_sparse + call. + + Parameters + ---------- + py_factor : CholeskyFactor + The input cholmod_factor and cholmod_common objects, wrapped in + a Python object. + ldl : None or bool, optional + If True, return the LDL.T form, otherwise return the LL.T form. Default + is to use the form of the existing factor ``py_factor._factor.is_ll``. + + Returns + ------- + res : csc_array + L scipy.sparse.csc_array that is a view onto the CHOLMOD factor. This + array is *read-only*, so attempts to modify it will raise an error. + + Notes + ----- + The ``cholmod_factor_to_sparse`` function moves the memory from the + ``cholmod_factor`` to the newly-created ``cholmod_sparse`` struct, and sets + the ``xtype`` of the factor to ``CHOLMOD_PATTERN``. This behavior is fine + for standalone functions that return a matrix and no longer need the + factor. For our :obj:`CholeskyFactor` class, however, we need to keep the + factor intact for future updates or conversions to LL or LDL formats. + Therefore, we use this function to create a view onto the factor without + destroying it. + """ + cdef cholmod_factor *L = py_factor._factor + cdef cholmod_common *common = py_factor._cm + + if L is NULL: + raise ValueError("The factor pointer is NULL.") + + if L.xtype == CHOLMOD_PATTERN: + raise ValueError("The factor has no numerical values.") + + # Ensure the factor is in simplicial, packed, monotonic format + cdef int to_ll = L.is_ll if ldl is None else not ldl + cdef int to_super = False # simplicial format + cdef int to_packed = True + cdef int to_monotonic = True + + if L.itype == CHOLMOD_INT: + cholmod_change_factor( + L.xtype, to_ll, to_super, to_packed, to_monotonic, L, common + ) + else: + cholmod_l_change_factor( + L.xtype, to_ll, to_super, to_packed, to_monotonic, L, common + ) + + _handle_errors(common.status) + + # Create numpy arrays + cdef int np_itypenum = _np_itypenum_from_cholmod(L.itype) + cdef int np_dtypenum = _np_dtypenum_from_cholmod(L.xtype, L.dtype) + + indptr = cnp.PyArray_SimpleNewFromData(1, [L.n + 1], np_itypenum, L.p) + indices = cnp.PyArray_SimpleNewFromData(1, [L.nzmax], np_itypenum, L.i) + data = cnp.PyArray_SimpleNewFromData(1, [L.nzmax], np_dtypenum, L.x) + + # NOTE need to increment reference count of base manually for each array, + # since the PyArray_SetBaseObject steals a reference. + # Take ownership of the data + for arr in (indptr, indices, data): + Py_INCREF(py_factor) + if cnp.PyArray_SetBaseObject(arr, py_factor) < 0: + raise MemoryError("Failed to set base object for array.") + cnp.PyArray_CLEARFLAGS(arr, cnp.NPY_ARRAY_WRITEABLE) # make read-only + + return csc_array((data, indices, indptr), shape=(L.n, L.n)) + + +cdef cholmod_sparse* _cholesky_pattern( + cholmod_sparse *A, + cholmod_sparse *F, + size_t N, + int32_t *Parent, + int32_t *ColCount, + bint col_etree, + cholmod_common *cm +): + """Compute the Cholesky pattern from the given matrices. + + Parameters + ---------- + A, F : cholmod_sparse* + Pointers to the sparse matrices to analyze. + N : size_t + The number of rows or columns in A. + Parent : int32_t* + Pointer to the array of the elimination tree. + ColCount : int32_t* + Pointer to the array of column counts of the Cholesky factor. + col_etree : bint + If True, analyze the column case F @ F.T. Otherwise, determine the case + from ``A->stype``. + cm : cholmod_common* + Pointer to a CHOLMOD common structure for configuration and status. + + Returns + ------- + L : cholmod_sparse* + A pointer to the array containing the pattern of the Cholesky factor. + """ + if A is NULL or F is NULL or cm is NULL: + raise ValueError("Input pointer is NULL.") + + cdef cholmod_sparse *A_in = NULL + cdef cholmod_sparse *F_in = NULL + + if A.stype == 1: + A_in = A + elif A.stype == -1: + A_in = F + elif col_etree: + # column case: analyze F @ F.T + A_in = F + F_in = A + else: + # row case: analyze A @ A.T + A_in = A + F_in = F + + # Count the total number of entries in L + cdef int32_t lnz = 0 + cdef size_t j + + for j in range(N): + lnz += ColCount[j] + + # Initialize the CHOLMOD sparse matrix for L + cdef cholmod_sparse *L = cholmod_allocate_sparse( + N, N, lnz, True, True, 0, CHOLMOD_PATTERN, cm + ) + + cdef int32_t *Lp = L.p + cdef int32_t *Li = L.i + + # Initialize column pointers + lnz = 0 + + for j in range(N): + Lp[j] = lnz + lnz += ColCount[j] + + Lp[N] = lnz + + # Create a copy of the column pointers + cdef int32_t *W = cholmod_malloc(N, sizeof(int32_t), cm) + memcpy(W, Lp, N * sizeof(int32_t)) + + # Get workspace for computing one row of L + cdef cholmod_sparse *R = cholmod_allocate_sparse( + N, 1, N, False, True, 0, CHOLMOD_PATTERN, cm + ) + + cdef int32_t *Rp = R.p + cdef int32_t *Ri = R.i + cdef size_t k + cdef size_t p + cdef size_t idx + + for k in range(N): + # Get the kth row of L and store in the columns of L + cholmod_row_subtree(A_in, F_in, k, Parent, R, cm) + + for p in range(Rp[1]): + idx = W[Ri[p]] + Li[idx] = k + W[Ri[p]] += 1 + + # Add the diagonal entry + idx = W[k] + Li[idx] = k + W[k] += 1 + + # Free the workspace + cholmod_free(N, sizeof(int32_t), W, cm) + cholmod_free_sparse(&R, cm) + + return L + + +cdef cholmod_sparse* _cholesky_l_pattern( + cholmod_sparse *A, + cholmod_sparse *F, + size_t N, + int64_t *Parent, + int64_t *ColCount, + bint col_etree, + cholmod_common *cm +): + """Compute the Cholesky pattern from the given matrices. + + Parameters + ---------- + A, F : cholmod_sparse* + Pointers to the sparse matrices to analyze. + N : size_t + The number of rows or columns in A. + Parent : int64_t* + Pointer to the array of the elimination tree. + ColCount : int64_t* + Pointer to the array of column counts of the Cholesky factor. + col_etree : bint + If True, analyze the column case F @ F.T. Otherwise, determine the case + from ``A->stype``. + cm : cholmod_common* + Pointer to a CHOLMOD common structure for configuration and status. + + Returns + ------- + L : cholmod_sparse* + A pointer to the array containing the pattern of the Cholesky factor. + """ + if A is NULL or F is NULL or cm is NULL: + raise ValueError("Input pointer is NULL.") + + cdef cholmod_sparse *A_in = NULL + cdef cholmod_sparse *F_in = NULL + + if A.stype == 1: + A_in = A + elif A.stype == -1: + A_in = F + elif col_etree: + # column case: analyze F @ F.T + A_in = F + F_in = A + else: + # row case: analyze A @ A.T + A_in = A + F_in = F + + # Count the total number of entries in L + cdef int64_t lnz = 0 + cdef size_t j + + for j in range(N): + lnz += ColCount[j] + + # Initialize the CHOLMOD sparse matrix for L + cdef cholmod_sparse *L = cholmod_l_allocate_sparse( + N, N, lnz, True, True, 0, CHOLMOD_PATTERN, cm + ) + + cdef int64_t *Lp = L.p + cdef int64_t *Li = L.i + + # Initialize column pointers + lnz = 0 + + for j in range(N): + Lp[j] = lnz + lnz += ColCount[j] + + Lp[N] = lnz + + # Create a copy of the column pointers + cdef int64_t *W = cholmod_l_malloc(N, sizeof(int64_t), cm) + memcpy(W, Lp, N * sizeof(int64_t)) + + # Get workspace for computing one row of L + cdef cholmod_sparse* R = cholmod_l_allocate_sparse( + N, 1, N, False, True, 0, CHOLMOD_PATTERN, cm + ) + + cdef int64_t *Rp = R.p + cdef int64_t *Ri = R.i + cdef size_t k, p, idx + + for k in range(N): + # Get the kth row of L and store in the columns of L + cholmod_l_row_subtree(A_in, F_in, k, Parent, R, cm) + + for p in range(Rp[1]): + idx = W[Ri[p]] + Li[idx] = k + W[Ri[p]] += 1 + + # Add the diagonal entry + idx = W[k] + Li[idx] = k + W[k] += 1 + + # Free the workspace + cholmod_l_free_sparse(&R, cm) + + return L + + +# ----------------------------------------------------------------------------- +# CSC <==> CHOLMOD Dense +# ----------------------------------------------------------------------------- +cdef void _cholmod_dense_from_ndarray( + floating_t[::1, :] Xd, + cholmod_dense *X_static +) noexcept: + """Create a CHOLMOD dense matrix from a numpy.ndarray. + + See the CHOLMOD MATLAB interface for details [#sputil_get_dense]_. + + Parameters + ---------- + Xd : (M, N) ndarray + The input dense matrix. Must be 2-dimensional in column-major (Fortran) + order. + X_static : cholmod_sparse* + Pointer to a preallocated CHOLMOD sparse matrix structure. Contents + need not be initialized. Contains the CHOLMOD sparse matrix on output. + + Returns + ------- + res : ndarray + A reference to the memoryview ``Xd``. There is no use for the output of + this function, except to keep the underlying data from being garbage + collected until the cholmod_dense object is freed. + + References + ---------- + .. [#sputil_get_dense] ``sputil2.c`` - CHOLMOD MATLAB utilities + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/sputil2.c + """ + # Initialize the CHOLMOD dense matrix + cdef cholmod_dense* X = X_static + memset(X, 0, sizeof(cholmod_dense)) + + X.nrow = Xd.shape[0] + X.ncol = Xd.shape[1] + X.d = X.nrow + X.nzmax = X.nrow * X.ncol + X.dtype = _single_or_double[floating_t]() + X.z = NULL + + # Get the numerical values of X + cdef floating_t dummy_value + X.xtype = _real_or_complex[floating_t]() + X.x = &Xd[0, 0] if Xd.size > 0 else &dummy_value + + +cdef class _CholmodDenseDestructor: + """A destructor for CHOLMOD dense matrices. + + This class is used as a base for NumPy arrays that are views on CHOLMOD + dense matrices. It ensures that the CHOLMOD dense matrix is properly + freed when the NumPy array is no longer in use. + + Attributes + ---------- + _dense : cholmod_dense* + The CHOLMOD dense matrix to be freed. + _use_int32 : bint + Whether to use 32-bit or 64-bit integers. + _common : cholmod_common* + The CHOLMOD common structure used for memory management. + """ + + cdef cholmod_dense* _dense + cdef cholmod_common* _common + cdef bint _use_int32 + + cdef void init(self, cholmod_dense* A, bint use_int32, cholmod_common* common): + assert A is not NULL + assert common is not NULL + self._dense = A + self._common = common + self._use_int32 = use_int32 + + def __dealloc__(self): + if self._use_int32: + cholmod_free_dense(&self._dense, self._common) + else: + cholmod_l_free_dense(&self._dense, self._common) + + +cdef cnp.ndarray _ndarray_from_cholmod_dense( + cholmod_dense* X, bint use_int32, cholmod_common* common +): + """Create a numpy.ndarray that is a view onto a cholmod_dense object. + + Parameters + ---------- + X : cholmod_dense* + The CHOLMOD dense matrix to convert to a NumPy array. + use_int32 : bint + Whether to use 32-bit or 64-bit integers. + common : cholmod_common* + The CHOLMOD common structure used for memory management. + + Returns + ------- + res : ndarray + A NumPy array that is a view onto the CHOLMOD dense matrix. The array + has a base with a destructor that frees the CHOLMOD dense matrix when + the array is no longer in use. + """ + cdef int np_dtypenum = _np_dtypenum_from_cholmod(X.xtype, X.dtype) + + # convert to NumPy array + arr = cnp.PyArray_SimpleNewFromData(1, [X.nrow * X.ncol], np_dtypenum, X.x) + + # set destructor and check if writeable + cdef _CholmodDenseDestructor base = _CholmodDenseDestructor() + base.init(X, use_int32, common) + + # NOTE need to increment reference count of base manually, since the + # PyArray_SetBaseObject steals a reference. + Py_INCREF(base) + if cnp.PyArray_SetBaseObject(arr, base) < 0: + raise MemoryError("Failed to set base object for array.") + + # Cholmod dense matrices are stored in column-major order, so reshape + return arr.reshape((X.nrow, X.ncol), order="F") + + +cdef cnp.ndarray _ndarray_copy_from_intptr(void* ptr, size_t N, bint use_int32): + """Create a NumPy array from a pointer to an integer array. + + Parameters + ---------- + ptr : void* + A pointer to the C array. + N : size_t + The size of the vector. + use_int32 : bool + Whether to use 32-bit or 64-bit integers for the array indices. + + Returns + ------- + p : ndarray + A copy of the vector as a NumPy array. + """ + if ptr is NULL: + raise ValueError("ptr is NULL, cannot get array") + + cdef int np_itypenum = cnp.NPY_INT32 if use_int32 else cnp.NPY_INT64 + p = cnp.PyArray_SimpleNewFromData(1, [N], np_itypenum, ptr) + return p.copy() # return a copy in case ptr is freed + + +cdef cnp.ndarray _ndarray_int_view_from_factor( + void* ptr, size_t N, CholeskyFactor py_factor +): + """Create a NumPy array from the an integer vector in a CHOLMOD factor. + + Parameters + ---------- + ptr : void* + A pointer to the C array, *e.g.* ``cholmod_factor.Perm``. + N : size_t + The size of the vector. + py_factor : CholeskyFactor + The CholeskyFactor object from which to extract the permutation. + + Returns + ------- + p : ndarray + The permutation vector as a NumPy array. This array is *read-only*, + so attempts to modify it will raise an error. + """ + cdef cholmod_factor *L = py_factor._factor + + if L is NULL: + raise ValueError("The factor pointer is NULL.") + + if ptr is NULL: + raise ValueError("The input pointer is NULL.") + + cdef int np_itypenum = _np_itypenum_from_cholmod(L.itype) + p = cnp.PyArray_SimpleNewFromData(1, [N], np_itypenum, ptr) + + # NOTE need to increment reference count of base manually, + # since the PyArray_SetBaseObject steals a reference. + Py_INCREF(py_factor) + if cnp.PyArray_SetBaseObject(p, py_factor) < 0: + raise MemoryError("Failed to set base object for array.") + cnp.PyArray_CLEARFLAGS(p, cnp.NPY_ARRAY_WRITEABLE) # set to be read-only + + return p + + +# ----------------------------------------------------------------------------- +# Utilities +# ----------------------------------------------------------------------------- +cdef dict _supernodal_modes = { + "auto": CHOLMOD_AUTO, + "simplicial": CHOLMOD_SIMPLICIAL, + "supernodal": CHOLMOD_SUPERNODAL, +} + + +cdef dict _ordering_methods = { + "default": None, + "best": None, + "natural": CHOLMOD_NATURAL, + "given": CHOLMOD_GIVEN, + "amd": CHOLMOD_AMD, + "metis": CHOLMOD_METIS, + "nesdis": CHOLMOD_NESDIS, + "colamd": CHOLMOD_COLAMD, + "postordered": CHOLMOD_POSTORDERED, +} + + +cdef dict _ordering_methods_inv = { + v: k for k, v in _ordering_methods.items() if v is not None +} + + +cdef void _set_ordering_method(object order, cholmod_common* cm): + """Set the ordering method in the CHOLMOD common struct. + + This function sets the values of ``cm->nmethods``, and possibly + ``cm->method[0].ordering`` and ``cm->postorder``. + + Parameters + ---------- + order : None or str in {"default", "best", "natural", "metis", \ + "nesdis", "amd", "colamd", "postordered"} + The desired ordering method. + cm : cholmod_common* + Pointer to a CHOLMOD common structure for configuration and status. + Contains the ordering method on output. + """ + if order == "default": + cm.nmethods = 0 + elif order == "best": + cm.nmethods = CHOLMOD_MAXMETHODS + else: + # CHOLMOD_POSTORDERED is not an input, but an output flag. We treat it + # as "natural" + postordering, per cholmod.h description. + ordering = "natural" if (order is None or order == "postordered") else order + cm.nmethods = 1 + cm.method[0].ordering = _ordering_methods.get(ordering, CHOLMOD_NATURAL) + cm.postorder = ( + order == "postordered" + or ordering not in ["natural", "given"] + ) + + +cdef inline object _npitype_class_from_iype(int itype): + """Get the NumPy integer dtype class corresponding to the given CHOLMOD itype. + + Parameters + ---------- + itype : int + The CHOLMOD itype of the matrix. + + Returns + ------- + np_itype_class : type + The corresponding NumPy integer dtype class. + """ + return np.int32 if itype == CHOLMOD_INT else np.int64 + + +cdef inline object _npdtype_class_from_xdtype(int xtype, int dtype): + """Get the NumPy dtype class corresponding to the given CHOLMOD xtype and dtype. + + Parameters + ---------- + xtype : int + The CHOLMOD xtype of the matrix. + dtype : int + The CHOLMOD dtype of the matrix. + + Returns + ------- + np_dtype_class : type + The corresponding NumPy dtype class. + """ + if xtype == CHOLMOD_REAL: + return np.float32 if dtype == CHOLMOD_SINGLE else np.float64 + elif xtype == CHOLMOD_COMPLEX: + return np.complex64 if dtype == CHOLMOD_SINGLE else np.complex128 + elif xtype == CHOLMOD_PATTERN: + return np.bool_ + else: + return object + + +# ----------------------------------------------------------------------------- +# CholeskyFactor Object +# ----------------------------------------------------------------------------- +cdef void _copy_cholmod_common(cholmod_common* dest, cholmod_common* src): + """Copy the contents of one cholmod_common struct to another.""" + assert dest is not NULL + assert src is not NULL + + # Copy known input fields, ignore others + dest.supernodal = src.supernodal + dest.quick_return_if_not_posdef = src.quick_return_if_not_posdef + + # Ordering + dest.nmethods = src.nmethods + dest.current = src.current + dest.selected = src.selected + + cdef int i + if src.method is not NULL: + for i in range(src.nmethods): + dest.method[i].lnz = src.method[i].lnz + dest.method[i].fl = src.method[i].fl + dest.method[i].prune_dense = src.method[i].prune_dense + dest.method[i].prune_dense2 = src.method[i].prune_dense2 + dest.method[i].nd_oksep = src.method[i].nd_oksep + dest.method[i].nd_small = src.method[i].nd_small + dest.method[i].aggressive = src.method[i].aggressive + dest.method[i].order_for_lu = src.method[i].order_for_lu + dest.method[i].nd_compress = src.method[i].nd_compress + dest.method[i].nd_camd = src.method[i].nd_camd + dest.method[i].nd_components = src.method[i].nd_components + dest.method[i].ordering = src.method[i].ordering + + dest.postorder = src.postorder + dest.itype = src.itype + + # Output Statistics + dest.status = src.status + dest.fl = src.fl + dest.lnz = src.lnz + dest.anz = src.anz + dest.modfl = src.modfl + dest.malloc_count = src.malloc_count + dest.memory_usage = src.memory_usage + dest.memory_inuse = src.memory_inuse + dest.nrealloc_col = src.nrealloc_col + dest.nrealloc_factor = src.nrealloc_factor + dest.ndbounds_hit = src.ndbounds_hit + dest.nsbounds_hit = src.nsbounds_hit + dest.rowfacfl = src.rowfacfl + dest.aatfl = src.aatfl + dest.called_nd = src.called_nd + dest.blas_ok = src.blas_ok + + dest.SPQR_grain = src.SPQR_grain + dest.SPQR_small = src.SPQR_small + dest.SPQR_shrink = src.SPQR_shrink + dest.SPQR_nthreads = src.SPQR_nthreads + + dest.SPQR_flopcount = src.SPQR_flopcount + dest.SPQR_analyze_time = src.SPQR_analyze_time + dest.SPQR_factorize_time = src.SPQR_factorize_time + dest.SPQR_solve_time = src.SPQR_solve_time + dest.SPQR_flopcount_bound = src.SPQR_flopcount_bound + dest.SPQR_tol_used = src.SPQR_tol_used + dest.SPQR_norm_E_fro = src.SPQR_norm_E_fro + + if src.SPQR_istat is not NULL: + for i in range(8): + dest.SPQR_istat[i] = src.SPQR_istat[i] + + # Skip GPU related fields + + +cdef class CholeskyFactor: + """The main object used for creating and manipulating a Cholesky factor. + + The constructor computes the symbolic analysis of the matrix and + determines a fill-reducing ordering (if ``order`` is not ``None`` or + ``"natural"``) such that: + + .. math :: + + L L^{\\top} = P A P^{\\top}. + + The numeric factorization is not computed until :meth:`.factorize` is + called. + + Parameters + ---------- + A : (N, N) array_like or sparse array + An array convertible to a sparse matrix in Compressed Sparse Column + (CSC) format. The matrix must be square and symmetric positive + definite. Only the upper or lower triangular part of the matrix is + used, and no check is made for symmetry. + sym_kind : str in {"sym", "row", "col"}, optional + The type of factorization for which to analyze the matrix: + + * ``sym``: Symmetric factorization. No check is made for symmetry. + * ``row``: Unsymmetric factorization of :math:`A A^{\\top}`. + * ``col``: Unsymmetric factorization of :math:`A^{\\top} A`. + + supernodal_mode : str in {"auto", "simplicial", "supernodal"}, optional + The type of factorization to use: + + * ``auto``: Automatically select the factorization type. + * ``simplicial``: Use a simplicial factorization. + * ``supernodal``: Use a supernodal factorization. + + Default is ``auto``. This mode also applies to any subsequent calls to + :meth:`.factorize`. Note that the ``simplicial`` mode may be slow for + large matrices. + + lower : bool, optional + If True, use the lower triangular part of ``A``. + order : str in {"default", "best", "natural", "metis", \ + "nesdis", "amd", "colamd", "postordered"}, optional + The permutation algorithm to use for the factorization. By default, + the natural ordering of the input matrix is used. The other options + are: + + * ``default``: Use the default method, which first tries AMD, then METIS. + * ``best``: Automatically select the best ordering based on the input. + * ``metis``: Use the METIS library for graph partitioning. + * ``nesdis``: Use the NESDIS library for nested dissection. + * ``amd``: Use the Approximate Minimum Degree (AMD) algorithm. + * ``colamd``: Use the Approximate Minimum Degree (AMD) algorithm + for the symmetric case, or the COLAMD algorithm for the + unsymmetric case (:math:`A A^{{\\top}}` or :math:`A^{{\\top}} A`). + * ``postordered``: Use natural ordering followed by postordering. + + By default, methods other than ``natural`` will also be + postordered. + + .. warning:: + + The ordering method ``best`` may be quite slow for large + matrices, but if the factorization is reused many times, it can + be worth it. + + Attributes + ---------- + N : int + The number of rows and columns in the factor. + is_ll : bool + Whether the factor is in ``LL.T`` form (True) or ``LDL.T`` form (False). + is_super : bool + Whether the factor is in supernodal (True) or simplicial (False) format. + itype : :obj:`numpy.int32` or :obj:`numpy.int64` + The integer type used for indices and indptr in the factor. + dtype : numpy.dtype + The data type used for numerical values in the factor. + colcount : *(N,)* :obj:`numpy.ndarray` of int + The number of nonzeros in each column of the factor. + nnz : int + The number of nonzeros in the factor. + order : str or int + The ordering method used for the factorization. If an unknown ordering + was used, returns the integer value. + perm : *(N,)* :obj:`numpy.ndarray` of int + A read-only view of the permutation vector used for the factorization. + factor : :obj:`~scipy.sparse.csc_array` + A view of the the Cholesky factor in Compressed Sparse Column (CSC) + format. If ``self.is_ll``, the returned matrix is lower triangular. + Otherwise, the matrix view contains the lower triangular and the + diagonal factors combined. + + .. note:: + + The view is always in lower triangular form, even if the factor was + created using ``lower=False``. To get the upper triangular factor, + use :obj:`get_factor` with ``lower=False``. To get the split `L` + and `D` factors, use :obj:`get_factor` with ``kind="LDL"``. + + .. warning:: + + The returned matrix is a view on the internal data of the CHOLMOD + factor. It will be modified if the factor is modified (*e.g.*, by + calling :meth:`.factorize`). To get a copy, use + :meth:`.get_factor`. + + Raises + ------ + CholmodNotPositiveDefiniteError + If the input matrix is structurally singular (*e.g.*, if it is the zero + matrix). The input *may* be numerically indefinite, but this property + is not checked until :meth:`.factorize` is called. + + See Also + -------- + cholesky, ldl, cho_factor, ldl_factor + + Notes + ----- + The symbolic analysis follows that of the SuiteSparse CHOLMOD ``analyze`` + MATLAB function [#analyze_c]_. + + .. warning:: + + Calling ``CholeskyFactor.__new__(CholeskyFactor)`` will leave the object in an + "unsafe" state, since the internal CHOLMOD structures will not be initialized. + Always use the constructor ``CholeskyFactor(...)`` to create a new object. + + + References + ---------- + .. [#analyze_c] ``analyze.c`` - CHOLMOD MATLAB analyze function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/analyze.c + """ + + cdef cholmod_common _Common + cdef cholmod_common *_cm + cdef cholmod_factor *_factor + cdef bint _use_int32 + cdef bint _is_lower + cdef int _stype + + def __init__( + self, + object A, + *, + bint lower=True, + object order=None, + object sym_kind=None, + object supernodal_mode=None, + ): + A, self._use_int32, _ = validate_csc_input(A, require_square=True) + + if sym_kind is None: + sym_kind = "sym" + + if supernodal_mode is None: + supernodal_mode = "auto" + + if sym_kind not in {"sym", "row", "col"}: + raise ValueError( + f"Unknown symmetry kind: {sym_kind}. " + "Must be one of 'sym', 'row', 'col'." + ) + + if supernodal_mode not in _supernodal_modes: + raise ValueError( + f"Unknown factorization mode: {supernodal_mode}. " + f"Must be one of {set(_supernodal_modes.keys())}." + ) + + # Check the input ordering method + if order is not None and order not in _ordering_methods: + raise ValueError( + f"Unknown ordering method: {order}. " + f"Must be one of {set(_ordering_methods.keys())}." + ) + + # Matrix of all zeros + if A.shape[0] > 0 and A.nnz == 0: + raise CholmodNotPositiveDefiniteError("Input matrix not positive definite.") + + # Get the input matrix into CHOLMOD format + cdef cholmod_sparse Amatrix + cdef cholmod_sparse *Ac = &Amatrix + cdef cholmod_sparse *C + + # Use lower or upper triangular part of A + self._is_lower = lower + cdef int stype = -1 if self._is_lower else 1 + cdef bint transpose = False + + if sym_kind in ["row", "col"]: + stype = 0 # unsymmetric A @ A.T or A.T @ A + transpose = (sym_kind == "col") # A.T @ A + + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + + self._stype = Ac.stype + + # Initialize the Common object + self._cm = &self._Common + + if self._use_int32: + cholmod_start(self._cm) + else: + cholmod_l_start(self._cm) + + self._cm.supernodal = _supernodal_modes[supernodal_mode] + _set_ordering_method(order, self._cm) + + # Analyze the matrix, but do not factorize yet + if transpose: + if self._use_int32: + C = cholmod_transpose(Ac, CHOLMOD_TRANS_PATTERN, self._cm) + self._factor = cholmod_analyze(Ac, self._cm) + cholmod_free_sparse(&C, self._cm) + else: + C = cholmod_l_transpose(Ac, CHOLMOD_TRANS_PATTERN, self._cm) + self._factor = cholmod_l_analyze(Ac, self._cm) + cholmod_l_free_sparse(&C, self._cm) + else: + if self._use_int32: + self._factor = cholmod_analyze(Ac, self._cm) + else: + self._factor = cholmod_l_analyze(Ac, self._cm) + + # Check for errors + _handle_errors(self._cm.status, self._factor.minor) + + def __dealloc__(self): + """Deallocate memory used by the CholeskyFactor.""" + if self._cm is not NULL: + if self._use_int32: + if self._factor is not NULL: + cholmod_free_factor(&self._factor, self._cm) + cholmod_finish(self._cm) + else: + if self._factor is not NULL: + cholmod_l_free_factor(&self._factor, self._cm) + cholmod_l_finish(self._cm) + + def _require_factorized(self): + """Raise an error if the factor is symbolic only.""" + if not self.is_numeric: + raise CholmodError("Factor is symbolic. Call `factorize` before updating.") + + def __repr__(self): + return ( + f"CholeskyFactor(" + f"N={self.N}, " + f"nnz={self.nnz}, " + f"is_ll={self.is_ll}, " + f"is_super={self.is_super}, " + f"itype=np.{self.itype.name}, " + f"dtype=np.{self.dtype.name}, " + f"order={self.order}" + ")" + ) + + def __str__(self): + lines = [ + f"Cholesky factorization of size {self.N}x{self.N}", + f" Nonzeros: {self.nnz}", + f" Form: {'LL.T' if self.is_ll else 'LDL.T'}", + f" Triangle: {'lower' if self.is_lower else 'upper'}", + f" Storage: {'supernodal' if self.is_super else 'simplicial'}", + f" itype: np.{self.itype.name}", + f" dtype: np.{self.dtype.name}", + f" order: {self.order}", + ] + return "\n".join(lines) + + # ------------------------------------------------------------------------- + # Properties + # ------------------------------------------------------------------------- + @property + def is_ll(self): + return bool(self._factor.is_ll) + + @property + def is_lower(self): + return bool(self._is_lower) + + @property + def is_super(self): + return bool(self._factor.is_super) + + @property + def is_numeric(self): + return self._factor.xtype != CHOLMOD_PATTERN + + @property + def itype(self): + return np.dtype(_npitype_class_from_iype(self._factor.itype)) + + @property + def dtype(self): + # "np.int32" etc. are dtype classes, not actual dtypes. numpy handles + # both well, but be explicit and return a dtype object. + return np.dtype( + _npdtype_class_from_xdtype(self._factor.xtype, self._factor.dtype) + ) + + @property + def N(self): + return self._factor.n + + @property + def colcount(self): + return _ndarray_int_view_from_factor(self._factor.ColCount, self._factor.n, self) + + @property + def nnz(self): + return np.sum(self.colcount) + + @property + def order(self): + cdef int iorder = self._factor.ordering + return _ordering_methods_inv.get(iorder, iorder) + + @property + def perm(self): + return _ndarray_int_view_from_factor(self._factor.Perm, self._factor.n, self) + + @property + def factor(self): + return _csc_view_from_cholmod_factor(self) + + # ------------------------------------------------------------------------- + # Public Methods + # ------------------------------------------------------------------------- + def copy(self): + """Return a copy of the CholeskyFactor object. + + This method creates a deep copy of the CholeskyFactor object, + including the CHOLMOD common struct and the factor itself. + + This method does not copy *all* of the underlying `cholmod_common` + struct, only the parts that are necessary for using the factor. + + Returns + ------- + CholeskyFactor + A deep copy of the CholeskyFactor object. + """ + cdef CholeskyFactor cf = CholeskyFactor.__new__(CholeskyFactor) + + cf._cm = &cf._Common + + # Allocate and copy the CHOLMOD common struct + if self._use_int32: + cholmod_start(cf._cm) + else: + cholmod_l_start(cf._cm) + + _copy_cholmod_common(self._cm, cf._cm) + + # Copy the factor + if self._use_int32: + cf._factor = cholmod_copy_factor(self._factor, cf._cm) + else: + cf._factor = cholmod_l_copy_factor(self._factor, cf._cm) + + _handle_errors(cf._cm.status) + + cf._use_int32 = self._use_int32 + cf._is_lower = self._is_lower + cf._stype = self._stype + + return cf + + def get_factor(self, kind=None, lower=None): + """Return a copy of the Cholesky factor in the specified format. + + Parameters + ---------- + kind : None or str in {'LL', 'LDL'}, optional + The type of factor to return. If ``LL``, return the Cholesky + factor `L` such that :math:`L L^{\\top} = P A P^{\\top}`. If + ``LDL``, return the combined `LD` factor such that + :math:`L D L^{\\top} = P A P^{\\top}`. Default is None, which + uses the kind with which ``factorize`` was called. + lower : None or bool, optional + If True, return the lower triangular factor `L`. If False, return + the upper triangular factor `R`. If None (default), return the + factor in the same triangular form as it was created with + ``factorize``. + + Returns + ------- + L : csc_array + The Cholesky factor in Compressed Sparse Column (CSC) format. + D : diags_array, optional + If ``kind="LDL"``, also returns the `D` factor. + """ + if kind is None: + kind = "LL" if self.is_ll else "LDL" + + if kind not in ("LL", "LDL"): + raise ValueError("kind must be 'LL' or 'LDL'.") + + if lower is None: + lower = self._is_lower + + # Drop explicit zeros from returned copies + if kind == "LL": + L = _csc_view_from_cholmod_factor(self, ldl=False).copy() + L.eliminate_zeros() + if not lower: + L = L.T.conj() + return L + else: + # Extract L and D from combined LD factor + L = _csc_view_from_cholmod_factor(self, ldl=True).copy() + D = diags_array(L.diagonal()) + L.setdiag(1.0) + L.eliminate_zeros() + if not lower: + L = L.T.conj() + return L, D + + def get_perm(self): + """Return a copy of the permutation vector used in the factorization. + + Returns + ------- + p : ndarray + The permutation vector `p` such that :math:`P A P^{\\top}` is the + matrix that was factorized, where `P` is the permutation matrix + corresponding to `p`, *i.e.*, ``P = I[p]``. + """ + return self.perm.copy() + + def factorize(self, object A, object ldl=None, double beta=0.0): + """Compute the numerical Cholesky factorization of a sparse matrix. + + This method computes the numerical values of :math:`P A P^{\\top} + = R^{\\top} R` or :math:`P A P^{\\top} = L L^{\\top}` decomposition of + a Hermitian positive-definite matrix `A`, with fill-reducing + permutation `P`. + + Parameters + ---------- + A : (N, N) {array_like, sparse array} + An array convertible to a sparse matrix in Compressed Sparse Column + (CSC) format. The matrix must be square and symmetric positive + definite. Only the upper or lower triangular part of the matrix is + used, and no check is made for symmetry. This matrix can be + numericaly different from the matrix used to initialize the + :obj:`CholeskyFactor` object, but it must have the same sparsity + pattern. + ldl : bool, optional + If True, compute the LDL factorization instead of the Cholesky + factorization. Default is None, which uses the same type of + factorization as the previous call to :meth:`.factorize`, or False + if this is the first call. + beta : float, optional + The scalar value to add to the diagonal of the matrix before + factorization. Default is 0. + + Notes + ----- + If ``ldl=False``, this function computes the Cholesky factorization of + a symmetric positive definite matrix `A`: + + .. math:: + + R^{\\top} R = P A P^{\\top}, + + where `R` is an upper triangular matrix. Only the upper triangular part + of `A` is used. If ``self.is_lower`` is True, the lower triangular + factor `L` is computed instead, such that: + + .. math:: + + L L^{\\top} = P A P^{\\top}. + + In this case, only the lower triangular part of `A` is used. + + If ``ldl=True``, compute the factorization: + + .. math:: + + R^{\\top} D R = P A P^{\\top}, + + or + + .. math:: + + L D L^{\\top} = P A P^{\\top}, + + respectively. + + If ``beta`` is not None, the factorization is computed for the matrix: + + .. math:: + + P A P^{\\top} + \\beta I. + + Note that if the :obj:`CholeskyFactor` was initialized with ``sym_kind`` + equal to ``"row"`` or ``"col"``, the factorization is computed for + :math:`P A A^{\\top} P^{\\top}` or :math:`P A^{\\top} A P^{\\top}`, + respectively. Similarly, ``beta`` is added to the diagonal of these + matrices. + """ + assert self._factor is not NULL, "The factor has not been initialized." + + A, _, _ = validate_csc_input(A, require_square=True) + + if ldl is None: + if self.is_numeric: + ldl = not self.is_ll # use the existing factor type + else: + ldl = False # default to LL for first factorization + + if not isinstance(ldl, bool): + raise ValueError("ldl must be a boolean value.") + + # See CHOLMOD/MATLAB/ldlchol.c and/or lchol.c for details + self._cm.final_asis = False + self._cm.final_super = False + self._cm.final_ll = not ldl # LL.T for Cholesky, LDL.T for LDL + self._cm.final_pack = True + self._cm.final_monotonic = True + + # We do *not* drop numerically zero entries from the symbolic + # pattern, so that the resulting factor can be updated by `.update`. + # We *do* drop entries that result from supernodal amalgamation. + self._cm.final_resymbol = True + + self._cm.quick_return_if_not_posdef = True + + # Get the input matrix into CHOLMOD format + cdef cholmod_sparse Amatrix + cdef cholmod_sparse *Ac = &Amatrix + + stype = self._stype # set in __cinit__ with sym_kind + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + + # Set beta + if not np.isscalar(beta): + raise ValueError("beta must be a scalar value.") + + cdef double betac[2] + betac[0] = beta + betac[1] = 0.0 + + # Factorize the matrix + if self._use_int32: + cholmod_factorize_p(Ac, betac, NULL, 0, self._factor, self._cm) + else: + cholmod_l_factorize_p(Ac, betac, NULL, 0, self._factor, self._cm) + + # Check for errors + _handle_errors(self._cm.status, self._factor.minor) + + return self # for method chaining + + def solve(self, b): + """Solve the linear system :math:`A x = b` for `x`, using the + factorization. + + Parameters + ---------- + b : (N,) or (N, K) ndarray or sparse matrix + The right-hand side vector or matrix. + + Returns + ------- + x : (N,) or (N, K) ndarray or sparse matrix + The solution vector or matrix, returned in the same format as `b`. + + Raises + ------ + CholmodNotPositiveDefiniteError + If the matrix `A` is exactly singular, or singular to working + precision. + + Notes + ----- + This function solves the linear system: + + .. math:: + + R^{\\top} R x = b, + + where `R` is the upper triangular factor from the Cholesky factorization + of `A`. The input `b` is either dense or sparse, vector or matrix. + + If ``order`` was not ``natural`` when the factorization was computed, + solve the system: + + .. math:: + + P^{\\top} R^{\\top} R P x = b + + where `P` is the permutation matrix corresponding to the permutation + vector. Similarly, if ``lower`` was True when the factorization was + computed, the system solved is: + + .. math:: + + P^{\\top} L L^{\\top} P x = b. + + If the factorization is in LDL form, the system solved is: + + .. math:: + + P^{\\top} L D L^{\\top} P x = b. + + This function uses the CHOLMOD library to solve the linear system. It + is intended to combine the MATLAB interfaces ``cholmod2.m`` + [#cholmod_c]_, and ``ldlsolve.m`` [#ldlsolve_c]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#cholmod_c] ``cholmod2.c`` - CHOLMOD MATLAB interface + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/cholmod2.c + .. [#ldlsolve_c] ``ldlsolve.c`` - CHOLMOD MATLAB interface + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/ldlsolve.c + """ + self._require_factorized() + + if not (isinstance(b, np.ndarray) or issparse(b)): + raise ValueError("b must be an ndarray or sparse matrix.") + + if b.ndim not in (1, 2): + raise ValueError("b must be a 1D or 2D array.") + + cdef size_t N = self._factor.n + + if b.shape[0] != N: + raise ValueError( + "Right-hand side b must have the same number of rows as L." + ) + + # Special case: empty matrix + if N == 0: + return type(b)(b.shape, dtype=b.dtype) + + # Check the condition number before solving + self._check_rcond() + + cdef bint return_1D = b.ndim == 1 + + # CHOLMOD requires a 2D array + if b.ndim == 1: + b = b.reshape((N, 1)) + + if issparse(b): + X = self._solve_sparse(b.tocsc()) + else: + # For LDL, permute the RHS + if not self.is_ll: + b = b[self.perm] + + X = self._solve_dense(np.asfortranarray(b)) + + # For LDL, unpermute the solution + if not self.is_ll: + X = X[np.argsort(self.perm)] + + # Convert to 1D array if input b is 1D + if return_1D: + X = X[:, 0] + + return X + + cdef object _solve_sparse(self, object b): + """Solve the system A x = b with a sparse right-hand side.""" + # Get the b vector or matrix into CHOLMOD format + cdef cholmod_sparse Bspmatrix + cdef cholmod_sparse* Bs = &Bspmatrix + + # For LDL, permute the RHS + if not self.is_ll: + b = b[self.perm] + + cdef int stype = 0 + + b, _, _ = validate_csc_input(b) + + _cholmod_sparse_from_csc( + b.shape, b.indptr, b.indices, b.data, stype, &Bspmatrix + ) + + # Solve the system + cdef cholmod_sparse* Xs + + cdef int system = CHOLMOD_A if self._factor.is_ll else CHOLMOD_LDLt + + if self._use_int32: + Xs = cholmod_spsolve(system, self._factor, Bs, self._cm) + else: + Xs = cholmod_l_spsolve(system, self._factor, Bs, self._cm) + + _handle_errors(self._cm.status) + + return _csc_from_cholmod_sparse(Xs, self._cm) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _solve_dense(self, floating_t[::1, :] b not None): + """Solve the system A x = b with a dense right-hand side.""" + # Get the b vector or matrix into CHOLMOD format + cdef cholmod_dense Bmatrix + cdef cholmod_dense* Bd = &Bmatrix + + _cholmod_dense_from_ndarray(b, Bd) + + # Solve the system + cdef cholmod_dense* Xd + + cdef int system = CHOLMOD_A if self._factor.is_ll else CHOLMOD_LDLt + + if self._use_int32: + Xd = cholmod_solve(system, self._factor, Bd, self._cm) + else: + Xd = cholmod_l_solve(system, self._factor, Bd, self._cm) + + _handle_errors(self._cm.status) + + return _ndarray_from_cholmod_dense(Xd, self._use_int32, self._cm) + + cdef int _check_rcond(self) except -1: + """Check the condition number.""" + cdef double rcond + cdef double eps = np.finfo(np.float64).eps + + if self._use_int32: + rcond = cholmod_rcond(self._factor, self._cm) + else: + rcond = cholmod_l_rcond(self._factor, self._cm) + + _handle_errors(self._cm.status) + + if rcond == 0: + raise CholmodNotPositiveDefiniteError( + "Matrix is indefinite or singular to working precision." + ) + elif rcond < eps: + warnings.warn( + "Matrix is nearly singular." + f" Results may be inaccurate (rcond={rcond:.2e}).", + CholmodWarning, + ) + + def update(self, C): + return self._update(C, updown="up") + + def downdate(self, C): + return self._update(C, updown="down") + + def _update(self, C, updown="up"): + assert updown in ("up", "down") + + self._require_factorized() + + if not issparse(C) or C.ndim not in {1, 2}: + raise ValueError(f"Update matrix C is type {type(C)}. " + "Expected a 1D or 2D sparse array.") + + cdef size_t N = self._factor.n + + if C.shape[0] != N: + raise ValueError("Update matrix C must have the same number of rows as L.") + + # Ensure C is in CSC format + if C.ndim == 1: + C = C.reshape((-1, 1)).tocsc() # (N, 1) + + cdef int stype = 0 # use all of C + C, _, _ = validate_csc_input(C) + + cdef cholmod_sparse Cmatrix + cdef cholmod_sparse* Cc = &Cmatrix + + _cholmod_sparse_from_csc( + C.shape, C.indptr, C.indices, C.data, stype, &Cmatrix + ) + + # Permute C so it is accepted in "matrix" space. + # From Modify/cholmod_updown.c: + # Note that the fill-reducing permutation L->Perm is NOT used. The row + # indices of C refer to the rows of L, not A. If your original system is + # LDL' = PAP' (where P = L->Perm), and you want to compute the LDL' + # factorization of A+CC', then you must permute C first. That is: + # + # PAP' = LDL' + # P(A+CC')P' = PAP'+PCC'P' = LDL' + (PC)(PC)' = LDL' + Cnew*Cnew' + # where Cnew = P*C. + # + # You can use the cholmod_submatrix routine in the MatrixOps module + # to permute C, with: + # + # Cnew = cholmod_submatrix (C, L->Perm, L->n, NULL, -1, TRUE, TRUE, Common) ; + # + # Note that the sorted input parameter to cholmod_submatrix must be TRUE, + # because cholmod_updown requires C with sorted columns. + cdef cholmod_sparse *C_perm + + if self._use_int32: + C_perm = cholmod_submatrix( + Cc, self._factor.Perm, N, NULL, -1, True, True, self._cm + ) + else: + C_perm = cholmod_l_submatrix( + Cc, self._factor.Perm, N, NULL, -1, True, True, self._cm + ) + + # Compute the update or downdate + cdef int update = updown == "up" + cdef int ok + + if self._use_int32: + ok = cholmod_updown(update, C_perm, self._factor, self._cm) + else: + ok = cholmod_l_updown(update, C_perm, self._factor, self._cm) + + if self._use_int32: + cholmod_free_sparse(&C_perm, self._cm) + else: + cholmod_l_free_sparse(&C_perm, self._cm) + + if not ok: + raise CholmodError("Update or downdate failed.") + + return self + + def rowadd(self, k, C): + r"""Add a row to a sparse LDL factorization. + + Compute a rank-1 update of a sparse LDL factorization. This method + "adds" a row by setting the :math:`k^{th}` row and column of the + original matrix to ``C``. + + Parameters + ---------- + k : int :math:`\in [0, N)` + The row/column index to modify. + C : (N, 1) csc_array, optional + If given, change the factorization such that row and column ``k`` + of the original matrix equal ``C``. The number of rows must match + that of ``L`` and ``D``. + + Returns + ------- + CholeskyFactor + The current object, for method chaining. + + .. versionadded:: 0.5.0 + """ + self._require_factorized() + + if not (0 <= k < self.N): + raise IndexError( + f"Row index k={k} is out of bounds for matrix of size {self.N}." + ) + + if not issparse(C) or C.ndim not in {1, 2}: + raise ValueError( + f"Update matrix C is type {type(C)}." + "Expected a 1D or 2D sparse array." + ) + + if C.shape[0] != self.N: + raise ValueError( + "Update matrix C must have the same number of rows as L." + ) + + # Get C Matrix + cdef cholmod_sparse Cmatrix + cdef cholmod_sparse* Cc = &Cmatrix + cdef int stype = 0 # use all of C + + # Ensure C is in CSC format + if C.ndim == 1: + C = C.reshape((-1, 1)).tocsc() # (N, 1) + + C, _, _ = validate_csc_input(C) + _cholmod_sparse_from_csc( + C.shape, C.indptr, C.indices, C.data, stype, &Cmatrix + ) + + # Compute the Update + cdef int ok + + if self._use_int32: + ok = cholmod_rowadd(k, Cc, self._factor, self._cm) + else: + ok = cholmod_l_rowadd(k, Cc, self._factor, self._cm) + + if not ok: + raise CholmodError("cholmod_rowadd failed.") + + return self + + def rowdel(self, k): + r"""Delete a row from a sparse LDL factorization. + + Compute a rank-1 update of a sparse LDL factorization. This method + "deletes" a row by setting the :math:`k^{th}` row and column of the + original matrix to the identity. + + Parameters + ---------- + k : int :math:`\in [0, N)` + The row/column index to modify. + + Returns + ------- + CholeskyFactor + The current object, for method chaining. + + .. versionadded:: 0.5.0 + """ + self._require_factorized() + + if not (0 <= k < self.N): + raise IndexError( + f"Row index k={k} is out of bounds for matrix of size {self.N}." + ) + + cdef int ok + + if self._use_int32: + ok = cholmod_rowdel(k, NULL, self._factor, self._cm) + else: + ok = cholmod_l_rowdel(k, NULL, self._factor, self._cm) + + if not ok: + raise CholmodError("cholmod_rowdel failed.") + + return self + + def resymbol(self, object A, bint is_permuted=True): + """Recompute the symbolic Cholesky factorization of a sparse matrix. + + This function is useful after a series of downdates via + :meth:`.update` or :meth:`.rowdel`, since downdates do not remove + any entries in ``L`` [#resymbol_c]_. + + Parameters + ---------- + A : (N, N) csc_array + The input matrix in Compressed Sparse Column (CSC) format. Must be + square and symmetric. Only the lower triangular part of ``A`` is + used, and no check is made for symmetry. The numerical values of + ``A`` are ignored. Only its non-zero pattern is used. + + .. note :: The input matrix ``A`` is expected to be the + permuted matrix :math:`P A P^{\\top}`, where `P` is the + permutation matrix corresponding to the permutation vector + returned by :meth:`.get_perm`. + + is_permuted : bool + If True (default), the input matrix ``A`` is assumed to be + permuted by the fill-reducing permutation used in the factorization: + :math:`P A P^{\\top}`. If False, ``A`` is assumed to be in the + original ordering. + + Returns + ------- + :class:`.CholeskyFactor` + The current object, for method chaining. + + See Also + -------- + cholesky, ldl, update, rowadd, rowdel + + References + ---------- + .. [#resymbol_c] ``resymbol.c`` - CHOLMOD MATLAB resymbolization function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/resymbol.c + + .. versionadded:: 0.5.0 + """ + self._require_factorized() + + cdef bint A_use_int32 + A, A_use_int32, _ = validate_csc_input(A, require_square=True) + + if A.shape[0] != self.N: + raise ValueError( + "Input matrix A must have the same number of rows as L." + ) + + if self._use_int32 and not A_use_int32: + raise ValueError( + "A and factor must have the same integer type. " + f"Got {A.indptr.dtype=}, but {self._use_int32=}." + ) + + # Special Cases + if self.N == 0: + return self + + if A.nnz == 0: + raise CholmodNotPositiveDefiniteError("Input matrix not positive definite.") + + # Get sparse *pattern* + cdef cholmod_sparse Amatrix + cdef cholmod_sparse* Ac = &Amatrix + cdef int stype = -1 # use tril(A) only + + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + Ac.xtype = CHOLMOD_PATTERN + Ac.x = NULL + + # NOTE the "noperm" version expects that A is already permuted by + # self._factor.Perm (i.e., A = A[p][:, p]). The regular version uses + # self._factor.Perm to permute A internally. + if self._use_int32: + if is_permuted: + cholmod_resymbol_noperm(Ac, NULL, 0, True, self._factor, self._cm) + else: + cholmod_resymbol(Ac, NULL, 0, True, self._factor, self._cm) + else: + if is_permuted: + cholmod_l_resymbol_noperm(Ac, NULL, 0, True, self._factor, self._cm) + else: + cholmod_l_resymbol(Ac, NULL, 0, True, self._factor, self._cm) + + return self + + def logdet(self): + """Compute the (natural) log-determinant of the matrix from its + Cholesky factorization. + + Returns + ------- + logdet : float + The natural logarithm of the determinant of the matrix `A` that was + factorized. + + See Also + -------- + slogdet, det, numpy.linalg.slogdet, numpy.linalg.det, scipy.linalg.det + + Notes + ----- + This function computes the log-determinant of the matrix `A` from its + Cholesky factorization. If the factorization is in :math:`LL^T` form, + the determinant is computed as: + + .. math:: + + \\log \\det(A) = 2 \\sum_i \\log L_{ii}. + + If the factorization is in :math:`LDL^{\\top}` form, the determinant is + computed as: + + .. math:: + + \\log \\det(A) = \\sum_i \\log D_{ii}. + + .. versionadded:: 0.2 + """ + self._require_factorized() + if self.is_ll: + L = self.get_factor() + return 2 * np.sum(np.log(L.diagonal())) + else: + _, D = self.get_factor() + return np.sum(np.log(D.diagonal())) + + def slogdet(self): + """Compute the sign and (natural) log-determinant of the matrix from + its Cholesky factorization. + + Returns + ------- + sign : int + The sign of the determinant of the matrix `A` that was + factorized. This is always 1 for a positive definite matrix. + logdet : float + The natural logarithm of the absolute value of the determinant of + the matrix `A` that was factorized. + + See Also + -------- + logdet, det, numpy.linalg.slogdet, numpy.linalg.det, scipy.linalg.det + + Notes + ----- + This function computes the sign and log-determinant of the matrix `A` + from its Cholesky factorization. If the factorization is in + :math:`LL^T` form, the determinant is computed as: + + .. math:: + + \\log \\det(A) = 2 \\sum_i \\log L_{ii}. + + If the factorization is in :math:`LDL^{\\top}` form, the determinant is + computed as: + + .. math:: + + \\log \\det(A) = \\sum_i \\log D_{ii}. + + .. versionadded:: 0.2 + """ + return (self.dtype.type(1.0), self.logdet()) + + def det(self): + """Compute the determinant of the matrix from its Cholesky + factorization. + + .. versionadded:: 0.2 + + .. warning:: + + This function may overflow or underflow for large matrices. Use + :meth:`.logdet` or :meth:`.slogdet` instead. + + Returns + ------- + det : float + The determinant of the matrix `A` that was factorized. + + See Also + -------- + logdet, slogdet, numpy.linalg.det, numpy.linalg.slogdet, scipy.linalg.det + """ + return np.exp(self.logdet()) + + def inv(self): + """Compute the inverse of the matrix from its Cholesky factorization. + + .. warning:: For most purposes, it is better to use :meth:`.solve` + instead of computing the inverse explicitly. The + following two lines of code are mathematically equivalent:: + + x = f.solve(b) + x = f.inv() @ b # DO NOT USE + + but the first line is both faster and more numerically stable. + + Returns + ------- + Ainv : csc_array + The inverse of the matrix `A` that was factorized. + + See Also + -------- + numpy.linalg.inv, scipy.linalg.inv + + Notes + ----- + This function computes the inverse of the matrix `A` from its Cholesky + factorization. If the factorization is in :math:`LL^T` form, the + inverse is computed as: + + .. math:: + + A^{-1} = P^{\\top} L^{-\\top} L^{-1} P, + + where `P` is the permutation matrix corresponding to the permutation + vector returned by :meth:`.get_perm`. If the factorization is in + :math:`LDL^{\\top}` form, the inverse is computed as: + + .. math:: + + A^{-1} = P^{\\top} L^{-\\top} D^{-1} L^{-1} P. + + .. versionadded:: 0.2 + """ + return self.solve(eye_array(self.N, format='csc', dtype=self.dtype)) + + +# ----------------------------------------------------------------------------- +# Docstrings for CholeskyFactor +# ----------------------------------------------------------------------------- +_DOC_UPDATE_TEMPLATE = """ +Multiple-rank {direction} of a sparse LDL factorization. + +Compute a {direction} to the factorization of a sparse matrix `A` [#{tag}]_: + +.. math:: + + L' D' L'^{{\\top}} = P (A {sign} C C^{{\\top}}) P^{{\\top}} + +where `L` is a lower triangular matrix with unit diagonal, and `D` is +a diagonal matrix. The input ``C`` is a sparse matrix representing the +{direction} to the factorization. The fill-reducing permutation is *not* +recomputed from the original `A`. + +Parameters +---------- +C : (N, K) csc_array + The sparse matrix representing the rank-`k` update or downdate to + the matrix. + +Returns +------- +CholeskyFactor + The current object, for method chaining. + +References +---------- +.. [#{tag}] ``cholmod_updown.c`` - CHOLMOD up/downdate function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/Modify/cholmod_updown.c + +.. versionadded:: 0.5.0 +""" + +# Assign the docstrings +CholeskyFactor.update.__doc__ = _DOC_UPDATE_TEMPLATE.format( + direction="update", sign="+", tag="update_c", +) + +CholeskyFactor.downdate.__doc__ = _DOC_UPDATE_TEMPLATE.format( + direction="downdate", sign="-", tag="downdate_c", +) + + +# ----------------------------------------------------------------------------- +# Convenience functions +# ----------------------------------------------------------------------------- +def cho_factor( + A, beta=0.0, *, lower=False, order=None, sym_kind=None, supernodal_mode=None +): + return CholeskyFactor( + A, lower=lower, order=order, sym_kind=sym_kind, supernodal_mode=supernodal_mode + ).factorize(A, ldl=False, beta=beta) + + +def ldl_factor( + A, beta=0.0, *, lower=True, order=None, sym_kind=None, supernodal_mode=None +): + return CholeskyFactor( + A, lower=lower, order=order, sym_kind=sym_kind, supernodal_mode=supernodal_mode + ).factorize(A, ldl=True, beta=beta) + + +# csc_arrays from the factorization, and optionally the permutation +def cholesky( + A, beta=0.0, *, lower=False, order=None, sym_kind=None, supernodal_mode=None +): + f = cho_factor( + A, + beta=beta, + lower=lower, + order=order, + sym_kind=sym_kind, + supernodal_mode=supernodal_mode, + ) + R = f.get_factor() + p = f.get_perm() + return R if order is None else (R, p) + + +def ldl(A, beta=0.0, *, lower=True, order=None, sym_kind=None, supernodal_mode=None): + f = ldl_factor( + A, + beta=beta, + lower=lower, + order=order, + sym_kind=sym_kind, + supernodal_mode=supernodal_mode, + ) + R, D = f.get_factor() + p = f.get_perm() + return (R, D) if order is None else (R, D, p) + + +# ----------------------------------------------------------------------------- +# Docstring Template +# ----------------------------------------------------------------------------- +_CHOLMOD_DOC_TEMPLATE = """ +{intro} +Parameters +---------- +A : (N, N) {{array_like, sparse array}} + An array convertible to a sparse matrix in Compressed Sparse Column + (CSC) format. The matrix must be square and symmetric positive definite. + Only the upper or lower triangular part of the matrix is used, and no check + is made for symmetry. +beta : float, optional + The scalar value to add to the diagonal of the matrix before factorization. +lower : bool, optional + If True, return the lower triangular factor `L`, otherwise return the + upper triangular factor `R`. +order : None or str in {{"default", "best", "natural", "metis", "nesdis", \ + "amd", "colamd", "postordered"}}, optional + The permutation algorithm to use for the factorization. By default, the + natural ordering of the input matrix is used. The other options are: + + * ``default``: Use the default method, which first tries AMD, then METIS. + * ``best``: Automatically select the best ordering based on the input. + * ``metis``: Use the METIS library for graph partitioning. + * ``nesdis``: Use the NESDIS library for nested dissection. + * ``amd``: Use the Approximate Minimum Degree (AMD) algorithm. + * ``colamd``: Use the Approximate Minimum Degree (AMD) algorithm for the + symmetric case, or the COLAMD algorithm for the unsymmetric case + (:math:`A A^{{\\top}}` or :math:`A^{{\\top}} A`). + * ``postordered``: Use natural ordering followed by postordering. + + By default, methods other than ``natural`` will also be postordered. + + .. warning:: + + The ordering method ``best`` may be quite slow for large matrices, + but if the factorization is reused many times, it can be worth it. + +sym_kind : str in {{"sym", "row", "col"}}, optional + The type of factorization for which to analyze the matrix: + + * ``sym``: Symmetric factorization. No check is made for symmetry. + * ``row``: Unsymmetric factorization of :math:`A A^{{\\top}}`. + * ``col``: Unsymmetric factorization of :math:`A^{{\\top}} A`. + +supernodal_mode : str in {{"auto", "simplicial", "supernodal"}}, optional + The type of factorization to use: + + * ``auto``: Automatically select the factorization type. + * ``simplicial``: Use a simplicial factorization. + * ``supernodal``: Use a supernodal factorization. + + Note that the ``simplicial`` mode may be slow for large matrices. + +Returns +------- +{returns} + +Raises +------ +:exc:`CholmodNotPositiveDefiniteError` + If the input matrix is not positive definite. + +See Also +-------- +{see_also} + +Notes +----- +This function is an interface to the CHOLMOD library, which is part of +the SuiteSparse collection by Timothy A. Davis. For more details, see the +documentation in the header file [{doc_tag}]_. + + +{version_notes} + +References +---------- +.. [{doc_tag}] ``cholmod.h`` - SuiteSparse CHOLMOD header file. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/Include/cholmod.h + +Examples +-------- +{example} +""" + + +_CHOLESKY_RETURNS = """R : csc_array + The triangular factor of the Cholesky decomposition. The data type will + match that of ``A``. +{ldl_D_output} +p : ndarray of int, optional + The permutation vector used in the factorization. Only returned if the + ordering is not ``None``. +""" + + +# ----------------------------------------------------------------------------- +# Cholesky Docstring +# ----------------------------------------------------------------------------- +_cholesky_intro = """Compute the Cholesky factorization of a sparse matrix. + +This function computes the Cholesky factorization of a symmetric positive +definite matrix `A`: + +.. math:: + + R^{\\top} R = P A P^{\\top}, + +where `R` is an upper triangular matrix. Only the upper triangular part of +`A` is used. If ``lower`` is True, the lower triangular factor `L` is +returned instead, such that: + +.. math:: + + L L^{\\top} = P A P^{\\top}. + +In this case, only the lower triangular part of `A` is used. + +If ``beta`` is a scalar value, compute the factorization of: + +.. math:: + + P A P^{\\top} + \\beta I, + +where `I` is the identity matrix. +""" + +_cho_factor_returns = """CholeskyFactor + The factorization object. Use its methods to solve linear systems + and manipulate the factorization. +""" + +_cholesky_example = """ +>>> import numpy as np +>>> from scipy.sparse import coo_array +>>> from sksparse.cholmod import cholesky, cho_factor +>>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) +>>> N = 11 +>>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) +>>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) +>>> rng = np.random.default_rng(56) +>>> vals = rng.random(len(rows), dtype=np.float64) +>>> L = coo_array((vals, (rows, cols)), shape=(N, N)) +>>> A = L + L.T # make it symmetric +>>> A.setdiag(N) # make it strongly positive definite +>>> A = A.tocsc() +>>> L, p = cholesky(A, order='amd', lower=True) +>>> L + +>>> p +array([ 4, 8, 6, 0, 3, 5, 1, 2, 9, 10, 7]) +>>> f = cho_factor(A, order='amd', lower=True) +>>> f +CholeskyFactor(N=11, nnz=30, is_ll=True, is_super=False, itype=np.int64, + dtype=np.float64, order=natural) +>>> np.allclose(L.toarray(), f.get_factor().toarray(), atol=1e-15) +True +>>> np.array_equal(p, f.get_perm()) +True +>>> # Solve a linear system +>>> expect_x = np.arange(N, dtype=np.float64) +>>> b = A @ expect_x +>>> x = f.solve(b) +>>> np.allclose(x, expect_x) +True +""" + + +cho_factor.__doc__ = _CHOLMOD_DOC_TEMPLATE.format( + intro=_cholesky_intro, + returns=_cho_factor_returns, + see_also="cholesky, ldl, ldl_factor", + version_notes=".. versionadded:: 0.5.0", + doc_tag="#cho_factor_h", + example=_cholesky_example, +) + + +_cholesky_version_notes=""".. versionadded:: 0.1.0 +.. versionchanged:: 0.5.0 + The function now returns the matrix directly instead of a ``Factor`` + object, and the permutation vector when an ordering method is specified. +""" + +cholesky.__doc__ = _CHOLMOD_DOC_TEMPLATE.format( + intro=_cholesky_intro, + returns=_CHOLESKY_RETURNS.format(ldl_D_output=""), + see_also="cho_factor, ldl, ldl_factor", + version_notes=_cholesky_version_notes, + doc_tag="#cholesky_h", + example=_cholesky_example, +) + +# ----------------------------------------------------------------------------- +# LDL Docstring +# ----------------------------------------------------------------------------- +_ldl_intro = """ +Compute the LDL factorization of a sparse matrix. + +This function computes the LDL factorization of a symmetric matrix `A`: + +.. math:: + + L D L^{\\top} = P A P^{\\top}, + +where `L` is a lower triangular matrix with unit diagonal, and `D` is +a diagonal matrix. Only the lower triangular part of `A` is used. If +``lower`` is False, the upper triangular factor `R` is returned instead, +such that: + +.. math:: + + R^{\\top} D R = P A P^{\\top}. + +In this case, only the upper triangular part of `A` is used. + +If ``beta`` is a scalar value, compute the factorization of: + +.. math:: + + P A P^{\\top} + \\beta I, + +where `I` is the identity matrix. +""" + +_ldl_D_output = """D : dia_array + The diagonal matrix `D` of the factorization, in sparse DIA format. + The data type will match that of ``A``.""" + + +_ldl_example = """ +>>> import numpy as np +>>> from scipy.sparse import coo_array +>>> from sksparse.cholmod import ldl, ldl_factor +>>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) +>>> N = 11 +>>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) +>>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) +>>> rng = np.random.default_rng(56) +>>> vals = rng.random(len(rows), dtype=np.float64) +>>> L = coo_array((vals, (rows, cols)), shape=(N, N)) +>>> A = L + L.T # make it symmetric +>>> A.setdiag(N) # make it strongly positive definite +>>> A = A.tocsc() +>>> L, D, p = ldl(A, order='amd') +>>> L + +>>> D + +>>> p +array([ 4, 8, 6, 0, 3, 5, 1, 2, 9, 10, 7]) +>>> f = ldl_factor(A, order='amd') +>>> f +CholeskyFactor(N=11, nnz=30, is_ll=False, is_super=False, itype=np.int64, + dtype=np.float64, order=amd) +>>> Lf, Df = f.get_factor() +>>> np.allclose(L.toarray(), Lf.toarray(), atol=1e-15) +True +>>> np.allclose(D.toarray(), Df.toarray(), atol=1e-15) +True +>>> np.array_equal(p, f.get_perm()) +True +>>> # Solve a linear system +>>> expect_x = np.arange(N, dtype=np.float64) +>>> b = A @ expect_x +>>> x = f.solve(b) +>>> np.allclose(x, expect_x) +True +""" + + +ldl_factor.__doc__ = _CHOLMOD_DOC_TEMPLATE.format( + intro=_ldl_intro, + returns=_cho_factor_returns, + see_also="ldl, cholesky, cho_factor", + version_notes=".. versionadded:: 0.5.0", + doc_tag="#ldl_factor_h", + example=_ldl_example, +) + + +ldl.__doc__ = _CHOLMOD_DOC_TEMPLATE.format( + intro=_ldl_intro, + returns=_CHOLESKY_RETURNS.format(ldl_D_output=_ldl_D_output), + see_also="ldl_factor, cholesky, cho_factor", + version_notes=".. versionadded:: 0.5.0", + doc_tag="#ldl_h", + example=_ldl_example, +) + + +# ----------------------------------------------------------------------------- +# Symbolic Functions +# ----------------------------------------------------------------------------- +def symbfact(A, *, kind=None, bint lower=False, bint return_factor=False): + """Symbolic factorization of a sparse matrix for Cholesky or LDL. + + This function performs the symbolic factorization of a sparse matrix ``A`` + for either Cholesky or LDL factorization. It computes the elimination + tree and analyzes the sparsity pattern of the matrix [#symbfact_c]_. + + Parameters + ---------- + A : (N, N) csc_array + The input matrix in Compressed Sparse Column (CSC) format. Must be + square and symmetric. No check is made for symmetry, so the upper (or + lower) triangular part of the matrix is used for the factorization, depending + on the ``lower`` parameter. + kind : str in {"sym", "row", "col"}, optional + The type of factorization for which to analyze the matrix: + + * ``sym``: Symmetric factorization. Only the upper triangular part of + ``A`` is used, and no check is made for symmetry. + * ``row``: Unsymmetric factorization of :math:`A A^{\\top}`. + * ``col``: Unsymmetric factorization of :math:`A^{\\top} A`. + * ``lo``: Lower triangular factorization. Same as ``symbfact(A.T)``. + Only the lower triangular part of ``A`` is used, and no check is made + for symmetry. + + If ``kind`` is None, it defaults to ``sym``. + lower : bool, optional + If True, the symbolic factorization is performed on the lower + triangular part of the matrix. If False, the upper triangular part is + used. Default is False (upper triangular). + return_factor : bool, optional + If True, the symbolic factorization returns the structure of the + Cholesky factor `L` (or `LD` for LDL factorization) as a sparse matrix. + Default is False. + + Returns + ------- + count : (N,) ndarray of int + The count of nonzeros in each column of the Cholesky factor. + h : int + The height of the elimination tree. + parent : (N,) ndarray of int + The parent of each node in the elimination tree. The root has no parent + (parent[0] = -1). + post : (N,) ndarray of int + The postorder of the elimination tree. The first node in the postorder + is the root of the tree. + L : (N, N) csc_array + The symbolic factorization of the matrix. Only returned if + ``return_factor`` is True. + + See Also + -------- + etree + + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#symbfact_c] ``symbfact2.c`` - CHOLMOD MATLAB symbolic factorization function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/symbfact2.c + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import coo_array + >>> from sksparse.cholmod import cholesky, symbfact + >>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) + >>> N = 11 + >>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + >>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + >>> rng = np.random.default_rng(56) + >>> vals = rng.random(len(rows), dtype=np.float64) + >>> L = coo_array((vals, (rows, cols)), shape=(N, N)) + >>> A = L + L.T # make it symmetric + >>> A.setdiag(N) # make it strongly positive definite + >>> A = A.tocsc() + >>> L = cholesky(A, lower=True) + >>> count, h, parent, post = symbfact(A) + >>> count + array([3, 3, 4, 3, 3, 4, 4, 3, 3, 2, 1]) + >>> np.array_equal(count, np.count_nonzero(L.toarray(), axis=0)) + True + >>> h + 6 + >>> parent + array([ 5, 2, 7, 5, 7, 6, 8, 9, 9, 10, -1]) + >>> post + array([ 1, 2, 4, 7, 0, 3, 5, 6, 8, 9, 10]) + """ + cdef bint use_int32 + A, use_int32, out_itype = validate_csc_input(A) + + if kind is None: + kind = "sym" + + if kind not in {"sym", "row", "col", "lo"}: + raise ValueError(f"Unknown factorization kind: {kind}") + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if kind not in ["row", "col"] and M != N: + raise ValueError(f"Input matrix A must be square, got shape {A.shape}.") + + # Special Cases + # sym: A = (0, 0) + # row: AA.T = (0, N) * (N, 0) = (0, 0) + # col: A.TA = (0, M) * (M, 0) = (0, 0) + if kind == "row" and M == 0 or N == 0: + empty = np.array([], dtype=out_itype) + count, h, parent, post, L = empty, 0, empty, empty, A.copy() + if return_factor: + return count, h, parent, post, L + else: + return count, h, parent, post + + if A.nnz == 0: + D = N if kind == "col" else M + count = np.zeros(D, dtype=out_itype) + h = 1 + parent = np.full(D, -1, dtype=out_itype) + post = np.arange(D, dtype=out_itype) + L = eye_array(D, dtype=A.dtype) + if return_factor: + return count, h, parent, post, L + else: + return count, h, parent, post + + # ------------------------------------------------------------------------- + # Start the Analysis + # ------------------------------------------------------------------------- + cdef cholmod_common Common + cdef cholmod_common *cm = &Common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + cdef cholmod_sparse Amatrix + cdef cholmod_sparse* Ac = &Amatrix + + N = A.shape[0] + cdef int stype = 1 # default kind="sym" uses triu(A) only + cdef bint col_etree = False + + if kind == "row": + stype = 0 # use A * A.T + elif kind == "col": + N = A.shape[1] + stype = 0 # use A.T * A + col_etree = True + elif kind == "lo": + stype = -1 # use tril(A) only + + # Get sparse *pattern* + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + Ac.xtype = CHOLMOD_PATTERN + Ac.x = NULL + + # ------------------------------------------------------------------------- + # Compute the Outputs + # ------------------------------------------------------------------------- + cdef void *Parent + cdef void *Post + cdef void *ColCount + cdef void *First + cdef void *Level + + if use_int32: + Parent = cholmod_malloc(N, sizeof(int32_t), cm) + Post = cholmod_malloc(N, sizeof(int32_t), cm) + ColCount = cholmod_malloc(N, sizeof(int32_t), cm) + First = cholmod_malloc(N, sizeof(int32_t), cm) + Level = cholmod_malloc(N, sizeof(int32_t), cm) + else: + Parent = cholmod_l_malloc(N, sizeof(int64_t), cm) + Post = cholmod_l_malloc(N, sizeof(int64_t), cm) + ColCount = cholmod_l_malloc(N, sizeof(int64_t), cm) + First = cholmod_l_malloc(N, sizeof(int64_t), cm) + Level = cholmod_l_malloc(N, sizeof(int64_t), cm) + + cdef cholmod_sparse *Fc + cdef cholmod_sparse *Aup + cdef cholmod_sparse *Alo + + if use_int32: + Fc = cholmod_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + else: + Fc = cholmod_l_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + + if Ac.stype == 1 or col_etree: + Aup = Ac + Alo = Fc + else: + Aup = Fc + Alo = Ac + + if use_int32: + cholmod_etree(Aup, Parent, cm) + else: + cholmod_l_etree(Aup, Parent, cm) + + _handle_errors(cm.status) + + if use_int32: + if cholmod_postorder(Parent, N, NULL, Post, cm) != N: + raise CholmodError("Postordering failed.") + else: + if cholmod_l_postorder(Parent, N, NULL, Post, cm) != N: + raise CholmodError("Postordering failed.") + + if use_int32: + cholmod_rowcolcounts( + Alo, + NULL, + 0, + Parent, + Post, + NULL, + ColCount, + First, + Level, + cm + ) + else: + cholmod_l_rowcolcounts( + Alo, + NULL, + 0, + Parent, + Post, + NULL, + ColCount, + First, + Level, + cm + ) + + _handle_errors(cm.status) + + # Return the results + count = _ndarray_copy_from_intptr(ColCount, N, use_int32) + + # Compute height of the elimination tree + cdef int32_t h_int32 = 0 + cdef int64_t h_int64 = 0 + cdef size_t i + + if use_int32: + for i in range(N): + h_int32 = max(h_int32, (Level)[i]) + h = h_int32 + 1 + else: + for i in range(N): + h_int64 = max(h_int64, (Level)[i]) + h = h_int64 + 1 + + parent = _ndarray_copy_from_intptr(Parent, N, use_int32) + post = _ndarray_copy_from_intptr(Post, N, use_int32) + + # Construct symbolic L if requested + cdef cholmod_sparse *Ls + cdef cholmod_sparse *Rs + + if return_factor: + if use_int32: + Ls = _cholesky_pattern( + Ac, Fc, N, Parent, ColCount, col_etree, cm + ) + if not lower: + Rs = cholmod_transpose(Ls, CHOLMOD_TRANS_PATTERN, cm) + cholmod_free_sparse(&Ls, cm) + Ls = Rs + else: + Ls = _cholesky_l_pattern( + Ac, Fc, N, Parent, ColCount, col_etree, cm + ) + if not lower: + Rs = cholmod_l_transpose(Ls, CHOLMOD_TRANS_PATTERN, cm) + cholmod_l_free_sparse(&Ls, cm) + Ls = Rs + + # Convert the symbolic L to a CSC array + L = _csc_from_cholmod_sparse(Ls, cm) + + # Fill the L matrix data with boolean ones (for python) + L.data = np.ones(L.nnz, dtype=np.bool_) + + # Free memory (arrays are copied to numpy) + if use_int32: + cholmod_free(N, sizeof(int32_t), Parent, cm) + cholmod_free(N, sizeof(int32_t), Post, cm) + cholmod_free(N, sizeof(int32_t), ColCount, cm) + cholmod_free(N, sizeof(int32_t), First, cm) + cholmod_free(N, sizeof(int32_t), Level, cm) + cholmod_free_sparse(&Fc, cm) + cholmod_finish(cm) + else: + cholmod_l_free(N, sizeof(int64_t), Parent, cm) + cholmod_l_free(N, sizeof(int64_t), Post, cm) + cholmod_l_free(N, sizeof(int64_t), ColCount, cm) + cholmod_l_free(N, sizeof(int64_t), First, cm) + cholmod_l_free(N, sizeof(int64_t), Level, cm) + cholmod_l_free_sparse(&Fc, cm) + cholmod_l_finish(cm) + + if return_factor: + return count, h, parent, post, L + else: + return count, h, parent, post + + +def etree(A, *, kind=None, bint return_post=False): + """Symbolic factorization of a sparse matrix for Cholesky or LDL. + + This function determines the elimination tree of a sparse matrix ``A``, and + optionally postorders the tree [#etree_c]_. + + Parameters + ---------- + A : (N, N) csc_array + The input matrix in Compressed Sparse Column (CSC) format. Must be + square and symmetric. No check is made for symmetry, so the upper (or + lower) triangular part of the matrix is used for the factorization, depending + on the ``lower`` parameter. + kind : str in {"sym", "row", "col"}, optional + The type of factorization for which to analyze the matrix: + + * ``sym``: Symmetric factorization. Only the upper triangular part of + ``A`` is used, and no check is made for symmetry. + * ``row``: Unsymmetric factorization of :math:`A A^{\\top}`. + * ``col``: Unsymmetric factorization of :math:`A^{\\top} A`. + * ``lo``: Lower triangular factorization. Same as ``symbfact(A.T)``. + Only the lower triangular part of ``A`` is used, and no check is made + for symmetry. + + If ``kind`` is None, it defaults to ``sym``. + return_post : bool, optional + If True, the function returns the postorder of the elimination tree. + Default is False. + + Returns + ------- + parent : (N,) ndarray of int + The parent of each node in the elimination tree. The root has no parent + (parent[0] = -1). + post : (N,) ndarray of int, optional + The postorder of the elimination tree. The first node in the postorder + is the root of the tree. + + .. versionadded:: 0.5.0 + + See Also + -------- + symbfact + + References + ---------- + .. [#etree_c] ``etree2.c`` - CHOLMOD MATLAB symbolic factorization function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/etree2.c + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import coo_array + >>> from sksparse.cholmod import etree + >>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) + >>> N = 11 + >>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + >>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + >>> rng = np.random.default_rng(56) + >>> vals = rng.random(len(rows), dtype=np.float64) + >>> L = coo_array((vals, (rows, cols)), shape=(N, N)) + >>> A = L + L.T # make it symmetric + >>> A.setdiag(N) # make it strongly positive definite + >>> A = A.tocsc() + >>> parent, post = etree(A, return_post=True) + >>> parent + array([ 5, 2, 7, 5, 7, 6, 8, 9, 9, 10, -1]) + >>> post + array([ 1, 2, 4, 7, 0, 3, 5, 6, 8, 9, 10]) + """ + cdef bint use_int32 + A, use_int32, out_itype = validate_csc_input(A) + + if kind is None: + kind = "sym" + + if kind not in {"sym", "row", "col", "lo"}: + raise ValueError(f"Unknown factorization kind: {kind}") + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if kind not in ["row", "col"] and M != N: + raise ValueError(f"Input matrix A must be square, got shape {A.shape}.") + + # Special Cases + # sym: A = (0, 0) + # row: AA.T = (0, N) * (N, 0) = (0, 0) + # col: A.TA = (0, M) * (M, 0) = (0, 0) + if kind == "row" and M == 0 or N == 0: + parent = np.array([], dtype=out_itype) + if return_post: + return parent, parent.copy() + else: + return parent + + if A.nnz == 0: + D = N if kind == "col" else M + parent = np.full(D, -1, dtype=out_itype) + if return_post: + post = np.arange(D, dtype=out_itype) + return parent, post + else: + return parent + + # ------------------------------------------------------------------------- + # Start the Analysis + # ------------------------------------------------------------------------- + cdef cholmod_common Common + cdef cholmod_common *cm = &Common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + cdef cholmod_sparse Amatrix + cdef cholmod_sparse* Ac = &Amatrix + + cdef int stype = 1 # default kind="sym" uses triu(A) only + N = A.shape[0] + cdef bint col_etree = False + + if kind == "row": + stype = 0 # use A * A.T + elif kind == "col": + N = A.shape[1] + stype = 0 # use A.T * A + col_etree = True + elif kind == "lo": + stype = -1 # use tril(A) only + + # Get sparse *pattern* + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + Ac.xtype = CHOLMOD_PATTERN + Ac.x = NULL + + # ------------------------------------------------------------------------- + # Compute the Outputs + # ------------------------------------------------------------------------- + cdef void *Parent + cdef void *Post + + if use_int32: + Parent = cholmod_malloc(N, sizeof(int32_t), cm) + else: + Parent = cholmod_l_malloc(N, sizeof(int64_t), cm) + + cdef cholmod_sparse *Rc + + if Ac.stype == 1 or col_etree: + # symmetric case: etree(A), using triu(A) + # column case: column etree of A, which is etree(A.T @ A) + if use_int32: + cholmod_etree(Ac, Parent, cm) + else: + cholmod_l_etree(Ac, Parent, cm) + else: + # symmetric case: etree(A), using tril(A) + # row case: row etree of A, which is etree(A @ A.T) + # R = A.T + if use_int32: + Rc = cholmod_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + cholmod_etree(Rc, Parent, cm) + cholmod_free_sparse(&Rc, cm) + else: + Rc = cholmod_l_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + cholmod_l_etree(Rc, Parent, cm) + cholmod_l_free_sparse(&Rc, cm) + + _handle_errors(cm.status) + + # Get the ndarray to return + parent = _ndarray_copy_from_intptr(Parent, N, use_int32) + + if return_post: + if use_int32: + Post = cholmod_malloc(N, sizeof(int32_t), cm) + if cholmod_postorder(Parent, N, NULL, Post, cm) != N: + raise CholmodError("Postordering failed.") + else: + Post = cholmod_l_malloc(N, sizeof(int64_t), cm) + if cholmod_l_postorder(Parent, N, NULL, Post, cm) != N: + raise CholmodError("Postordering failed.") + + post = _ndarray_copy_from_intptr(Post, N, use_int32) + + # Free memory (arrays are copied to numpy) + if use_int32: + cholmod_free(N, sizeof(int32_t), Parent, cm) + if return_post: + cholmod_free(N, sizeof(int32_t), Post, cm) + cholmod_finish(cm) + else: + cholmod_l_free(N, sizeof(int64_t), Parent, cm) + if return_post: + cholmod_l_free(N, sizeof(int64_t), Post, cm) + cholmod_l_finish(cm) + + if return_post: + return parent, post + else: + return parent + + +# ----------------------------------------------------------------------------- +# Partition Functions +# ----------------------------------------------------------------------------- +def bisect(A, *, kind=None): + """Compute a node separator for a sparse matrix graph. + + Parameters + ---------- + A : (M, N) csc_array + The input matrix in Compressed Sparse Column (CSC) format. Must be + square and symmetric if ``kind`` is None or ``"sym"``. No check is made + for symmetry. + kind : str in {"sym", "row", "col"}, optional + The type of factorization for which to analyze the matrix: + + * ``sym``: Symmetric factorization. Only the upper triangular part of + ``A`` is used, and no check is made for symmetry. + * ``row``: Unsymmetric factorization of :math:`A A^{\\top}`. + * ``col``: Unsymmetric factorization of :math:`A^{\\top} A`. + + If ``kind`` is None, it defaults to ``sym``. + + Returns + ------- + s : (K,) ndarray of int + The dimension ``K`` is either ``M`` or ``N``, depending on the + ``kind`` parameter. The output can take 3 values: + + * ``0``: The node is in the left subgraph. + * ``1``: The node is in the right subgraph. + * ``2``: The node is in the separator. + + See Also + -------- + nesdis, metis + + Notes + ----- + This function is based on the SuiteSparse CHOLMOD MATLAB interface + [#bisect_c]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#bisect_c] ``bisect.c`` - CHOLMOD MATLAB bisect function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/bisect.c + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import coo_array + >>> from sksparse.cholmod import bisect + >>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) + >>> N = 11 + >>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + >>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + >>> rng = np.random.default_rng(56) + >>> vals = rng.random(len(rows), dtype=np.float64) + >>> L = coo_array((vals, (rows, cols)), shape=(N, N)) + >>> A = L + L.T # make it symmetric + >>> A.setdiag(N) # make it strongly positive definite + >>> A = A.tocsc() + >>> s = bisect(A) + >>> s + array([0, 1, 1, 0, 1, 0, 0, 1, 0, 2, 2]) + """ + cdef bint use_int32 + A, use_int32, out_itype = validate_csc_input(A) + + if kind is None: + kind = "sym" + + if kind not in {"sym", "row", "col"}: + raise ValueError(f"Unknown factorization kind: {kind}") + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if kind not in ["row", "col"] and M != N: + raise ValueError(f"Input matrix A must be square, got shape {A.shape}.") + + # Special Cases + # sym: A = (0, 0) + # row: AA.T = (0, N) * (N, 0) = (0, 0) + # col: A.TA = (0, M) * (M, 0) = (0, 0) + if kind == "row" and M == 0 or N == 0: + return np.array([], dtype=out_itype) + + if A.nnz == 0: + D = N if kind == "col" else M + s = np.empty(D, dtype=out_itype) + k = D // 2 + s[:k] = 0 # left subgraph + s[k:] = 1 # right subgraph + s[-1] = 2 # separator + return s + + # ------------------------------------------------------------------------- + # Start the Analysis + # ------------------------------------------------------------------------- + cdef cholmod_common Common + cdef cholmod_common *cm = &Common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + cdef cholmod_sparse Amatrix + cdef cholmod_sparse* Ac = &Amatrix + + cdef int stype = -1 # default kind="sym" uses tril(A) only + cdef bint transpose = False + + if kind == "row": + stype = 0 # use A * A.T + elif kind == "col": + stype = 0 # use A.T * A + transpose = True + elif kind == "lo": + stype = -1 # use tril(A) only + + # Get sparse *pattern* + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + Ac.xtype = CHOLMOD_PATTERN + Ac.x = NULL + + # ------------------------------------------------------------------------- + # Compute the Outputs + # ------------------------------------------------------------------------- + cdef void *Partition + cdef cholmod_sparse *C + cdef int64_t ok + + if transpose: + # C = A.T, then bisect C @ C.T + if use_int32: + C = cholmod_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + N = C.nrow + Partition = cholmod_malloc(N, sizeof(int32_t), cm) + ok = (cholmod_bisect(C, NULL, 0, True, Partition, cm) >= 0) + cholmod_free_sparse(&C, cm) + else: + C = cholmod_l_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + N = C.nrow + Partition = cholmod_l_malloc(N, sizeof(int64_t), cm) + ok = (cholmod_l_bisect(C, NULL, 0, True, Partition, cm) >= 0) + cholmod_l_free_sparse(&C, cm) + else: + N = Ac.nrow + if use_int32: + Partition = cholmod_malloc(N, sizeof(int32_t), cm) + ok = (cholmod_bisect(Ac, NULL, 0, True, Partition, cm) >= 0) + else: + Partition = cholmod_l_malloc(N, sizeof(int64_t), cm) + ok = (cholmod_l_bisect(Ac, NULL, 0, True, Partition, cm) >= 0) + + if not ok: + raise CholmodError("Bisecting failed.") + + # Get the ndarray to return + s = _ndarray_copy_from_intptr(Partition, N, use_int32) + + # Free memory (arrays are copied to numpy) + if use_int32: + cholmod_free(N, sizeof(int32_t), Partition, cm) + cholmod_finish(cm) + else: + cholmod_l_free(N, sizeof(int64_t), Partition, cm) + cholmod_l_finish(cm) + + return s + + +class SeparatorTree(): + """The separator tree of a sparse matrix graph. + + This object is typically created by :func:`.nesdis`. + + Attributes + ---------- + cp : *(C,)* numpy.ndarray of int, optional + The separator tree, where ``C`` is the number of components found. The + value ``cp[c]`` is the parent of the component ``c`` in the separator + tree, or ``-1`` if ``c`` is the root of the tree. There is a maximum of + ``N`` components, where ``N`` is the dimension of the input matrix. + cmember : *(N,)* numpy.ndarray of int, optional + The component membership vector, where ``cmember[i]`` is the component + to which node ``i`` belongs. + + + .. versionadded:: 0.5.0 + """ + def __init__(self, cp, cmember): + self._cp = np.ascontiguousarray(cp) + self._cmember = np.ascontiguousarray(cmember) + + @property + def cp(self): + return self._cp + + @property + def cmember(self): + return self._cmember + + def __repr__(self): + return f"SeparatorTree(components={len(self._cp)}, nodes={len(self._cmember)})" + + def prune(self, *, nd_oksep=None, nd_small=None): + """Prune the separator tree. + + Parameters + ---------------- + nd_oksep : double in [0, 1], optional + Controls when a separator is kept. A separator is kept if + ``nsep < nd_oksep * n``, where ``nsep`` is the number of nodes in the + separator and ``n`` is the number of nodes in the graph being cut + (default is 1.0). + nd_small : int >= 0, optional + The smallest subgraph that should not be partitioned (default is 200). + + Returns + ------- + pruned_septree : SeparatorTree + The pruned separator tree. ``cp`` will be of length ``C'``, where + ``C' <= C`` is the number of components remaining after pruning. + + Notes + ----- + This function is based on the SuiteSparse CHOLMOD MATLAB interface + [#septree_c]_. + + References + ---------- + .. [#septree_c] ``septree.c`` - CHOLMOD MATLAB septree function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/septree.c + """ + if nd_oksep is None: + nd_oksep = 1.0 # see CHOLMOD/MATLAB/nesdis.c + + if nd_small is None: + nd_small = 200 # see CHOLMOD/MATLAB/nesdis.c + + return self._prune(self._cp, self._cmember, nd_oksep, nd_small) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _prune( + self, + index_t[::1] cp not None, + index_t[::1] cmember not None, + double nd_oksep, + Py_ssize_t nd_small + ): + cdef bint use_int32 = index_t is int32_t + + cdef cholmod_common Common + cdef cholmod_common *cm = &Common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + cdef Py_ssize_t Nc = cp.shape[0] + cdef Py_ssize_t N = cmember.shape[0] + + # Copy input arrays into new cholmod arrays (modified for output) + cdef index_t *CParent + cdef index_t *CMember + + ch_malloc = cholmod_malloc if use_int32 else cholmod_l_malloc + CParent = ch_malloc(Nc, sizeof(index_t), cm) + CMember = ch_malloc(N, sizeof(index_t), cm) + memcpy(CParent, &cp[0], Nc * sizeof(index_t)) + memcpy(CMember, &cmember[0], N * sizeof(index_t)) + + cdef int64_t nc_new + + if use_int32: + nc_new = cholmod_collapse_septree( + N, Nc, nd_oksep, nd_small, CParent, CMember, cm + ) + else: + nc_new = cholmod_l_collapse_septree( + N, Nc, nd_oksep, nd_small, CParent, CMember, cm + ) + + if nc_new < 0: + raise CholmodError("Pruning the separator tree failed.") + + # Get the ndarrays to return + cp_out = _ndarray_copy_from_intptr(CParent, nc_new, use_int32) + cmember_out = _ndarray_copy_from_intptr(CMember, N, use_int32) + + # Free memory (arrays are copied to numpy) + if use_int32: + cholmod_free(Nc, sizeof(int32_t), CParent, cm) + cholmod_free(N, sizeof(int32_t), CMember, cm) + cholmod_finish(cm) + else: + cholmod_l_free(Nc, sizeof(int64_t), CParent, cm) + cholmod_l_free(N, sizeof(int64_t), CMember, cm) + cholmod_l_finish(cm) + + return SeparatorTree(cp_out, cmember_out) + + +def nesdis( + A, + *, + kind=None, + bint return_separator=False, + nd_small=None, + nd_components=None, + nd_oksep=None, + nd_camd=None, +): + """Nested dissection ordering of a sparse matrix. + + Parameters + ---------- + A : (M, N) csc_array + The input matrix in Compressed Sparse Column (CSC) format. Must be + square and symmetric if ``kind`` is None or ``"sym"``. No check is made + for symmetry. + kind : str in {"sym", "row", "col"}, optional + The type of factorization for which to analyze the matrix: + + * ``sym``: Symmetric factorization. Only the upper triangular part of + ``A`` is used, and no check is made for symmetry. + * ``row``: Unsymmetric factorization of :math:`A A^{\\top}`. + * ``col``: Unsymmetric factorization of :math:`A^{\\top} A`. + + If ``kind`` is None, it defaults to ``sym``. + return_separator : bool, optional + If True, the function returns the separator tree and component + membership vector. Default is False. + + Returns + ------- + p : (M or N,) ndarray of int + The permutation vector that gives the nested dissection ordering of the + nodes in the graph represented by the sparse matrix ``A``. + septree : SeparatorTree, optional + The separator tree and component membership vector, returned if + ``return_separator`` is True. + + Other Parameters + ---------------- + nd_small : int, optional + The smallest subgraph that should not be partitioned (default is 200). + nd_components : bool, optional + True if connected components should be split independently (default is + False). + nd_oksep : double, optional + Controls when a separator is kept. A separator is kept if + ``nsep < nd_oksep * n``, where ``nsep`` is the number of nodes in the + separator and ``n`` is the number of nodes in the graph being cut + (default is 1). + nd_camd : int, optional + Controls whether the smallest subgraphs should be ordered. If 0, they + are not ordered. For the "sym" case, 1 to order by ``camd``, 2 to order + by ``csymamd`` (default 1). For other cases: 0 to order naturally, or + 1 to order by ``colamd``. + + See Also + -------- + bisect, metis + + Notes + ----- + This function is based on the SuiteSparse CHOLMOD MATLAB interface + [#nesdis_c]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#nesdis_c] ``nesdis.c`` - CHOLMOD MATLAB nesdis function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/nesdis.c + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import coo_array + >>> from sksparse.cholmod import nesdis + >>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) + >>> N = 11 + >>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + >>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + >>> rng = np.random.default_rng(56) + >>> vals = rng.random(len(rows), dtype=np.float64) + >>> L = coo_array((vals, (rows, cols)), shape=(N, N)) + >>> A = L + L.T # make it symmetric + >>> A.setdiag(N) # make it strongly positive definite + >>> A = A.tocsc() + >>> p, s = nesdis(A, return_separator=True) + >>> p + array([ 1, 4, 6, 8, 0, 3, 5, 2, 9, 10, 7]) + >>> s + SeparatorTree(components=1, nodes=11) + """ + cdef bint use_int32 + A, use_int32, out_itype = validate_csc_input(A) + + if kind is None: + kind = "sym" + + if kind not in {"sym", "row", "col"}: + raise ValueError(f"Unknown factorization kind: {kind}") + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if kind not in ["row", "col"] and M != N: + raise ValueError(f"Input matrix A must be square, got shape {A.shape}.") + + # Special Cases + # sym: A = (0, 0) + # row: AA.T = (0, N) * (N, 0) = (0, 0) + # col: A.TA = (0, M) * (M, 0) = (0, 0) + if kind == "row" and M == 0 or N == 0: + p = np.array([], dtype=out_itype) + if return_separator: + cp = np.array([-1], dtype=out_itype) # only one component + cmember = np.array([], dtype=out_itype) + return p, SeparatorTree(cp, cmember) + else: + return p + + if A.nnz == 0: + D = N if kind == "col" else M + p = np.arange(D, dtype=out_itype) + if return_separator: + cp = np.array([-1], dtype=out_itype) # only one component + cmember = np.zeros(D, dtype=out_itype) + return p, SeparatorTree(cp, cmember) + else: + return p + + # ------------------------------------------------------------------------- + # Start the Analysis + # ------------------------------------------------------------------------- + cdef cholmod_common Common + cdef cholmod_common *cm = &Common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + # Set the options for nested dissection + if nd_small is not None: + cm.method[0].nd_small = nd_small + + if nd_components is not None: + cm.method[0].nd_components = nd_components + + if nd_oksep is not None: + cm.method[0].nd_oksep = nd_oksep + + if nd_camd is not None: + cm.method[0].nd_camd = nd_camd + + cdef cholmod_sparse Amatrix + cdef cholmod_sparse* Ac = &Amatrix + + cdef int stype = -1 # default kind="sym" uses tril(A) only + cdef bint transpose = False + + if kind == "row": + stype = 0 # use A * A.T + elif kind == "col": + stype = 0 # use A.T * A + transpose = True + elif kind == "lo": + stype = -1 # use tril(A) only + + # Get sparse *pattern* + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + Ac.xtype = CHOLMOD_PATTERN + Ac.x = NULL + + # ------------------------------------------------------------------------- + # Compute the Outputs + # ------------------------------------------------------------------------- + cdef void *Perm + cdef void *CParent + cdef void *CMember + cdef cholmod_sparse *C + cdef int64_t ncomp + + if transpose: + # C = A.T, then order C @ C.T + if use_int32: + C = cholmod_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + N = C.nrow + Perm = cholmod_malloc(N, sizeof(int32_t), cm) + CParent = cholmod_malloc(N, sizeof(int32_t), cm) + CMember = cholmod_malloc(N, sizeof(int32_t), cm) + ncomp = cholmod_nested_dissection( + C, NULL, 0, Perm, CParent, CMember, cm + ) + cholmod_free_sparse(&C, cm) + else: + C = cholmod_l_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + N = C.nrow + Perm = cholmod_l_malloc(N, sizeof(int64_t), cm) + CParent = cholmod_l_malloc(N, sizeof(int64_t), cm) + CMember = cholmod_l_malloc(N, sizeof(int64_t), cm) + ncomp = cholmod_l_nested_dissection( + C, NULL, 0, Perm, CParent, CMember, cm + ) + cholmod_l_free_sparse(&C, cm) + else: + N = Ac.nrow + if use_int32: + Perm = cholmod_malloc(N, sizeof(int32_t), cm) + CParent = cholmod_malloc(N, sizeof(int32_t), cm) + CMember = cholmod_malloc(N, sizeof(int32_t), cm) + ncomp = cholmod_nested_dissection( + Ac, NULL, 0, Perm, CParent, CMember, cm + ) + else: + Perm = cholmod_l_malloc(N, sizeof(int64_t), cm) + CParent = cholmod_l_malloc(N, sizeof(int64_t), cm) + CMember = cholmod_l_malloc(N, sizeof(int64_t), cm) + ncomp = cholmod_l_nested_dissection( + Ac, NULL, 0, Perm, CParent, CMember, cm + ) + + if ncomp < 0: + raise CholmodError("Nested dissection failed.") + + # Get the ndarrays to return + p = _ndarray_copy_from_intptr(Perm, N, use_int32) + cp = _ndarray_copy_from_intptr(CParent, ncomp, use_int32) + cmember = _ndarray_copy_from_intptr(CMember, N, use_int32) + + # Free memory (arrays are copied to numpy) + if use_int32: + cholmod_free(N, sizeof(int32_t), Perm, cm) + cholmod_free(N, sizeof(int32_t), CParent, cm) + cholmod_free(N, sizeof(int32_t), CMember, cm) + cholmod_finish(cm) + else: + cholmod_l_free(N, sizeof(int64_t), Perm, cm) + cholmod_free(N, sizeof(int64_t), CParent, cm) + cholmod_free(N, sizeof(int64_t), CMember, cm) + cholmod_l_finish(cm) + + if return_separator: + return p, SeparatorTree(cp, cmember) + else: + return p + + +def metis(A, *, kind=None): + """Nested dissection ordering of a sparse matrix using METIS. + + Parameters + ---------- + A : (M, N) csc_array + The input matrix in Compressed Sparse Column (CSC) format. Must be + square and symmetric if ``kind`` is None or ``"sym"``. No check is made + for symmetry. + kind : str in {"sym", "row", "col"}, optional + The type of factorization for which to analyze the matrix: + + * ``sym``: Symmetric factorization. Only the upper triangular part of + ``A`` is used, and no check is made for symmetry. + * ``row``: Unsymmetric factorization of :math:`A A^{\\top}`. + * ``col``: Unsymmetric factorization of :math:`A^{\\top} A`. + + If ``kind`` is None, it defaults to ``sym``. + + Returns + ------- + p : (M or N,) ndarray of int + The permutation vector that gives the nested dissection ordering of the + nodes in the graph represented by the sparse matrix ``A``. + + See Also + -------- + bisect, nesdis + + Notes + ----- + This function is based on the SuiteSparse CHOLMOD MATLAB interface + [#metis_c]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#metis_c] ``metis.c`` - CHOLMOD MATLAB metis function + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/CHOLMOD/MATLAB/metis.c + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import coo_array + >>> from sksparse.cholmod import metis + >>> # Create a symmetric positive definite matrix from (Davis, Eqn 2.1) + >>> N = 11 + >>> rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + >>> cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + >>> rng = np.random.default_rng(56) + >>> vals = rng.random(len(rows), dtype=np.float64) + >>> L = coo_array((vals, (rows, cols)), shape=(N, N)) + >>> A = L + L.T # make it symmetric + >>> A.setdiag(N) # make it strongly positive definite + >>> A = A.tocsc() + >>> p = metis(A) + >>> p + array([ 8, 3, 6, 0, 5, 2, 4, 7, 1, 9, 10]) + """ + cdef bint use_int32 + A, use_int32, out_itype = validate_csc_input(A) + + if kind is None: + kind = "sym" + + if kind not in {"sym", "row", "col"}: + raise ValueError(f"Unknown factorization kind: {kind}") + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if kind not in ["row", "col"] and M != N: + raise ValueError(f"Input matrix A must be square, got shape {A.shape}.") + + # Special Cases + # sym: A = (0, 0) + # row: AA.T = (0, N) * (N, 0) = (0, 0) + # col: A.TA = (0, M) * (M, 0) = (0, 0) + if kind == "row" and M == 0 or N == 0: + return np.array([], dtype=out_itype) + + if A.nnz == 0: + D = N if kind == "col" else M + return np.arange(D, dtype=out_itype) + + # ------------------------------------------------------------------------- + # Start the Analysis + # ------------------------------------------------------------------------- + cdef cholmod_common Common + cdef cholmod_common *cm = &Common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + cdef cholmod_sparse Amatrix + cdef cholmod_sparse* Ac = &Amatrix + + cdef int stype = -1 # default kind="sym" uses tril(A) only + cdef bint transpose = False + + if kind == "row": + stype = 0 # use A * A.T + elif kind == "col": + stype = 0 # use A.T * A + transpose = True + elif kind == "lo": + stype = -1 # use tril(A) only + + # Get sparse *pattern* + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + Ac.xtype = CHOLMOD_PATTERN + Ac.x = NULL + + # ------------------------------------------------------------------------- + # Compute the Outputs + # ------------------------------------------------------------------------- + cdef void *Perm + cdef cholmod_sparse *C + cdef bint postorder = True + cdef int64_t ok + + if transpose: + # C = A.T, then metis C @ C.T + if use_int32: + C = cholmod_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + N = C.nrow + Perm = cholmod_malloc(N, sizeof(int32_t), cm) + ok = cholmod_metis(C, NULL, 0, postorder, Perm, cm) + cholmod_free_sparse(&C, cm) + else: + C = cholmod_l_transpose(Ac, CHOLMOD_TRANS_PATTERN, cm) + N = C.nrow + Perm = cholmod_l_malloc(N, sizeof(int64_t), cm) + ok = cholmod_l_metis(C, NULL, 0, postorder, Perm, cm) + cholmod_l_free_sparse(&C, cm) + else: + N = Ac.nrow + if use_int32: + Perm = cholmod_malloc(N, sizeof(int32_t), cm) + ok = cholmod_metis(Ac, NULL, 0, postorder, Perm, cm) + else: + Perm = cholmod_l_malloc(N, sizeof(int64_t), cm) + ok = cholmod_l_metis(Ac, NULL, 0, postorder, Perm, cm) + + if not ok: + raise CholmodError("metis failed.") + + # Get the ndarray to return + p = _ndarray_copy_from_intptr(Perm, N, use_int32) + + # Free memory (arrays are copied to numpy) + if use_int32: + cholmod_free(N, sizeof(int32_t), Perm, cm) + cholmod_finish(cm) + else: + cholmod_l_free(N, sizeof(int64_t), Perm, cm) + cholmod_l_finish(cm) + + return p diff --git a/src/sksparse/colamd.pxd b/src/sksparse/colamd.pxd new file mode 100644 index 00000000..04cd3a3c --- /dev/null +++ b/src/sksparse/colamd.pxd @@ -0,0 +1,103 @@ +# Cython COLAMD header interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: colamd.pxd +# Created: 2025-07-31 09:45 +# ============================================================================= +# distutils: language = c +# cython: language_level=3 + +from libc.stddef cimport size_t +from libc.stdint cimport int32_t, int64_t +from libc.stdlib cimport calloc, free + + +cdef extern from "colamd.h": + ctypedef void* (*alloc_func)(size_t, size_t) + ctypedef void (*free_func)(void *) + + # Get all #defined constants + enum: + # sizes of input and output arrays + COLAMD_KNOBS + COLAMD_STATS + + # indices of knobs + COLAMD_DENSE_ROW + COLAMD_DENSE_COL + COLAMD_AGGRESSIVE + + # indices of stats + COLAMD_DEFRAG_COUNT + COLAMD_STATUS + COLAMD_INFO1 + COLAMD_INFO2 + COLAMD_INFO3 + + # return values of colamd + COLAMD_OK + COLAMD_OK_BUT_JUMBLED + COLAMD_ERROR_A_not_present + COLAMD_ERROR_p_not_present + COLAMD_ERROR_nrow_negative + COLAMD_ERROR_ncol_negative + COLAMD_ERROR_nnz_negative + COLAMD_ERROR_p0_nonzero + COLAMD_ERROR_A_too_small + COLAMD_ERROR_col_length_negative + COLAMD_ERROR_row_index_out_of_bounds + COLAMD_ERROR_out_of_memory + COLAMD_ERROR_internal_error + + size_t colamd_recommended(int32_t nnz, int32_t n_row, int32_t n_col) + size_t colamd_l_recommended(int64_t nnz, int64_t n_row, int64_t n_col) + + void colamd_set_defaults(double knobs[COLAMD_KNOBS]) + void colamd_l_set_defaults(double knobs[COLAMD_KNOBS]) + + int c_colamd "colamd"( + int32_t n_row, + int32_t n_col, + int32_t Alen, + int32_t A[], + int32_t p[], + double knobs[COLAMD_KNOBS], + int32_t stats[COLAMD_STATS] + ) + + int c_colamd_l "colamd_l"( + int64_t n_row, + int64_t n_col, + int64_t Alen, + int64_t A[], + int64_t p[], + double knobs[COLAMD_KNOBS], + int64_t stats[COLAMD_STATS] + ) + + int c_symamd "symamd"( + int32_t n, + int32_t A[], + int32_t p[], + int32_t perm[], + double knobs[COLAMD_KNOBS], + int32_t stats[COLAMD_STATS], + alloc_func allocate, + free_func release + ) + + int c_symamd_l "symamd_l"( + int64_t n, + int64_t A[], + int64_t p[], + int64_t perm[], + double knobs[COLAMD_KNOBS], + int64_t stats[COLAMD_STATS], + alloc_func allocate, + free_func release + ) diff --git a/src/sksparse/colamd.pyx b/src/sksparse/colamd.pyx new file mode 100644 index 00000000..dc630716 --- /dev/null +++ b/src/sksparse/colamd.pyx @@ -0,0 +1,603 @@ +# Cython COLAMD python interface +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: colamd.pyx +# Created: 2025-07-31 10:13 +# ============================================================================= + +""" +============================================================================ +Column Approximate Minimum Degree (COLAMD) Ordering (:mod:`sksparse.colamd`) +============================================================================ + +.. currentmodule:: sksparse.colamd + +.. versionadded:: 0.5.0 + +Python interface to the `Column Approximate Minimum Degree (COLAMD) +`_ ordering +algorithm. + + +.. _colamd-interface: + +Interface +--------- + +.. autosummary:: + :toctree: generated/ + + colamd - Function to compute the column ordering of any shape sparse matrix. + symamd - Function to compute the column ordering of a symmetric sparse matrix. + colamd_get_defaults - Get the default knobs for COLAMD. + + +.. _colamd-exceptions: + +Exceptions and Warnings +----------------------- + +.. autosummary:: + :toctree: generated/ + + COLAMDError - Base class for COLAMD errors. + COLAMDValueError - Raised when COLAMD encounters a value error. + COLAMDMemoryError - Raised when COLAMD runs out of memory. + COLAMDInternalError - Raised when COLAMD encounters an internal error. + COLAMDStats - Dataclass containing statistics about the ordering. + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse COLAMD `_ +* COLAMD Algorithm Publications: + + * T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, An approximate column + minimum degree ordering algorithm, *ACM Transactions on Mathematical + Software*, vol. 30, no. 3., pp. 353-376, 2004. + + * T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, Algorithm 836: COLAMD, + an approximate column minimum degree ordering algorithm, *ACM + Transactions on Mathematical Software*, vol. 30, no. 3., pp. 377-380, + 2004. + +""" + +cimport cython + +import numpy as np +from dataclasses import dataclass + +from .utils import validate_csc_input + +__all__ = [ + "COLAMDError", + "COLAMDValueError", + "COLAMDMemoryError", + "COLAMDInternalError", + "COLAMDStats", + "colamd", + "symamd", + "colamd_get_defaults" +] + + +ctypedef fused index_t: + int32_t + int64_t + + +class COLAMDError(Exception): + """Base class for COLAMD errors.""" + pass + + +class COLAMDValueError(COLAMDError, ValueError): + """Raised when COLAMD encounters a value error.""" + pass + + +class COLAMDMemoryError(COLAMDError, MemoryError): + """Raised when COLAMD runs out of memory.""" + pass + + +class COLAMDInternalError(COLAMDError, RuntimeError): + """Raised when COLAMD encounters an internal error.""" + pass + + +# Define COLAMD error codes +cdef dict _COLAMD_ERROR_CODES = { + COLAMD_OK: "ok", + COLAMD_OK_BUT_JUMBLED: "ok but A has unsorted columns or duplicate entries", + COLAMD_ERROR_A_not_present: "A is a null pointer", + COLAMD_ERROR_p_not_present: "p is a null pointer", + COLAMD_ERROR_nrow_negative: "nrow is negative", + COLAMD_ERROR_ncol_negative: "ncol is negative", + COLAMD_ERROR_nnz_negative: "nnz is negative", + COLAMD_ERROR_p0_nonzero: "p[0] is nonzero", + COLAMD_ERROR_A_too_small: "A is too small", + COLAMD_ERROR_col_length_negative: "column has a negative number of entries", + COLAMD_ERROR_row_index_out_of_bounds: "row index out of bounds", + COLAMD_ERROR_out_of_memory: "out of memory", + COLAMD_ERROR_internal_error: "internal error" +} + + +cdef int _handle_errors(int ok, index_t[::1] stats) except -1 with gil: + """Handle errors from COLAMD based on the return status and stats array.""" + if ok: + assert stats[COLAMD_STATUS] == COLAMD_OK, \ + "COLAMD returned OK but status is not COLAMD_OK." + else: + if stats[COLAMD_STATUS] == COLAMD_ERROR_out_of_memory: + raise COLAMDMemoryError("COLAMD ran out of memory.") + elif stats[COLAMD_STATUS] == COLAMD_ERROR_internal_error: + raise COLAMDInternalError("COLAMD encountered an internal error.") + else: + raise COLAMDValueError( + f"COLAMD returned an error:{_COLAMD_ERROR_CODES[stats[COLAMD_STATUS]]}." + ) + + +@dataclass(frozen=True) +class COLAMDStats: + """Information statistics returned by the COLAMD algorithm. + + This class wraps the contents of the ``stats`` array returned by + C ``colamd()`` into a Python dataclass. + + Attributes + ---------- + N_rows_ignored : int + The number of dense or empty rows ignored in the ordering. + N_cols_ignored : int + The number of dense or empty columns ignored in the ordering. + Ncmpa : int + The number of garbage collections performed. + status : int + Status code indicating the result of the COLAMD operation. If non-zero, + ``colamd`` will throw an appropriate exception that interprets this + status code. + + The following fields take on different meanings depending on the value of + ``status``: + + info1 : int + Value of ``status``: + + * 0: the highest numbered column that is unsorted or has + duplicate entries. + * -3: the value of ``n_row``. + * -4: the value of ``n_col``. + * -5: the value of ``nnz == p[n_col]``. + * -6: the value of ``p[0]``. + * -7: the required ``Alen`` value. + * -8: the column with negative entries. + * -9: the column with a row index out of bounds. + info2 : int + Value of ``status``: + + * 0: the last seen duplicate or unsorted row index. + * -7: the actual ``Alen`` value. + * -9: the bad row index. + info3 : int + Value of ``status``: + + * 0: the number of duplicates or unsorted row indices. + * -9: ``n_row``. + + Notes + ----- + Field descriptions are adapted from SuiteSparse ``colamd.c`` + [#colamd_fields]_. + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#colamd_fields] ``colamd.c`` - SuiteSparse AMD source file. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/COLAMD/Source/colamd.c + """ + N_rows_ignored : int + N_cols_ignored : int + Ncmpa : int + status : int + info1 : int + info2 : int + info3 : int + + @classmethod + def from_array(cls, stats: "np.ndarray") -> "COLAMDStats": + """Create a COLAMDStats instance from an array.""" + return cls( + N_rows_ignored=int(stats[COLAMD_DENSE_ROW]), + N_cols_ignored=int(stats[COLAMD_DENSE_COL]), + Ncmpa=int(stats[COLAMD_DEFRAG_COUNT]), + status=int(stats[COLAMD_STATUS]), + info1=int(stats[COLAMD_INFO1]), + info2=int(stats[COLAMD_INFO2]), + info3=int(stats[COLAMD_INFO3]), + ) + + +def _colamd_base( + object A, + *, + bint is_symmetric=False, + object dense_row_thresh=None, + object dense_col_thresh=None, + object aggressive=None, + bint return_info=False +): + """A common base function for colamd and symamd.""" + A, _, out_dtype = validate_csc_input(A, is_symmetric) + + cdef Py_ssize_t M = A.shape[0] + cdef Py_ssize_t N = A.shape[1] + + if M == 0 or N == 0: + return np.empty(0, dtype=out_dtype) + + if A.nnz == 0 or (not is_symmetric and M == 1): + return np.arange(N, dtype=out_dtype) + + if N == 1: + return np.zeros(N, dtype=out_dtype) + + # Set the default knobs + knobs = np.zeros(COLAMD_KNOBS, dtype=np.double) + cdef double[::1] knobs_view = knobs + colamd_set_defaults(&knobs_view[0]) + + # Override with user knobs if provided + if dense_row_thresh is not None: + knobs_view[COLAMD_DENSE_ROW] = dense_row_thresh + + if dense_col_thresh is not None: + knobs_view[COLAMD_DENSE_COL] = dense_col_thresh + + if aggressive is not None: + knobs_view[COLAMD_AGGRESSIVE] = 1.0 if aggressive else 0.0 + + # Allocate output arrays + perm = np.zeros(N + 1, dtype=out_dtype) + stats = np.zeros(COLAMD_STATS, dtype=out_dtype) + + # Compute the ordering + if is_symmetric: + _symamd(M, N, A.indptr, A.indices, perm, knobs_view, stats) + else: + _colamd(M, N, A.indptr, A.indices, perm, knobs_view, stats) + + # Return the permutation array + q = np.asarray(perm[:N]) + + if return_info: + return q, COLAMDStats.from_array(stats) + else: + return q + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _colamd( + Py_ssize_t M, + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + index_t[::1] perm, + double[::1] knobs, + index_t[::1] stats +): + """Internal Cython wrapper for COLAMD. + + Parameters + ---------- + M : int + Number of rows in the matrix. + N : int + Number of columns in the matrix. + Ap : array_like + Column pointer array of size (N + 1,). + Ai : array_like + Row index array of size (nnz,). + perm : array_like + Output permutation array of size (N + 1,). + knobs : array_like + Knobs array of size (COLAMD_KNOBS,). + stats : array_like + Stats array of size (COLAMD_STATS,). + """ + cdef int ok + + # Get the recommended size for the Alen array + cdef index_t Alen = 0 + cdef Py_ssize_t nnz = Ai.shape[0] + + if index_t is int32_t: + Alen = colamd_recommended(nnz, M, N) + else: + Alen = colamd_l_recommended(nnz, M, N) + + if Alen == 0: + raise ValueError("Recommended Alen is zero: one of {A.nnz, M, N} is erroneous.") + + assert Alen >= nnz, "Recommended Alen is less than nnz." + + # Copy the input arrays, since they are altered in the C function + itype = np.int32 if index_t is int32_t else np.int64 + cdef index_t[::1] Ai_work = np.zeros(Alen, dtype=itype) + Ai_work[:nnz] = Ai + perm[:] = Ap + + # Compute the ordering + if index_t is int32_t: + ok = c_colamd(M, N, Alen, &Ai_work[0], &perm[0], &knobs[0], &stats[0]) + else: + ok = c_colamd_l(M, N, Alen, &Ai_work[0], &perm[0], &knobs[0], &stats[0]) + + _handle_errors(ok, stats) + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _symamd( + Py_ssize_t M, + Py_ssize_t N, + index_t[::1] Ap, + index_t[::1] Ai, + index_t[::1] perm, + double[::1] knobs, + index_t[::1] stats +): + """Internal Cython wrapper for SYMAMD. + + Parameters + ---------- + M : int + Number of rows in the matrix. + N : int + Number of columns in the matrix. + Ap : array_like + Column pointer array of size (N + 1,). + Ai : array_like + Row index array of size (nnz,). + perm : array_like + Output permutation array of size (N + 1,). + knobs : array_like + Knobs array of size (COLAMD_KNOBS,). + stats : array_like + Stats array of size (COLAMD_STATS,). + """ + cdef int ok + + # Compute the ordering + if index_t is int32_t: + ok = c_symamd(N, &Ai[0], &Ap[0], &perm[0], &knobs[0], &stats[0], calloc, free) + else: + ok = c_symamd_l(N, &Ai[0], &Ap[0], &perm[0], &knobs[0], &stats[0], calloc, free) + + _handle_errors(ok, stats) + + +def colamd( + A, + dense_row_thresh=None, + dense_col_thresh=None, + aggressive=None, + return_info=False +): + return _colamd_base( + A, + is_symmetric=False, + dense_row_thresh=dense_row_thresh, + dense_col_thresh=dense_col_thresh, + aggressive=aggressive, + return_info=return_info + ) + + +def symamd( + A, + dense_row_thresh=None, + dense_col_thresh=None, + aggressive=None, + return_info=False +): + return _colamd_base( + A, + is_symmetric=True, + dense_row_thresh=dense_row_thresh, + dense_col_thresh=dense_col_thresh, + aggressive=aggressive, + return_info=return_info + ) + + +_COLAMD_DOC_TEMPLATE = """ +{intro} +Parameters +---------- +{A_param} +dense_row_thresh, dense_col_thresh : float, optional + Threshold for considering a row/column dense. If + None, use the default value from COLAMD. The default value is 10. + The actual number of entries in a row/column is to be considered + "dense" is ``max(dense_row_thresh * sqrt(M), 16)`` where ``M`` is the + number of rows (or ``N`` for columns). Dense rows/columns are ignored + during ordering and moved to the end of the matrix. +aggressive : bool, optional + If True, use aggressive absorption. If None, uses the default value + from COLAMD. The default value is True. + +Returns +------- +q : (N,) :class:`~numpy.ndarray` + The permutation vector. +stats : :class:`COLAMDStats`, optional + If ``return_info`` is True, returns an object containing statistics + about the ordering. + +See Also +-------- +{see_also} + + +.. versionadded:: 0.5.0 + +References +---------- +.. {reftag} ``colamd.c`` - SuiteSparse AMD source file. + https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/COLAMD/Source/colamd.c + +Examples +-------- +{example} +""" + +# Define the docstrings +_colamd_reftag = "[#colamd_c]" + +_colamd_intro = f"""Compute the column approximate minimum degree ordering of +a sparse matrix. + +Adapted from the COLAMD documentation {_colamd_reftag}_: + + This function computes a column ordering for a sparse matrix `A` that + is appropriate for LU factorization of symmetric or unsymmetric + matrices, QR factorization, least squares, interior point methods for + linear programming problems, and other related problems. + + COLAMD computes a permutation `Q` such that the Cholesky factorization + of :math:`(AQ)^{{\\top}}(AQ)` has less fill-in and requires fewer floating + point operations than :math:`A^{{\\top}}A`. This also provides a good + ordering for sparse partial pivoting methods, :math:`P(AQ) = LU`, where + `Q` is computed prior to numerical factorization, and `P` is computed + during numerical factorization via conventional partial pivoting with + row interchanges. +""" + +_colamd_A_param = """A : (M, N) array_like or sparse matrix + The input matrix for which to compute the column ordering. + Must be 2D and convertible to CSC format. Need not be square.""" + +_colamd_example = """\ +>>> import numpy as np +>>> from scipy.sparse import random_array +>>> from sksparse.colamd import colamd +>>> # Create a non-symmetric matrix +>>> N = 11 +>>> rng = np.random.default_rng(56) +>>> A = random_array((N, N - 3), density=0.5, format='csc', rng=rng) +>>> A.setdiag(N) # make the diagonal non-zero +>>> p, info = colamd(A, return_info=True) +>>> p +array([0, 3, 5, 6, 7, 1, 2, 4], dtype=int32) +>>> info +COLAMDStats(N_rows_ignored=0, N_cols_ignored=0, Ncmpa=0, status=0, info1=-1, + info2=-1, info3=0) +""" + +colamd.__doc__ = _COLAMD_DOC_TEMPLATE.format( + intro=_colamd_intro, + A_param=_colamd_A_param, + see_also="symamd, ~sksparse.ccolamd.ccolamd, ~sksparse.ccolamd.csymamd", + reftag=_colamd_reftag, + example=_colamd_example, +) + + +# Define the docstring for symamd +_symamd_reftag = "[#symamd_c]" + +_symamd_intro = f"""Compute the column approximate minimum degree ordering of +a sparse symmetric matrix. + +Adapted from the COLAMD documentation {_symamd_reftag}_: + + This function computes an approximate minimum degree ordering for + Cholesky factorization of symmetric matrices. + + Symamd computes a permutation `P` of a symmetric matrix `A` such that + the Cholesky factorization of :math:`PAP^{{\\top}}` has less fill-in and + requires fewer floating point operations than `A`. Symamd constructs + a matrix `M` such that :math:`M^{{\\top}}M` has the same nonzero pattern + of `A`, and then orders the columns of `M` using colamd. The column + ordering of `M` is then returned as the row and column ordering `P` of + `A`. +""" + +_symamd_A_param = """A : (N, N) {array_like, sparse matrix} + The input matrix for which to compute the column ordering. + Must be 2D, square, and convertible to CSC format. + + .. note:: + + This routine only accesses the lower triangular part of ``A``, + which is *assumed* to be symmetric. If it is not, the results may + be incorrect or undefined. + +""" + +_symamd_example = """\ +>>> import numpy as np +>>> from scipy.sparse import random_array +>>> from sksparse.colamd import symamd +>>> # Create a non-symmetric matrix +>>> N = 11 +>>> rng = np.random.default_rng(56) +>>> A = random_array((N, N - 3), density=0.5, format='csc', rng=rng) +>>> A.setdiag(N) # make the diagonal non-zero +>>> A = (A.T @ A).tocsc() # make it symmetric +>>> p, info = symamd(A, return_info=True) +>>> p +array([4, 6, 7, 0, 1, 2, 3, 5], dtype=int32) +>>> info +COLAMDStats(N_rows_ignored=0, N_cols_ignored=0, Ncmpa=0, status=0, info1=-1, + info2=-1, info3=0) +""" + +symamd.__doc__ = _COLAMD_DOC_TEMPLATE.format( + intro=_symamd_intro, + A_param=_symamd_A_param, + see_also="colamd, ~sksparse.ccolamd.ccolamd, ~sksparse.ccolamd.csymamd", + reftag=_symamd_reftag, + example=_symamd_example, +) + + +def colamd_get_defaults(): + """Get the default knobs for COLAMD. + + Returns + ------- + knobs : dict + A dictionary containing the default knobs for COLAMD. + + The keys are: + + * 'dense_row_thresh': Threshold for considering a row/column dense. + Rows with more than ``max(dense_row_thresh * sqrt(M), 16)`` entries + are permuted to the end of the matrix. + * 'dense_col_thresh': Like `dense_row_thresh`, but for columns. + * 'aggressive': Default value for the aggressive knob. + + + .. versionadded:: 0.5.0 + """ + knobs = np.zeros(COLAMD_KNOBS, dtype=np.double) + cdef double[::1] knobs_view = knobs + colamd_set_defaults(&knobs_view[0]) + return dict( + dense_row_thresh=knobs[COLAMD_DENSE_ROW], + dense_col_thresh=knobs[COLAMD_DENSE_COL], + aggressive=knobs[COLAMD_AGGRESSIVE] + ) diff --git a/src/sksparse/klu.pxd b/src/sksparse/klu.pxd new file mode 100644 index 00000000..3cbfdff8 --- /dev/null +++ b/src/sksparse/klu.pxd @@ -0,0 +1,533 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: klu.pxd +# Created: 2025-10-30 20:39 +# ============================================================================= +# distutils: language = c + +from libc.stdint cimport int32_t, int64_t +from libc.string cimport memcpy + + +cdef extern from "klu.h": + # Get all #define constants + enum: + KLU_OK + KLU_SINGULAR + KLU_OUT_OF_MEMORY + KLU_INVALID + KLU_TOO_LARGE + + # ctypedef int32_t (*user_order_func) (int32_t, int32_t*, int32_t*, int32_t*, struct klu_common*) + + ctypedef struct klu_common: + double tol + double memgrow + double initmem_amd + double initmem + double maxwork + int btf + int ordering + int scale + # user_order_func* user_order # TODO + # void *user_data + int halt_if_singular + int status + int nrealloc + int32_t structural_rank + int32_t numerical_rank + int32_t singular_col + int32_t noffdiag + double flops + double rcond + double condest + double rgrowth + double work + size_t memusage + size_t mempeak + + ctypedef struct klu_l_common: + double tol + double memgrow + double initmem_amd + double initmem + double maxwork + int btf + int ordering + int scale + # user_order_func* user_order # TODO + # void *user_data + int halt_if_singular + int status + int nrealloc + int64_t structural_rank + int64_t numerical_rank + int64_t singular_col + int64_t noffdiag + double flops + double rcond + double condest + double rgrowth + double work + size_t memusage + size_t mempeak + + ctypedef struct klu_symbolic: + double symmetry + double est_flops + double lnz + double unz + double *Lnz + int32_t n + int32_t nz + int32_t *P + int32_t *Q + int32_t *R + int32_t nzoff + int32_t nblocks + int32_t maxblock + int32_t ordering + int32_t do_btf + int32_t structural_rank + + ctypedef struct klu_l_symbolic: + double symmetry + double est_flops + double lnz + double unz + double *Lnz + int64_t n + int64_t nz + int64_t *P + int64_t *Q + int64_t *R + int64_t nzoff + int64_t nblocks + int64_t maxblock + int64_t ordering + int64_t do_btf + int64_t structural_rank + + ctypedef struct klu_numeric: + int32_t n + int32_t nblocks + int32_t lnz + int32_t unz + int32_t max_lnz_block + int32_t max_unz_block + int32_t *Pnum + int32_t *Pinv + int32_t *Lip + int32_t *Uip + int32_t *Llen + int32_t *Ulen + void **LUbx + size_t *LUsize + void *Udiag + double *Rs + size_t worksize + void *Work + void *Xwork + int32_t *Iwork + int32_t *Offp + int32_t *Offi + void *Offx + int32_t nzoff + + ctypedef struct klu_l_numeric: + int64_t n + int64_t nblocks + int64_t lnz + int64_t unz + int64_t max_lnz_block + int64_t max_unz_block + int64_t *Pnum + int64_t *Pinv + int64_t *Lip + int64_t *Uip + int64_t *Llen + int64_t *Ulen + void **LUbx + size_t *LUsize + void *Udiag + double *Rs + size_t worksize + void *Work + void *Xwork + int64_t *Iwork + int64_t *Offp + int64_t *Offi + void *Offx + int64_t nzoff + + # --------------------------------------------------------------------------------- + # Functions + # --------------------------------------------------------------------------------- + int klu_defaults(klu_common *Common) + int klu_l_defaults(klu_l_common *Common) + + klu_symbolic* klu_analyze( + int32_t n, + int32_t Ap[], + int32_t Ai[], + klu_common *Common + ) + + klu_l_symbolic* klu_l_analyze( + int64_t n, + int64_t Ap[], + int64_t Ai[], + klu_l_common *Common + ) + + # NOTE alias so we can use "def klu_factor(...)" externally + klu_numeric* c_klu_factor "klu_factor"( + int32_t Ap[], + int32_t Ai[], + double Ax[], + klu_symbolic *Symbolic, + klu_common *Common + ) + + klu_numeric *klu_z_factor( + int32_t Ap[], + int32_t Ai[], + double Ax[], + klu_symbolic *Symbolic, + klu_common *Common + ) + + klu_l_numeric *klu_l_factor( + int64_t Ap[], + int64_t Ai[], + double Ax[], + klu_l_symbolic *Symbolic, + klu_l_common *Common + ) + + klu_l_numeric *klu_zl_factor( + int64_t Ap[], + int64_t Ai[], + double Ax[], + klu_l_symbolic *Symbolic, + klu_l_common *Common + ) + + + int klu_refactor( + int32_t Ap[], + int32_t Ai[], + double Ax[], + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_z_refactor( + int32_t Ap[], + int32_t Ai[], + double Ax[], + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_l_refactor( + int64_t Ap[], + int64_t Ai[], + double Ax[], + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_zl_refactor( + int64_t Ap[], + int64_t Ai[], + double Ax[], + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int c_klu_solve "klu_solve"( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + int32_t ldim, + int32_t nrhs, + double B[], + klu_common *Common + ) + + int klu_z_solve( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + int32_t ldim, + int32_t nrhs, + double B[], + klu_common *Common + ) + + int klu_l_solve( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + int64_t ldim, + int64_t nrhs, + double B[], + klu_l_common *Common + ) + + int klu_zl_solve( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + int64_t ldim, + int64_t nrhs, + double B[], + klu_l_common *Common + ) + + int klu_tsolve( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + int32_t ldim, + int32_t nrhs, + double B[], + klu_common *Common + ) + + int klu_z_tsolve( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + int32_t ldim, + int32_t nrhs, + double B[], + int conj_solve, + klu_common *Common + ) + + int klu_l_tsolve( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + int64_t ldim, + int64_t nrhs, + double B[], + klu_l_common *Common + ) + + int klu_zl_tsolve( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + int64_t ldim, + int64_t nrhs, + double B[], + int conj_solve, + klu_l_common *Common + ) + + int klu_sort( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_z_sort( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_l_sort( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_zl_sort( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_rcond( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_z_rcond( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_l_rcond( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_zl_rcond( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_flops( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_z_flops( + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_l_flops( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_zl_flops( + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_rgrowth( + int32_t Ap[], + int32_t Ai[], + double Ax[], + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_z_rgrowth( + int32_t Ap[], + int32_t Ai[], + double Ax[], + klu_symbolic *Symbolic, + klu_numeric *Numeric, + klu_common *Common + ) + + int klu_l_rgrowth( + int64_t Ap[], + int64_t Ai[], + double Ax[], + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_zl_rgrowth( + int64_t Ap[], + int64_t Ai[], + double Ax[], + klu_l_symbolic *Symbolic, + klu_l_numeric *Numeric, + klu_l_common *Common + ) + + int klu_extract( + klu_numeric *Numeric, + klu_symbolic *Symbolic, + int32_t *Lp, + int32_t *Li, + double *Lx, + int32_t *Up, + int32_t *Ui, + double *Ux, + int32_t *Fp, + int32_t *Fi, + double *Fx, + int32_t *P, + int32_t *Q, + double *Rs, + int32_t *R, + klu_common *Common + ) + + int klu_z_extract( + klu_numeric *Numeric, + klu_symbolic *Symbolic, + int32_t *Lp, + int32_t *Li, + double *Lx, + double *Lz, + int32_t *Up, + int32_t *Ui, + double *Ux, + double *Uz, + int32_t *Fp, + int32_t *Fi, + double *Fx, + double *Fz, + int32_t *P, + int32_t *Q, + double *Rs, + int32_t *R, + klu_common *Common + ) + + int klu_l_extract( + klu_l_numeric *Numeric, + klu_l_symbolic *Symbolic, + int64_t *Lp, + int64_t *Li, + double *Lx, + int64_t *Up, + int64_t *Ui, + double *Ux, + int64_t *Fp, + int64_t *Fi, + double *Fx, + int64_t *P, + int64_t *Q, + double *Rs, + int64_t *R, + klu_l_common *Common + ) + + int klu_zl_extract( + klu_l_numeric *Numeric, + klu_l_symbolic *Symbolic, + int64_t *Lp, + int64_t *Li, + double *Lx, + double *Lz, + int64_t *Up, + int64_t *Ui, + double *Ux, + double *Uz, + int64_t *Fp, + int64_t *Fi, + double *Fx, + double *Fz, + int64_t *P, + int64_t *Q, + double *Rs, + int64_t *R, + klu_l_common *Common + ) + + int klu_free_symbolic(klu_symbolic **Symbolic, klu_common *Common) + int klu_l_free_symbolic(klu_l_symbolic **Symbolic, klu_l_common *Common) + + int klu_free_numeric(klu_numeric **Numeric, klu_common *Common) + int klu_z_free_numeric (klu_numeric **Numeric, klu_common *Common) + int klu_l_free_numeric (klu_l_numeric **Numeric, klu_l_common *Common) + int klu_zl_free_numeric (klu_l_numeric **Numeric, klu_l_common *Common) + + void *klu_malloc(size_t n, size_t size, klu_common *Common) + void *klu_l_malloc(size_t n, size_t size, klu_l_common *Common) + + void *klu_free(void *p, size_t n, size_t size, klu_common *Common) + void *klu_l_free(void *p, size_t n, size_t size, klu_l_common *Common) diff --git a/src/sksparse/klu.pyx b/src/sksparse/klu.pyx new file mode 100644 index 00000000..48f56d38 --- /dev/null +++ b/src/sksparse/klu.pyx @@ -0,0 +1,1977 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: klu.pyx +# Created: 2025-10-30 21:02 +# ============================================================================= + +""" +================================================= +Clark Kent LU Decomposition (:mod:`sksparse.klu`) +================================================= + +.. currentmodule:: sksparse.klu + +.. versionadded:: 0.5.0 + + +An interface to the SuiteSparse `KLU +`_ +package, which computes the LU factorization and solves systems of equations +for sparse, possibly non-symmetric, indefinite matrices. + + +Function Interface +------------------ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + klu_solve - Solve a linear system using the KLU factorization. + + +Object Interface +---------------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + klu_factor - Compute the LU factorization of a sparse matrix. + KLUFactor - An object-oriented interface to KLU. + KLUInfo - A dataclass to return KLU info. + KLUControl - A dataclass to set KLU control parameters. + + +.. klupack-exceptions: + +Warnings and Exceptions +----------------------- + +.. autosummary:: + :toctree: generated/ + + KLUWarning + KLUSingularMatrixWarning + + KLUError + KLUOutOfMemoryError + KLUInvalidError + KLUOverflowError + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse KLU `_ +""" + +cimport cython + +from copy import deepcopy +import numpy as np +from scipy.sparse import issparse, csc_array +import warnings + +from .utils import validate_csc_input + + +__all__ = [ + "KLUWarning", + "KLUSingularMatrixWarning", + "KLUError", + "KLUOutOfMemoryError", + "KLUInvalidError", + "KLUOverflowError", + "KLUInfo", + "KLUControl", + "KLUFactor", + "klu_factor", + "klu_solve", +] + + +# ----------------------------------------------------------------------------- +# Define types +# ----------------------------------------------------------------------------- +ctypedef fused index_t: + int32_t + int64_t + + +ctypedef fused value_t: + double + double complex + + +ctypedef fused common_t: + klu_common + klu_l_common + + +ctypedef fused symbolic_t: + klu_symbolic + klu_l_symbolic + + +ctypedef fused numeric_t: + klu_numeric + klu_l_numeric + + +ctypedef fused ctrl_t: + int + double + + +# ------------------------------------------------------------------------------------- +# Warnings and Errors +# ------------------------------------------------------------------------------------- +class KLUWarning(Warning): + """Base warning for KLU-related warnings.""" + pass + + +class KLUSingularMatrixWarning(KLUWarning): + """Warning raised when a singular matrix is encountered.""" + pass + + +class KLUError(Exception): + """Base exception for KLU-related errors.""" + pass + + +class KLUOutOfMemoryError(MemoryError, KLUError): + """Exception raised when KLU runs out of memory.""" + pass + + +class KLUInvalidError(KLUError): + """Exception raised for invalid inputs to KLU.""" + pass + + +class KLUOverflowError(OverflowError, KLUError): + """Exception raised when KLU encounters an overflow.""" + pass + + +# Known Errors +cdef dict _ERROR_INDEX = { + KLU_SINGULAR: (KLUSingularMatrixWarning, "The matrix is singular."), + KLU_OUT_OF_MEMORY: (KLUOutOfMemoryError, "KLU ran out of memory."), + KLU_INVALID: (KLUInvalidError, "An invalid input was provided to KLU."), + KLU_TOO_LARGE: (KLUOverflowError, "The matrix is too large for KLU to handle."), +} + + +cdef int _handle_errors(int status) except -1 with gil: + """Handle KLU errors by raising Python exceptions or warnings. + + This function should be called with the return ``status`` after any KLU + C function that may fail. + + Parameters + ---------- + status : int + The KLU exit status code. + + Returns + ------- + None + + Raises + ------ + :exc:`KLUWarning` or subclass + Raises a warning for non-critical issues. + :exc:`KLUError` or subclass + Raises an appropriate Python exception based on the KLU status code. + """ + if status == KLU_OK: + return 0 + + # Fallback to generic error for unknown codes + exc_class, msg = _ERROR_INDEX.get( + status, + (KLUError, "An unknown KLU error occurred.") + ) + full_msg = f"{msg} (code {status:d})" + + if issubclass(exc_class, Warning): + warnings.warn(full_msg, exc_class, stacklevel=2) + else: + raise exc_class(full_msg) + + +# ------------------------------------------------------------------------------------- +# KLU Control and Info Classes +# ------------------------------------------------------------------------------------- +cdef object _get_scale_string(int scale): + """Convert KLU row scaling integer to string.""" + if scale == -1: + return "none_no_check" + elif scale == 0: + return "none" + elif scale == 1: + return "sum" + elif scale == 2: + return "max" + else: + return "unknown" + + +cdef object _get_ordering_string(int ordering): + """Convert KLU ordering integer to string.""" + if ordering == 0: + return "AMD" + elif ordering == 1: + return "COLAMD" + elif ordering == 2: + return "user_perm" + elif ordering == 3: + return "user_func" + else: + return "" + + +@cython.dataclasses.dataclass(frozen=True) +cdef class KLUInfo: + """A dataclass to store KLU information. + + Attributes + ---------- + noffdiag : int + Number of off-diagonal entries in the matrix. + nrealloc : int + Number of memory reallocations during factorization. + rcond : float + Estimate of the reciprocal of the condition number. + singular_col : int + Index of the first singular column, if any. + rgrowth : float + The reciprocal pivot growth factor. + flops : int + Estimated number of floating-point operations. + nblocks : int + Number of blocks in the BTF ordering of the matrix. + ordering : str + The fill-reducing ordering used. + scale : str + The row-scaling method used. + lnz : int + Number of nonzeros in the L factor. + unz : int + Number of nonzeros in the U factor. + nzoff : int + Number of nonzeros in the F factor ("offset"). + tol : float + The pivot tolerance used. + mempeak : int + Peak memory usage in bytes. + """ + noffdiag : int | None = None + nrealloc : int | None = None + rcond : double | None = None + singular_col : int | None = None + rgrowth : double | None = None + flops : int | None = None + nblocks : int | None = None + ordering : str | None = None + scale : str | None = None + lnz : int | None = None + unz : int | None = None + nzoff : int | None = None + tol : double | None = None + mempeak : int | None = None + + cdef KLUInfo update_from_klu( + self, klu_symbolic* symbolic, klu_numeric* numeric, klu_common* cm + ): + """Update a KLUInfo object from KLU structs.""" + if cm is not NULL: + self.noffdiag = cm.noffdiag + self.nrealloc = cm.nrealloc + self.rcond = cm.rcond + self.singular_col = cm.singular_col + self.rgrowth = cm.rgrowth + self.flops = cm.flops + self.ordering = _get_ordering_string(cm.ordering) + self.scale = _get_scale_string(cm.scale) + self.tol = cm.tol + self.mempeak = cm.mempeak + + if symbolic is not NULL: + self.nblocks = symbolic.nblocks + + if numeric is not NULL: + self.lnz = numeric.lnz + self.unz = numeric.unz + self.nzoff = numeric.nzoff + + return self + + cdef KLUInfo update_from_l_klu( + self, klu_l_symbolic* symbolic, klu_l_numeric* numeric, klu_l_common* cm + ): + """Create a KLUInfo object from KLU structs.""" + if cm is not NULL: + self.noffdiag = cm.noffdiag + self.nrealloc = cm.nrealloc + self.rcond = cm.rcond + self.singular_col = cm.singular_col + self.rgrowth = cm.rgrowth + self.flops = cm.flops + self.ordering = _get_ordering_string(cm.ordering) + self.scale = _get_scale_string(cm.scale) + self.tol = cm.tol + self.mempeak = cm.mempeak + + if symbolic is not NULL: + self.nblocks = symbolic.nblocks + + if numeric is not NULL: + self.lnz = numeric.lnz + self.unz = numeric.unz + self.nzoff = numeric.nzoff + + return self + + +cdef dict _SCALE_INDEX = { + "none_no_check": -1, + "none": 0, + "sum": 1, + "max": 2, +} + + +cdef dict _ORDERING_INDEX = { + "AMD": 0, + "COLAMD": 1, + "user_perm": 2, + "user_func": 3, +} + + +cdef list _CONTROL_KEYS = [ + "tol", + "memgrow", + "initmem_amd", + "initmem", + "maxwork", + "btf", + "ordering", + "scale", +] + + +cdef class KLUControl: + """A dataclass to set KLU control parameters. + + Attributes + ---------- + tol : float + The pivot tolerance. Default is ``None``, which uses the ``KLU`` default of + ``0.001``. + memgrow : float + The memory growth factor. Default is ``None``, which uses the ``KLU`` + default of ``1.2``. + initmem_amd : float + The initial memory allocation factor for AMD. Default is ``None``, which + uses the ``KLU`` default of ``1.2``. + initmem : float + The initial memory allocation factor for the numeric factorization. + Default is ``None``, which uses the ``KLU`` default of ``10``. + maxwork : float + The maximum work done by BTF. Default is ``None``, which uses the ``KLU`` + default of ``0``, or unlimited. + btf : bool + Whether to use BTF pre-ordering. Default is ``None``, which uses the + ``KLU`` default of ``True``. + ordering : str + The fill-reducing ordering. Accepted values are: + + * ``AMD``: Approximate Minimum Degree ordering. + * ``COLAMD`` : Column Approximate Minimum Degree ordering. + * ``user_perm``: User-provided ordering (not yet supported). + * ``user_func``: User-defined ordering function (not yet supported). + + Default is ``None``, which uses the ``KLU`` default setting of ``AMD``. + scale : str + The row-scaling method. Accepted values are: + + * ``none_no_check`` + * ``none`` + * ``sum`` + * ``max`` + + Default is ``None``, which uses the ``KLU`` default setting of ``max``. + """ + cdef: + double _FLOAT_NONE + int _INT_NONE + double _tol + double _memgrow + double _initmem_amd + double _initmem + double _maxwork + bint _btf + int _ordering + int _scale + + def __cinit__(self, **kwargs): + """Initialize the KLUControl object.""" + self._FLOAT_NONE = -999.0 + self._INT_NONE = -999 + + self._tol = self._FLOAT_NONE + self._memgrow = self._FLOAT_NONE + self._initmem_amd = self._FLOAT_NONE + self._initmem = self._FLOAT_NONE + self._maxwork = self._FLOAT_NONE + self._btf = self._INT_NONE + self._ordering = self._INT_NONE + self._scale = self._INT_NONE + + for key, value in kwargs.items(): + try: + setattr(self, key, value) + except KeyError: + raise KeyError( + f"Invalid control parameter: {key}. " + f"Expected one of {self.__dict__.keys()}" + ) + + @property + def tol(self): + return None if self._tol == self._FLOAT_NONE else self._tol + + @tol.setter + def tol(self, value): + if value is None: + self._tol = self._FLOAT_NONE + else: + try: + assert 0.0 <= value <= 1.0 + except (AssertionError, TypeError): + raise ValueError("tol must be a float in the range [0, 1].") + self._tol = value + + @property + def memgrow(self): + return None if self._memgrow == self._FLOAT_NONE else self._memgrow + + @memgrow.setter + def memgrow(self, value): + if value is None: + self._memgrow = self._FLOAT_NONE + else: + try: + assert value > 1.0 + except (AssertionError, TypeError): + raise ValueError("memgrow must be a float greater than 1.0.") + self._memgrow = value + + @property + def initmem_amd(self): + return None if self._initmem_amd == self._FLOAT_NONE else self._initmem_amd + + @initmem_amd.setter + def initmem_amd(self, value): + if value is None: + self._initmem_amd = self._FLOAT_NONE + else: + try: + assert value > 1.0 + except (AssertionError, TypeError): + raise ValueError("initmem_amd must be a float greater than 1.0.") + self._initmem_amd = value + + @property + def initmem(self): + return None if self._initmem == self._FLOAT_NONE else self._initmem + + @initmem.setter + def initmem(self, value): + if value is None: + self._initmem = self._FLOAT_NONE + else: + try: + assert value > 0.0 + except (AssertionError, TypeError): + raise ValueError("initmem must be a float greater than 0.0.") + self._initmem = value + + @property + def maxwork(self): + return None if self._maxwork == self._FLOAT_NONE else self._maxwork + + @maxwork.setter + def maxwork(self, value): + self._maxwork = value if value is not None else self._FLOAT_NONE + + @property + def btf(self): + return None if self._btf == self._INT_NONE else self._btf + + @btf.setter + def btf(self, value): + self._btf = value if value is not None else self._INT_NONE + + @property + def ordering(self): + return _get_ordering_string(self._ordering) + + @ordering.setter + def ordering(self, value): + if value is None: + self._ordering = self._INT_NONE + return + + if value in ["user_perm", "user_func"]: + raise NotImplementedError( + f"The ordering method '{value}' is not yet supported." + ) + + try: + self._ordering = _ORDERING_INDEX[value] + except KeyError: + raise ValueError( + f"Invalid value for 'ordering': {value}. " + f"Expected one of {list(_ORDERING_INDEX.keys())}" + ) + + @property + def scale(self): + return _get_scale_string(self._scale) + + @scale.setter + def scale(self, value): + if value is None: + self._scale = self._INT_NONE + return + + try: + self._scale = _SCALE_INDEX[value] + except KeyError: + raise ValueError( + f"Invalid value for 'scale': {value}. " + f"Expected one of {list(_SCALE_INDEX.keys())}" + ) + + def __iter__(self): + cdef str k + for k in _CONTROL_KEYS: + yield (k, getattr(self, k)) + + def __repr__(self): + attrs = ",\n ".join(f"{k}={repr(v)}" for k, v in self) + return f"{self.__class__.__name__}(\n {attrs}\n)" + + def __str__(self): + return self.__repr__() + + +cdef inline void _set_if_not_none(ctrl_t *dest, ctrl_t src, ctrl_t none_value) noexcept: + """Set a pointer if the source is not equal to none_value.""" + dest[0] = dest[0] if src == none_value else src + +# ------------------------------------------------------------------------------------- +# Copy Functions +# ------------------------------------------------------------------------------------- +cdef inline void* _malloc_copy( + const void* src, + size_t n, + size_t size, + const common_t* cm +): + """Allocate memory and copy data from src to the new memory.""" + assert cm is not NULL + if src is NULL: + return NULL + + cdef void* dest + + if common_t is klu_common: + dest = klu_malloc(n, size, cm) + else: + dest = klu_l_malloc(n, size, cm) + + _handle_errors(cm.status) + + if dest is NULL: + return NULL + + if n > 0: + memcpy(dest, src, n * size) + + return dest + + +cdef inline void _copy_symbolic_values( + symbolic_t* dest, + const symbolic_t* src, +) noexcept: + """Copy the top-level values of a KLU symbolic struct, excluding pointers.""" + dest.symmetry = src.symmetry + dest.est_flops = src.est_flops + dest.lnz = src.lnz + dest.unz = src.unz + dest.n = src.n + dest.nz = src.nz + dest.nzoff = src.nzoff + dest.nblocks = src.nblocks + dest.maxblock = src.maxblock + dest.ordering = src.ordering + dest.do_btf = src.do_btf + dest.structural_rank = src.structural_rank + + +cdef int _copy_symbolic_base( + symbolic_t* dest, + const symbolic_t* src, + const common_t* cm, + index_t _dummy=0 +) except -1: + """Deep copy a KLU symbolic struct.""" + assert dest is not NULL + assert src is not NULL + assert cm is not NULL + + # Check for bad type combos to prune the fused types before compilation + # The compiler will generate *all* combinations of the fused type arguments, but + # only some are valid, so this statement will create unreachable code that then + # gets pruned away. + if not ( + (symbolic_t is klu_symbolic and + common_t is klu_common and + index_t is int32_t) + or + (symbolic_t is klu_l_symbolic and + common_t is klu_l_common and + index_t is int64_t) + ): + assert False + return 0 + + # Copy the top-level data, but *not* pointers + _copy_symbolic_values(dest, src) + + cdef size_t n = src.n + + # Deep copy internal arrays + dest.Lnz = _malloc_copy(src.Lnz, n, sizeof(double), cm) + dest.P = _malloc_copy(src.P, n, sizeof(index_t), cm) + dest.Q = _malloc_copy(src.Q, n, sizeof(index_t), cm) + dest.R = _malloc_copy(src.R, n + 1, sizeof(index_t), cm) + + return 0 + + +cdef int _copy_symbolic( + symbolic_t* dest, + const symbolic_t* src, + const common_t* cm +) except -1: + """Deep copy a KLU symbolic struct.""" + if symbolic_t is klu_symbolic: + return _copy_symbolic_base(dest, src, cm, 0) + else: + return _copy_symbolic_base(dest, src, cm, 0) + + +cdef inline void _copy_numeric_values( + numeric_t* dest, + const numeric_t* src, +) noexcept: + """Copy the top-level values of a KLU numeric struct, excluding pointers.""" + dest.n = src.n + dest.nblocks = src.nblocks + dest.lnz = src.lnz + dest.unz = src.unz + dest.max_lnz_block = src.max_lnz_block + dest.max_unz_block = src.max_unz_block + dest.worksize = src.worksize + dest.nzoff = src.nzoff + + +cdef int _copy_numeric_base( + numeric_t* dest, + const numeric_t* src, + const common_t* cm, + index_t _dummy_int=0, + value_t _dummy_val=0 +) except -1: + """Deep copy a KLU numeric struct.""" + assert dest is not NULL + assert src is not NULL + assert cm is not NULL + + # Check for bad type combos to prune the fused types before compilation + # The compiler will generate *all* combinations of the fused type arguments, but + # only some are valid, so this statement will create unreachable code that then + # gets pruned away. + if not ( + (numeric_t is klu_numeric and + common_t is klu_common and + index_t is int32_t) + or + (numeric_t is klu_l_numeric and + common_t is klu_l_common and + index_t is int64_t) + ): + assert False + return 0 + + # Copy the top-level data and pointers + _copy_numeric_values(dest, src) + + cdef size_t n = src.n + cdef size_t nblocks = src.nblocks + + # Deep copy internal arrays + dest.Pnum = _malloc_copy(src.Pnum, n, sizeof(index_t), cm) + dest.Pinv = _malloc_copy(src.Pinv, n, sizeof(index_t), cm) + dest.Lip = _malloc_copy(src.Lip, n, sizeof(index_t), cm) + dest.Uip = _malloc_copy(src.Uip, n, sizeof(index_t), cm) + dest.Llen = _malloc_copy(src.Llen, n, sizeof(index_t), cm) + dest.Ulen = _malloc_copy(src.Ulen, n, sizeof(index_t), cm) + dest.Udiag = _malloc_copy(src.Udiag, n, sizeof(value_t), cm) + dest.LUsize = _malloc_copy(src.LUsize, nblocks, sizeof(size_t), cm) + + if numeric_t is klu_numeric: + dest.LUbx = klu_malloc(nblocks, sizeof(value_t*), cm) + else: + dest.LUbx = klu_l_malloc(nblocks, sizeof(value_t*), cm) + + _handle_errors(cm.status) + + cdef size_t k + + if dest.LUbx is not NULL and src.LUbx is not NULL and src.LUsize is not NULL: + for k in range(nblocks): + dest.LUbx[k] = _malloc_copy( + src.LUbx[k], src.LUsize[k], sizeof(value_t), cm + ) + + cdef size_t np1 = n + 1 + cdef size_t nzoffp1 = src.nzoff + 1 + + dest.Offp = _malloc_copy(src.Offp, np1, sizeof(index_t), cm) + dest.Offi = _malloc_copy(src.Offi, nzoffp1, sizeof(index_t), cm) + dest.Offx = _malloc_copy(src.Offx, nzoffp1, sizeof(value_t), cm) + + dest.Rs = _malloc_copy(src.Rs, n, sizeof(double), cm) + + # Workspace encompasses Xwork and Iwork, so just copy Work + dest.Work = _malloc_copy(src.Work, src.worksize, 1, cm) + dest.Xwork = dest.Work + if dest.Xwork is not NULL: + dest.Iwork = (dest.Xwork + n) + + return 0 + + +cdef int _copy_numeric( + numeric_t* dest, + const numeric_t* src, + const common_t* cm, + bint is_real +) except -1: + """Deep copy a KLU numeric struct.""" + cdef int32_t idx = 0 + cdef int64_t l_idx = 0 + cdef double val = 0 + cdef double complex c_val = 0 + + if numeric_t is klu_numeric: + if is_real: + return _copy_numeric_base(dest, src, cm, idx, val) + else: + return _copy_numeric_base(dest, src, cm, idx, c_val) + else: + if is_real: + return _copy_numeric_base(dest, src, cm, l_idx, val) + else: + return _copy_numeric_base(dest, src, cm, l_idx, c_val) + + +# ------------------------------------------------------------------------------------- +# KLU Class Interface +# ------------------------------------------------------------------------------------- +cdef class KLUFactor: + r"""Class to compute and store the KLU factorization of a sparse matrix. + + The constructor computes the symbolic analysis of a sparse matrix :math:`A` + and determines a fill-reducing ordering such that: + + .. math:: + L U + F = R P A Q. + + The numeric factorization is not computed until :meth:`.factorize` is called. + + .. note:: + + Note that the use of the scale factor ``R`` differs between KLU and UMFPACK: + + .. math:: + L U &= P R_{\mathrm{umf}} A Q \quad &&\text{(UMFPACK)}, \\ + L U + F &= R_{\mathrm{klu}} P A Q \quad &&\text{(KLU)}. + + They are related by :math:`R_{\mathrm{klu}} = P R_{\mathrm{umf}} P^{\top}`. + + Attributes + ---------- + N : int + The number of rows/columns in the matrix. + L : scipy.sparse.csc_array + The :math:`L` factor as a sparse CSC matrix. + U : scipy.sparse.csc_array + The :math:`U` factor as a sparse CSC matrix. + perm_r, perm_c : numpy.ndarray + The row and column permutation arrays, :math:`P` and :math:`Q`. + + Notes + ----- + This object is an interface to the SuiteSparse KLU library [#klu_url]_. + + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#klu_url] SuiteSparse KLU + https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/KLU + """ + + cdef: + Py_ssize_t _N + readonly object itype + readonly object dtype + bint _use_int32 + bint _is_real + # settings + output info + klu_common _common + klu_common* _cm + klu_l_common _l_common + klu_l_common* _l_cm + KLUInfo _info + # Symbolic analysis + klu_symbolic* _symbolic + klu_l_symbolic* _l_symbolic + # Numeric factorization + klu_numeric* _numeric + klu_l_numeric* _l_numeric + # Cached factor objects + object _L, _U, _F, _P, _Q, _Rs, _R + + def __init__(self, A, KLUControl control=None): + """Compute the KLU factorization of a sparse matrix. + + Parameters + ---------- + A : (N, N) numpy.ndarray or sparse array + The input matrix. Any object that can be converted to + a :class:`~scipy.sparse.csc_array` is accepted. + control : :class:`KLUControl`, optional + An optional :class:`KLUControl` object to set the factorization parameters. + If not provided, default parameters are used. + """ + A, self._use_int32, _ = validate_csc_input(A, require_square=True) + + self._N = A.shape[0] + self._init_common(control) + self._init_symbolic(self._N, A.indptr, A.indices, A.data) + + cdef int _init_common(self, KLUControl control=None) except -1: + """Initialize the KLU common struct with default or user settings.""" + # Initialize common struct with defaults + if self._use_int32: + self._cm = &self._common + assert klu_defaults(self._cm) + else: + self._l_cm = &self._l_common + assert klu_l_defaults(self._l_cm) + + if control is None: + return 0 + + # Set user-defined control parameters + cdef double _FNONE = control._FLOAT_NONE + cdef int _INONE = control._INT_NONE + + if self._use_int32: + _set_if_not_none(&self._cm.tol, control._tol, _FNONE) + _set_if_not_none(&self._cm.memgrow, control._memgrow, _FNONE) + _set_if_not_none(&self._cm.initmem_amd, control._initmem_amd, _FNONE) + _set_if_not_none(&self._cm.initmem, control._initmem, _FNONE) + _set_if_not_none(&self._cm.maxwork, control._maxwork, _FNONE) + _set_if_not_none(&self._cm.btf, control._btf, _INONE) + _set_if_not_none(&self._cm.ordering, control._ordering, _INONE) + _set_if_not_none(&self._cm.scale, control._scale, _INONE) + else: + _set_if_not_none(&self._l_cm.tol, control._tol, _FNONE) + _set_if_not_none(&self._l_cm.memgrow, control._memgrow, _FNONE) + _set_if_not_none(&self._l_cm.initmem_amd, control._initmem_amd, _FNONE) + _set_if_not_none(&self._l_cm.initmem, control._initmem, _FNONE) + _set_if_not_none(&self._l_cm.maxwork, control._maxwork, _FNONE) + _set_if_not_none(&self._l_cm.btf, control._btf, _INONE) + _set_if_not_none(&self._l_cm.ordering, control._ordering, _INONE) + _set_if_not_none(&self._l_cm.scale, control._scale, _INONE) + + return 0 + + @cython.boundscheck(False) + @cython.wraparound(False) + def _init_symbolic( + self, + Py_ssize_t N, + index_t[::1] indptr, + index_t[::1] indices, + value_t[::1] data, + ): + """Compute the symbolic factorization. + + Parameters + ---------- + N : int + Number of rows and columns of the matrix. + indptr : 1D array of index_t + The index pointer array of the CSC matrix. + indices : 1D array of index_t + The row indices array of the CSC matrix. + """ + self._is_real = value_t is double + + if self._use_int32: + self._symbolic = klu_analyze( + N, + &indptr[0], + &indices[0], + self._cm + ) + _handle_errors(self._cm.status) + else: + self._l_symbolic = klu_l_analyze( + N, + &indptr[0], + &indices[0], + self._l_cm + ) + _handle_errors(self._l_cm.status) + + self.itype = np.dtype(np.int32 if self._use_int32 else np.int64) + self.dtype = np.dtype(np.float64 if self._is_real else np.complex128) + + def __dealloc__(self): + """Deallocate KLU objects.""" + if self._use_int32: + if self._symbolic is not NULL: + klu_free_symbolic(&self._symbolic, self._cm) + if self._numeric is not NULL: + if self._is_real: + klu_free_numeric(&self._numeric, self._cm) + else: + klu_z_free_numeric(&self._numeric, self._cm) + else: + if self._l_symbolic is not NULL: + klu_l_free_symbolic(&self._l_symbolic, self._l_cm) + if self._l_numeric is not NULL: + if self._is_real: + klu_l_free_numeric(&self._l_numeric, self._l_cm) + else: + klu_zl_free_numeric(&self._l_numeric, self._l_cm) + + def __iter__(self): + for attr in ['L', 'U', 'perm_r', 'perm_c', 'rscale', 'F', 'rblocks']: + yield getattr(self, attr) + + # --------------------------------------------------------------------------------- + # Properties + # --------------------------------------------------------------------------------- + @property + def is_numeric(self): + if self._use_int32: + return self._symbolic is not NULL and self._numeric is not NULL + else: + return self._l_symbolic is not NULL and self._l_numeric is not NULL + + @property + def lnz(self): + if self._use_int32: + if self._numeric is NULL: + return None + val = self._numeric.lnz + else: + if self._l_numeric is NULL: + return None + val = self._l_numeric.lnz + return int(val) if val >= 0 else None + + @property + def unz(self): + if self._use_int32: + if self._numeric is NULL: + return None + val = self._numeric.unz + else: + if self._l_numeric is NULL: + return None + val = self._l_numeric.unz + return int(val) if val >= 0 else None + + @property + def nzoff(self): + if self._use_int32: + if self._numeric is NULL: + return None + val = self._numeric.nzoff + else: + if self._l_numeric is NULL: + return None + val = self._l_numeric.nzoff + return int(val) if val >= 0 else None + + @property + def nblocks(self): + if self._use_int32: + if self._symbolic is NULL: + return None + val = self._symbolic.nblocks + else: + if self._l_symbolic is NULL: + return None + val = self._l_symbolic.nblocks + return int(val) if val >= 0 else None + + @property + def nnz(self): + if self.lnz is None or self.unz is None: + return None + return int(self.lnz + self.unz) + + @property + def shape(self): + return (self._N, self._N) + + @property + def L(self): + if self._L is None: + self._get_numeric() + return self._L + + @property + def U(self): + if self._U is None: + self._get_numeric() + return self._U + + @property + def F(self): + if self._F is None: + self._get_numeric() + return self._F + + @property + def perm_r(self): + if self._P is None: + self._get_numeric() + return self._P + + @property + def perm_c(self): + if self._Q is None: + self._get_numeric() + return self._Q + + @property + def rscale(self): + if self._Rs is None: + self._get_numeric() + return self._Rs + + @property + def rblocks(self): + if self._R is None: + self._get_numeric() + return self._R + + @property + def info(self): + """Get information about the factorization and solve process.""" + if self._info is None: + self._info = KLUInfo() + + # Compute flops to store in info (needs "is_real" info) + if self._use_int32: + if self._is_real: + klu_flops(self._symbolic, self._numeric, self._cm) + else: + klu_z_flops(self._symbolic, self._numeric, self._cm) + else: + if self._is_real: + klu_l_flops(self._l_symbolic, self._l_numeric, self._l_cm) + else: + klu_zl_flops(self._l_symbolic, self._l_numeric, self._l_cm) + + if self._use_int32: + self._info.update_from_klu(self._symbolic, self._numeric, self._cm) + else: + self._info.update_from_l_klu(self._l_symbolic, self._l_numeric, self._l_cm) + + return self._info + + # --------------------------------------------------------------------------------- + # Public API + # --------------------------------------------------------------------------------- + def copy(self): + """Return a deep copy of the current KLUFactor object.""" + cdef KLUFactor klu = KLUFactor.__new__(KLUFactor) + + klu._N = self._N + klu.itype = self.itype + klu.dtype = self.dtype + klu._use_int32 = self._use_int32 + klu._is_real = self._is_real + klu._info = deepcopy(self._info) + + # settings + output info + if self._use_int32: + assert self._cm is not NULL + assert self._symbolic is not NULL + + klu._cm = &klu._common + memcpy(klu._cm, self._cm, sizeof(klu_common)) + + klu._symbolic = klu_malloc(1, sizeof(klu_symbolic), klu._cm) + _handle_errors(klu._cm.status) + _copy_symbolic(klu._symbolic, self._symbolic, klu._cm) + + if self._numeric is not NULL: + klu._numeric = klu_malloc(1, sizeof(klu_numeric), klu._cm) + _handle_errors(klu._cm.status) + _copy_numeric(klu._numeric, self._numeric, klu._cm, self._is_real) + else: + assert self._l_cm is not NULL + assert self._l_symbolic is not NULL + + klu._l_cm = &klu._l_common + memcpy(klu._l_cm, self._l_cm, sizeof(klu_l_common)) + + klu._l_symbolic = klu_l_malloc( + 1, sizeof(klu_l_symbolic), klu._l_cm + ) + _handle_errors(klu._l_cm.status) + _copy_symbolic(klu._l_symbolic, self._l_symbolic, klu._l_cm) + + if self._l_numeric is not NULL: + klu._l_numeric = klu_l_malloc( + 1, sizeof(klu_l_numeric), klu._l_cm + ) + _handle_errors(klu._l_cm.status) + _copy_numeric(klu._l_numeric, self._l_numeric, klu._l_cm, self._is_real) + + # Cached factor objects + klu._L = self._L.copy() if self._L is not None else None + klu._U = self._U.copy() if self._U is not None else None + klu._F = self._F.copy() if self._F is not None else None + klu._P = self._P.copy() if self._P is not None else None + klu._Q = self._Q.copy() if self._Q is not None else None + klu._Rs = self._Rs.copy() if self._Rs is not None else None + klu._R = self._R.copy() if self._R is not None else None + + return klu + + def factorize(self, object A): + r"""Compute the numeric factorization of the matrix. + + Computes the numeric factorization of a sparse matrix :math:`A` + and determines a fill-reducing ordering such that: + + .. math:: + L U + F = R P A Q. + + If given, the matrix :math:`A` must have the same shape and nonzero pattern as + the one used to create this :class:`KLUFactor` object, but need not have the + same values. + + .. warning:: + + No check is made on the non-zero structure of the input matrix, so if it is + different from the one used for the symbolic analysis, the results will be + incorrect without raising an error. + + Parameters + ---------- + A : (N, N) numpy.ndarray or sparse array + The input matrix. Must have the same shape and nonzero pattern as + the matrix used to create this :class:`KLUFactor` object. If not + provided, the original matrix given to the constructor will be + used. + + Returns + ------- + :class:`KLUFactor` + The current object, for method chaining. + """ + _msg = "Symbolic analysis not present. Cannot perform numeric factorization." + if self._use_int32: + assert self._symbolic is not NULL, _msg + else: + assert self._l_symbolic is not NULL, _msg + + A, _, itype = validate_csc_input(A, require_square=True) + self._check_input_matrix(A, itype) + + # Clear cached factor objects + self._L = None + self._U = None + self._F = None + self._P = None + self._Q = None + self._Rs = None + self._R = None + + if self._numeric is not NULL or self._l_numeric is not NULL: + # Refactorize with existing numeric struct + self._refactorize(A.indptr, A.indices, A.data) + else: + # Allocate and compute new numeric struct + self._factorize(A.indptr, A.indices, A.data) + + # Compute reciprocal pivot growth for KLUInfo O(|A| + |U|) + self._rgrowth(A.indptr, A.indices, A.data) + + # Compute rough condition number estimate min(abs(diag(U)) / max(abs(diag(U))) + self._rcond() + + return self + + @cython.boundscheck(False) + @cython.wraparound(False) + def _factorize( + self, + index_t[::1] indptr, + index_t[::1] indices, + value_t[::1] data, + ): + """Compute the numeric factorization given the CSC arrays. + + Parameters + ---------- + indptr : contiguous 1D array of index_t + The index pointer array of the CSC matrix. + indices : contiguous 1D array of index_t + The row indices array of the CSC matrix. + data : contiguous 1D array of value_t + The data array of the CSC matrix. + """ + # Compute the numeric factorization + if self._use_int32: + if self._is_real: + self._numeric = c_klu_factor( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + self._cm + ) + else: + self._numeric = klu_z_factor( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + self._cm + ) + _handle_errors(self._cm.status) + else: + if self._is_real: + self._l_numeric = klu_l_factor( + &indptr[0], + &indices[0], + &data[0], + self._l_symbolic, + self._l_cm + ) + else: + self._l_numeric = klu_zl_factor( + &indptr[0], + &indices[0], + &data[0], + self._l_symbolic, + self._l_cm + ) + _handle_errors(self._l_cm.status) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _refactorize( + self, + index_t[::1] indptr, + index_t[::1] indices, + value_t[::1] data, + ): + """Re-compute the numeric factorization given the CSC arrays. + + Parameters + ---------- + indptr : contiguous 1D array of index_t + The index pointer array of the CSC matrix. + indices : contiguous 1D array of index_t + The row indices array of the CSC matrix. + data : contiguous 1D array of value_t + The data array of the CSC matrix. + """ + # Compute the numeric factorization + if self._use_int32: + if self._is_real: + klu_refactor( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + self._numeric, + self._cm + ) + else: + klu_z_refactor( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + self._numeric, + self._cm + ) + _handle_errors(self._cm.status) + else: + if self._is_real: + klu_l_refactor( + &indptr[0], + &indices[0], + &data[0], + self._l_symbolic, + self._l_numeric, + self._l_cm + ) + else: + klu_zl_refactor( + &indptr[0], + &indices[0], + &data[0], + self._l_symbolic, + self._l_numeric, + self._l_cm + ) + _handle_errors(self._l_cm.status) + + def solve(self, object b, *, bint transpose=False): + r"""Solve a linear system using the KLU factorization. + + This method solves a linear system for :math:`x` given the right-hand side + :math:`b` as either a vector or a matrix with multiple right-hand sides. + + If ``transpose=False``, solve + + .. math:: + A x = b + + or, if ``transpose=True``, solve + + .. math:: + x A = b \Longleftrightarrow A^{\top} x^{\top} = b^{\top}. + + The method uses the LU factorization of :math:`A` previously computed by + :meth:`.factorize`. + + Parameters + ---------- + b : (N,) or (N, K) numpy.ndarray + The right-hand side vector or matrix. + + Returns + ------- + x : (N,) or (N, K) numpy.ndarray or sparse array + The solution vector or matrix. If ``b`` is a 1D array, then ``x`` is + returned as a 1D array. If ``b`` is a 2D array with ``K`` columns, + then ``x`` is returned as a 2D array with ``K`` columns. If ``b`` + is a sparse array, then ``x`` is also returned as a sparse array. + """ + if not (isinstance(b, np.ndarray) or issparse(b)): + raise ValueError("b must be an ndarray or sparse matrix.") + + if b.dtype != self.dtype: + raise ValueError( + f"LHS and RHS dtypes do not match. {self.dtype=} and {b.dtype=}" + ) + + if b.ndim not in (1, 2): + raise ValueError("b must be a 1D or 2D array.") + + cdef bint return_1D = b.ndim == 1 + cdef size_t N = b.shape[0] if (b.ndim == 1 or not transpose) else b.shape[1] + cdef size_t K = 1 if b.ndim == 1 else (b.shape[0] if transpose else b.shape[1]) + + if N != self._N: + raise ValueError( + "Right-hand side b must have compatible shape with A. " + f"Got {b.shape=}, but A.shape={self.shape} ({transpose=})." + ) + + # Check the condition number + self._check_rcond() + + cdef bint return_sparse = issparse(b) + + if return_sparse: + b = b.toarray() + else: + b = np.asarray(b) + + # Ensure columns are contiguous for multiple RHS + b = np.asfortranarray(b) + + # The klu_solve function overwrites the input with the output + x = b.copy() + + if transpose: + x = x.T.conj() + + # klu_solve expects B as a column-oriented 1D array + x = x.reshape(-1, order='F') + + if transpose: + self._tsolve(K, x) + else: + self._solve(K, x) + + # Reshape X into a 2D array + x = x.reshape(self._N, K, order='F') + + if return_sparse: + x = csc_array(x, dtype=b.dtype) + x.indptr = x.indptr.astype(self.itype) + x.indices = x.indices.astype(self.itype) + + if return_1D: + x = x[:, 0] + + if transpose: + x = x.T.conj() + + return x + + @cython.boundscheck(False) + @cython.wraparound(False) + def _solve(self, size_t K, value_t[::1] x): + """Solve Ax = b. + + Parameters + ---------- + K : int + The number of right-hand sides to solve. + x : (N * K,) array_like + The right-hand side matrix on input, in column-oriented form, solution on + output. + """ + cdef double *x_ptr = &x[0] + + if self._use_int32: + if self._is_real: + c_klu_solve(self._symbolic, self._numeric, self._N, K, x_ptr, self._cm) + else: + klu_z_solve(self._symbolic, self._numeric, self._N, K, x_ptr, self._cm) + _handle_errors(self._cm.status) + else: + if self._is_real: + klu_l_solve( + self._l_symbolic, self._l_numeric, self._N, K, x_ptr, self._l_cm + ) + else: + klu_zl_solve( + self._l_symbolic, self._l_numeric, self._N, K, x_ptr, self._l_cm + ) + _handle_errors(self._l_cm.status) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _tsolve(self, size_t K, value_t[::1] x): + """Solve xA = b. + + Parameters + ---------- + K : int + The number of right-hand sides to solve. + x : (N * K,) array_like + The right-hand side matrix on input, in column-oriented form, solution on + output. + """ + cdef double *x_ptr = &x[0] + cdef int conj_solve = True + + if self._use_int32: + if self._is_real: + klu_tsolve( + self._symbolic, + self._numeric, + self._N, + K, + x_ptr, + self._cm + ) + else: + klu_z_tsolve( + self._symbolic, + self._numeric, + self._N, + K, + x_ptr, + conj_solve, + self._cm + ) + _handle_errors(self._cm.status) + else: + if self._is_real: + klu_l_tsolve( + self._l_symbolic, + self._l_numeric, + self._N, + K, + x_ptr, + self._l_cm + ) + else: + klu_zl_tsolve( + self._l_symbolic, + self._l_numeric, + self._N, + K, + x_ptr, + conj_solve, + self._l_cm + ) + _handle_errors(self._l_cm.status) + + # --------------------------------------------------------------------------------- + # Private API + # --------------------------------------------------------------------------------- + def _check_input_matrix(self, object A, object itype): + """Check that the input matrix matches the existing factorization.""" + if A.shape != self.shape: + raise ValueError( + "The shape of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected {self.shape}, got {A.shape}." + ) + + if itype != self.itype: + raise ValueError( + "The integer size of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected '{self.itype}', got '{itype}'." + ) + + if A.dtype != self.dtype: + raise ValueError( + "The data type of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected '{self.dtype}', got '{A.dtype}'." + ) + + cdef int _rcond(self) except -1: + """Compute the condition number estimate.""" + if self._use_int32: + if self._is_real: + klu_rcond(self._symbolic, self._numeric, self._cm) + else: + klu_z_rcond(self._symbolic, self._numeric, self._cm) + _handle_errors(self._cm.status) + else: + if self._is_real: + klu_l_rcond(self._l_symbolic, self._l_numeric, self._l_cm) + else: + klu_zl_rcond(self._l_symbolic, self._l_numeric, self._l_cm) + _handle_errors(self._l_cm.status) + + cdef int _check_rcond(self) except -1: + """Check a rough estimate of the condition number. + + Computes ``min(abs(U.diagonal())) / max(abs(U.diagonal()))``. See + ``klu_condest`` for more accurate estimate from the full LU decomposition. + """ + cdef double rcond = self._cm.rcond if self._use_int32 else self._l_cm.rcond + cdef double eps = np.finfo(np.float64).eps + + cdef int singular_col = ( + self._cm.singular_col if self._use_int32 else self._l_cm.singular_col + ) + + if rcond == 0: + raise KLUError( + "Matrix is indefinite or singular to working precision. " + f"Failed on column {singular_col}." + ) + elif rcond < eps: + warnings.warn( + "Matrix is nearly singular." + f" Results may be inaccurate (rcond={rcond:.2e}).", + KLUSingularMatrixWarning + ) + + def _rgrowth( + self, + index_t[::1] indptr, + index_t[::1] indices, + value_t[::1] data + ): + """Compute the growth factor of the LU factorization.""" + if self._use_int32: + if self._is_real: + klu_rgrowth( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + self._numeric, + self._cm + ) + else: + klu_z_rgrowth( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + self._numeric, + self._cm + ) + _handle_errors(self._cm.status) + else: + if self._is_real: + klu_l_rgrowth( + &indptr[0], + &indices[0], + &data[0], + self._l_symbolic, + self._l_numeric, + self._l_cm + ) + else: + klu_zl_rgrowth( + &indptr[0], + &indices[0], + &data[0], + self._l_symbolic, + self._l_numeric, + self._l_cm + ) + _handle_errors(self._l_cm.status) + + cdef void _get_numeric(self) except *: + """Extract and cache the numeric factors from the klu_numeric struct.""" + if (self._use_int32 and self._numeric is NULL) or ( + not self._use_int32 and self._l_numeric is NULL + ): + raise KLUError( + "Numeric factorization not present. Run `KLUFactor.factorize(A)` first." + ) + + # Create output arrays + Lp = np.empty(self._N + 1, dtype=self.itype) + Li = np.empty(self.lnz, dtype=self.itype) + Lx = np.empty(self.lnz, dtype=np.float64) + + Up = np.empty(self._N + 1, dtype=self.itype) + Ui = np.empty(self.unz, dtype=self.itype) + Ux = np.empty(self.unz, dtype=np.float64) + + Fp = np.empty(self._N + 1, dtype=self.itype) + Fi = np.empty(self.nzoff, dtype=self.itype) + Fx = np.empty(self.nzoff, dtype=np.float64) + + self._P = np.empty(self._N, dtype=self.itype) + self._Q = np.empty(self._N, dtype=self.itype) + self._Rs = np.empty(self._N, dtype=np.float64) # always real + self._R = np.empty(self.nblocks + 1, dtype=self.itype) + + # Sort the row indices so the output has canonical format + if self._use_int32: + if self._is_real: + klu_sort(self._symbolic, self._numeric, self._cm) + else: + klu_z_sort(self._symbolic, self._numeric, self._cm) + _handle_errors(self._cm.status) + else: + if self._is_real: + klu_l_sort(self._l_symbolic, self._l_numeric, self._l_cm) + else: + klu_zl_sort(self._l_symbolic, self._l_numeric, self._l_cm) + _handle_errors(self._l_cm.status) + + # Extract the numeric factorization + if self._is_real: + self._extract( + Lp, Li, Lx, + Up, Ui, Ux, + Fp, Fi, Fx, + self._P, + self._Q, + self._Rs, + self._R + ) + + self._L = csc_array((Lx, Li, Lp), shape=self.shape) + self._U = csc_array((Ux, Ui, Up), shape=self.shape) + self._F = csc_array((Fx, Fi, Fp), shape=self.shape) + else: + # Allocate imaginary parts + Lz = np.empty(self.lnz, dtype=np.float64) + Uz = np.empty(self.unz, dtype=np.float64) + Fz = np.empty(self.nzoff, dtype=np.float64) + + self._z_extract( + Lp, Li, Lx, Lz, + Up, Ui, Ux, Uz, + Fp, Fi, Fx, Fz, + self._P, + self._Q, + self._Rs, + self._R + ) + + self._L = csc_array((Lx + 1j * Lz, Li, Lp), shape=self.shape) + self._U = csc_array((Ux + 1j * Uz, Ui, Up), shape=self.shape) + self._F = csc_array((Fx + 1j * Fz, Fi, Fp), shape=self.shape) + + # Return R as reciprocal so user doesn't have to invert it + np.reciprocal(self._Rs, out=self._Rs) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _extract( + self, + index_t[::1] Lp, index_t[::1] Li, value_t[::1] Lx, + index_t[::1] Up, index_t[::1] Ui, value_t[::1] Ux, + index_t[::1] Fp, index_t[::1] Fi, value_t[::1] Fx, + index_t[::1] P, + index_t[::1] Q, + double[::1] Rs, + index_t[::1] R, + ): + """Call the appropriate KLU extract function. + + Parameters + ---------- + Lp, Li, Lx : arrays for the L factor + The output arrays for the L factor in CSC format. + Up, Ui, Ux : arrays for the U factor + The output arrays for the U factor in CSC format. + Fp, Fi, Fx : arrays for the F factor + The output arrays for the F factor in CSC format. + P : array of index_t + The output row permutation array. + Q : array of index_t + The output column permutation array. + Rs : array of double + The output row scaling factors. + R : array of index_t + The output block boundaries. + """ + # Extract the numeric factorization + if self._use_int32: + klu_extract( + self._numeric, + self._symbolic, + &Lp[0], &Li[0], &Lx[0], + &Up[0], &Ui[0], &Ux[0], + &Fp[0], &Fi[0], &Fx[0], + &P[0], + &Q[0], + &Rs[0], + &R[0], + self._cm + ) + _handle_errors(self._cm.status) + else: + klu_l_extract( + self._l_numeric, + self._l_symbolic, + &Lp[0], &Li[0], &Lx[0], + &Up[0], &Ui[0], &Ux[0], + &Fp[0], &Fi[0], &Fx[0], + &P[0], + &Q[0], + &Rs[0], + &R[0], + self._l_cm + ) + _handle_errors(self._l_cm.status) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _z_extract( + self, + index_t[::1] Lp, index_t[::1] Li, value_t[::1] Lx, value_t[::1] Lz, + index_t[::1] Up, index_t[::1] Ui, value_t[::1] Ux, value_t[::1] Uz, + index_t[::1] Fp, index_t[::1] Fi, value_t[::1] Fx, value_t[::1] Fz, + index_t[::1] P, + index_t[::1] Q, + double[::1] Rs, + index_t[::1] R, + ): + """Call the appropriate KLU extract function. + + Parameters + ---------- + Lp, Li, Lx, Lz : arrays for the L factor + The output arrays for the L factor in CSC format. + Up, Ui, Ux, Uz : arrays for the U factor + The output arrays for the U factor in CSC format. + Fp, Fi, Fx, Fz : arrays for the F factor + The output arrays for the F factor in CSC format. + P : array of index_t + The output row permutation array. + Q : array of index_t + The output column permutation array. + Rs : array of double + The output row scaling factors. + R : array of index_t + The output block boundaries. + """ + # Extract the numeric factorization + if self._use_int32: + klu_z_extract( + self._numeric, + self._symbolic, + &Lp[0], &Li[0], &Lx[0], &Lz[0], + &Up[0], &Ui[0], &Ux[0], &Uz[0], + &Fp[0], &Fi[0], &Fx[0], &Fz[0], + &P[0], + &Q[0], + &Rs[0], + &R[0], + self._cm + ) + _handle_errors(self._cm.status) + else: + klu_zl_extract( + self._l_numeric, + self._l_symbolic, + &Lp[0], &Li[0], &Lx[0], &Lz[0], + &Up[0], &Ui[0], &Ux[0], &Uz[0], + &Fp[0], &Fi[0], &Fx[0], &Fz[0], + &P[0], + &Q[0], + &Rs[0], + &R[0], + self._l_cm + ) + _handle_errors(self._l_cm.status) + + +# ----------------------------------------------------------------------------- +# Convenience Functions +# ----------------------------------------------------------------------------- +def klu_factor(A, *, KLUControl control=None, **kwargs): + """Compute the LU factorization of a sparse matrix using KLU. + + This is a convenience function that creates a :class:`KLUFactor` object, + computes the numeric factorization, and returns the resulting object. + + Parameters + ---------- + A : (M, N) numpy.ndarray or sparse array + The input matrix to factorize. + control : :class:`KLUControl`, optional + An optional :class:`KLUControl` object to set the factorization parameters. + If not provided, default parameters are used. + **kwargs + Additional keyword arguments passed to the :class:`KLUControl` constructor. + + Returns + ------- + :class:`KLUFactor` + The LU factorization of the input matrix. + + Raises + ------ + :exc:`KLUSingularMatrixWarning` + If the matrix is exactly singular. + + See Also + -------- + KLUFactor, klu_solve + + + .. versionadded:: 0.5.0 + """ + if control is None: + control = KLUControl(**kwargs) + return KLUFactor(A, control).factorize(A) + + +def klu_solve(A, b, *, KLUControl control=None, bint transpose=False, **kwargs): + r"""Solve a linear system using KLU. + + This function solves a linear system for :math:`x` given the right-hand side + :math:`b` as either a vector or a matrix with multiple right-hand sides. + + If ``transpose=False``, solve + + .. math:: + A x = b + + or, if ``transpose=True``, solve + + .. math:: + x A = b \Longleftrightarrow A^{\top} x^{\top} = b^{\top}. + + This is a convenience function that creates a :class:`KLUFactor` object, computes + the numeric factorization, and solves the linear system with + :meth:`KLUFactor.solve`. + + Parameters + ---------- + A : (N, N) numpy.ndarray or sparse array + The input matrix to factorize. + b : (N,) or (N, K) numpy.ndarray + The right-hand side vector or matrix. + control : :class:`KLUControl`, optional + An optional :class:`KLUControl` object to set the factorization parameters. + If not provided, default parameters are used. + transpose : bool, optional + If True, solve :math:`x A = b`, otherwise, solve :math:`A x = b`. + **kwargs + Additional keyword arguments passed to the :class:`KLUControl` constructor. + + Returns + ------- + x : (N,) or (N, K) numpy.ndarray or sparse array + The solution vector or matrix of the same type and shape as the input + right-hand side ``b``. + + See Also + -------- + KLUFactor, klu_solve + + + .. versionadded:: 0.5.0 + """ + if control is None: + control = KLUControl(**kwargs) + + # factorize() and solve() will each warn for a singular matrix, + # so we catch the warnings from factorize() and re-raise only once. + with warnings.catch_warnings(record=True) as ws: + x = KLUFactor(A, control).factorize(A).solve(b, transpose=transpose) + + # Raise only the latest singular matrix warning from solve + if ws: + w = ws[-1] + warnings.warn(w.message, w.category) + + return x diff --git a/src/sksparse/spqr.pxd b/src/sksparse/spqr.pxd new file mode 100644 index 00000000..8ed0afe9 --- /dev/null +++ b/src/sksparse/spqr.pxd @@ -0,0 +1,252 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: spqr.pxd +# Created: 2025-11-06 19:04 +# ============================================================================= +# distutils: language = c++ + +from libc.stdint cimport int32_t, int64_t, uintptr_t +from libc.stdlib cimport malloc +from libc.string cimport memcpy + +from sksparse.cholmod cimport cholmod_common, cholmod_dense, cholmod_sparse + + +cdef extern from "SuiteSparseQR_definitions.h": + # Get the #define'd constants + enum: + # Ordering methods + SPQR_ORDERING_FIXED + SPQR_ORDERING_NATURAL + SPQR_ORDERING_COLAMD + SPQR_ORDERING_GIVEN + SPQR_ORDERING_CHOLMOD + SPQR_ORDERING_AMD + SPQR_ORDERING_METIS + SPQR_ORDERING_DEFAULT + SPQR_ORDERING_BEST + SPQR_ORDERING_BESTAMD + # tolerance options + SPQR_DEFAULT_TOL + SPQR_NO_TOL + # qmult methods + SPQR_QTX + SPQR_QX + SPQR_XQT + SPQR_XQ + # solution systems + SPQR_RX_EQUALS_B + SPQR_RETX_EQUALS_B + SPQR_RTX_EQUALS_B + SPQR_RTX_EQUALS_ETB + + +cdef extern from "SuiteSparseQR.hpp": + cdef cppclass spqr_gpu_impl[Int]: + pass + + cdef cppclass spqr_symbolic[Int]: + Int m + Int n + Int anz + + Int *Sp + Int *Sj + Int *Qfill + Int *PLinv + Int *Sleft + + Int nf + Int maxfn + + Int *Parent + Int *Child + Int *Childp + + Int *Super + Int *Rp + Int *Rj + Int *Post + Int rjsize + + Int do_rank_detection + Int maxstack + Int hisize + Int keepH + Int *Hip + Int ntasks + Int ns + + # These are for task parallelism (not yet supported) + Int *TaskChildp + Int *TaskChild + Int *TaskStack + Int *TaskFront + Int *TaskFrontp + Int *On_stack + Int *Stack_maxstack + Int *Fm + Int *Cm + + size_t maxcsize + size_t maxesize + Int *ColCount + + spqr_gpu_impl[Int] *QRgpu + + cdef cppclass spqr_numeric[Entry, Int]: + Entry **Rblock + Entry **Stacks + Int *Stack_size + Int hisize + Int n + Int m + Int nf + Int ntasks + Int ns + Int maxstack + # rank detection + char *Rdead + Int rank + Int rank1 + Int maxfrank + double norm_E_fro + # keeping Householder vectors + Int keepH + Int rjsize + Int *HStair + Entry *HTau + Int *Hii + Int *HPinv + Int *Hm + Int *Hr + Int maxfm + + cdef cppclass SuiteSparseQR_factorization[Entry, Int]: + double tol + spqr_symbolic[Int] *QRsym + spqr_numeric[Entry, Int] *QRnum + Int *R1p + Int *R1j + Entry *R1x + Int r1nz + Int *Q1fill + Int *P1inv + Int *HP1inv + Int *Rmap + Int *RmapInv + Int n1rows + Int n1cols + Int narows + Int nacols + Int bncols + Int rank + int allow_tol + + # "Simple" Functions + # [Q,R,E] = qr(A), returning Q as a sparse matrix + Int SuiteSparseQR_full "SuiteSparseQR"[Entry, Int]( + int ordering, + double tol, + Int econ, + cholmod_sparse *A, + cholmod_sparse **Q, + cholmod_sparse **R, + Int **E, + cholmod_common *cc + ) + + # [Q,R,E] = qr(A), discarding Q + Int SuiteSparseQR_noQ "SuiteSparseQR"[Entry, Int]( + int ordering, + double tol, + Int econ, + cholmod_sparse *A, + cholmod_sparse **R, + Int **E, + cholmod_common *cc + ) + + # [Q,R,E] = qr(A) where Q is returned in Householder form + Int SuiteSparseQR_householder "SuiteSparseQR"[Entry, Int]( + int ordering, + double tol, + Int econ, + cholmod_sparse *A, + cholmod_sparse **R, + Int **E, + cholmod_sparse **H, + Int **HPinv, + cholmod_dense **HTau, + cholmod_common *cc + ) + + cholmod_dense *SuiteSparseQR_qmult_Hd "SuiteSparseQR_qmult"[Entry, Int]( + int method, + cholmod_sparse *H, + cholmod_dense *HTau, + Int *HPinv, + cholmod_dense *Xdense, + cholmod_common *cc + ) + + cholmod_sparse *SuiteSparseQR_qmult_Hs "SuiteSparseQR_qmult"[Entry, Int]( + int method, + cholmod_sparse *H, + cholmod_dense *HTau, + Int *HPinv, + cholmod_sparse *X, + cholmod_common *cc + ) + + # "Expert" Functions + SuiteSparseQR_factorization[Entry, Int] *SuiteSparseQR_factorize[Entry, Int]( + int ordering, + double tol, + cholmod_sparse *A, + cholmod_common *cc + ) + + SuiteSparseQR_factorization[Entry, Int] *SuiteSparseQR_symbolic[Entry, Int]( + int ordering, + int allow_tol, + cholmod_sparse *A, + cholmod_common *cc + ) + + int SuiteSparseQR_numeric[Entry, Int]( + double tol, + cholmod_sparse *A, + SuiteSparseQR_factorization[Entry, Int] *QR, + cholmod_common *cc + ) + + cholmod_sparse *SuiteSparseQR_qmult_fs "SuiteSparseQR_qmult"[Entry, Int]( + int method, + SuiteSparseQR_factorization[Entry, Int] *QR, + cholmod_sparse *Xsparse, + cholmod_common *cc + ) + + cholmod_dense *SuiteSparseQR_qmult_fd "SuiteSparseQR_qmult"[Entry, Int]( + int method, + SuiteSparseQR_factorization[Entry, Int] *QR, + cholmod_dense *Xdense, + cholmod_common *cc + ) + + cholmod_dense *SuiteSparseQR_solve[Entry, Int]( + int system, + SuiteSparseQR_factorization[Entry, Int] *QR, + cholmod_dense *B, + cholmod_common *cc + ) + + int SuiteSparseQR_free[Entry, Int]( + SuiteSparseQR_factorization[Entry, Int] **QR, + cholmod_common *cc + ) diff --git a/src/sksparse/spqr.pyx b/src/sksparse/spqr.pyx new file mode 100644 index 00000000..5798d323 --- /dev/null +++ b/src/sksparse/spqr.pyx @@ -0,0 +1,2169 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: spqr.pyx +# Created: 2025-11-06 20:19 +# ============================================================================= + +""" +============================================== +Sparse QR Decomposition (:mod:`sksparse.spqr`) +============================================== + +.. currentmodule:: sksparse.spqr + +.. versionadded:: 0.5.0 + + +An interface to the SuiteSparse `SPQR +`_ +package, which computes the QR factorization and solves systems of equations +for sparse, possibly non-square, non-symmetric, indefinite matrices. + + +Function Interface +------------------ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + spqr - Compute the SPQR factorization of a sparse matrix. + spqr_qmult - Multiply by Q from the SPQR factorization. + spqr_solve - Solve a linear system using the SPQR factorization. + SPQRHouseholder - A class representing the Householder vectors. + + +Object Interface +---------------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + spqr_factor - Compute the QR factorization of a sparse matrix. + SPQRFactor - An object-oriented interface to SPQR. + SPQRInfo - A dataclass to return SPQR info. + + +.. spqr-exceptions: + +Warnings and Exceptions +----------------------- + +.. autosummary:: + :toctree: generated/ + + SPQRWarning + SPQRRankDeficiencyWarning + + SPQRError + SPQRNotInstalledError + SPQROutOfMemoryError + SPQROverflowError + SPQRInvalidInputError + SPQRGpuProblemError + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse SPQR `_ +""" + +cimport cython +from cython cimport doublecomplex as cdouble + +from sksparse.cholmod cimport ( + CHOLMOD_OK, + CHOLMOD_GPU_PROBLEM, + CHOLMOD_INVALID, + CHOLMOD_NOT_INSTALLED, + CHOLMOD_OUT_OF_MEMORY, + CHOLMOD_TOO_LARGE, + CHOLMOD_INT, + CHOLMOD_REAL, + cholmod_start, + cholmod_l_start, + cholmod_finish, + cholmod_l_finish, + _ndarray_copy_from_intptr, + _cholmod_dense_from_ndarray, + _ndarray_from_cholmod_dense, + _copy_cholmod_common, + _ndarray_copy_from_intptr, + _csc_from_cholmod_sparse, + cholmod_free, + cholmod_l_free, +) + +import numpy as np +from scipy.sparse import csc_array, issparse +from typing import NamedTuple +import warnings + +from sksparse.cholmod import _cholmod_sparse_from_csc + +from .utils import validate_csc_input + + +__all = [ + "SPQRError", + "SPQRNotInstalledError", + "SPQROutOfMemoryError", + "SPQROverflowError", + "SPQRInvalidInputError", + "SPQRGpuProblemError", + "SPQRFactor", + "spqr_factor", + "spqr_solve", +] + + +ctypedef fused index_t: + int32_t + int64_t + + +ctypedef fused value_t: + double + double complex + + +# Define specific instantiations +ctypedef spqr_symbolic[int32_t] spqr_symbolic_i +ctypedef spqr_symbolic[int64_t] spqr_symbolic_l + +ctypedef fused symbolic_t: + spqr_symbolic_i + spqr_symbolic_l + + +ctypedef spqr_numeric[double, int32_t] spqr_numeric_di +ctypedef spqr_numeric[double, int64_t] spqr_numeric_dl +ctypedef spqr_numeric[cdouble, int32_t] spqr_numeric_zi +ctypedef spqr_numeric[cdouble, int64_t] spqr_numeric_zl + +ctypedef fused numeric_t: + spqr_numeric_di + spqr_numeric_dl + spqr_numeric_zi + spqr_numeric_zl + + +ctypedef SuiteSparseQR_factorization[double, int32_t] spqr_factor_di +ctypedef SuiteSparseQR_factorization[double, int64_t] spqr_factor_dl +ctypedef SuiteSparseQR_factorization[cdouble, int32_t] spqr_factor_zi +ctypedef SuiteSparseQR_factorization[cdouble, int64_t] spqr_factor_zl + +ctypedef fused factor_t: + spqr_factor_di + spqr_factor_dl + spqr_factor_zi + spqr_factor_zl + + +# NOTE These are not defined in the header files, so we define them here +cdef: + int SPQR_ISTAT_NNZR_UPPER = 0 + int SPQR_ISTAT_NNZH_UPPER = 1 + int SPQR_ISTAT_NFRONTAL = 2 + int SPQR_ISTAT_NTASKS = 3 + int SPQR_ISTAT_EST_RANKA = 4 + int SPQR_ISTAT_COL_SINGLETONS = 5 + int SPQR_ISTAT_ROW_SINGLETONS = 6 + int SPQR_ISTAT_ORDERING = 7 + + +# ------------------------------------------------------------------------------------- +# Error Handling +# ------------------------------------------------------------------------------------- +class SPQRWarning(Warning): + """Base class for SPQR warnings.""" + pass + + +class SPQRRankDeficiencyWarning(SPQRWarning): + """Raised when SPQR detects a rank-deficient matrix.""" + pass + + +class SPQRError(Exception): + """Base class for SPQR exceptions.""" + pass + + +class SPQRNotInstalledError(SPQRError): + """Raised when the SPQR library is not installed.""" + pass + + +class SPQROutOfMemoryError(MemoryError, SPQRError): + """Raised when SPQR runs out of memory.""" + pass + + +class SPQROverflowError(SPQRError): + """Raised when SPQR encounters an integer overflow.""" + pass + + +class SPQRInvalidInputError(SPQRError): + """Raised when SPQR receives invalid input.""" + pass + + +class SPQRGpuProblemError(SPQRError): + """Raised when SPQR encounters a problem with CUDA.""" + pass + + +# Known Errors -- SPQR uses CHOLMOD error codes +cdef dict _ERROR_INDEX = { + CHOLMOD_NOT_INSTALLED: ( + SPQRNotInstalledError, "SPQR library is not installed or not found." + ), + CHOLMOD_OUT_OF_MEMORY: (SPQROutOfMemoryError, "SPQR ran out of memory."), + CHOLMOD_TOO_LARGE: (SPQROverflowError, "SPQR encountered an integer overflow."), + CHOLMOD_INVALID: (SPQRInvalidInputError, "SPQR received invalid input."), + CHOLMOD_GPU_PROBLEM: (SPQRGpuProblemError, "SPQR encountered a problem with CUDA."), +} + + +cdef int _handle_errors(int status) except -1 with gil: + """Handle SPQR errors by raising Python exceptions or warnings. + + This function should be called with the return ``status`` after any SPQR + C function that may fail. + + Parameters + ---------- + status : int + The SPQR exit status code. + + Returns + ------- + None + + Raises + ------ + :exc:`SPQRWarning` or subclass + Raises a warning for non-critical issues. + :exc:`SPQRError` or subclass + Raises an appropriate Python exception based on the SPQR status code. + """ + if status == CHOLMOD_OK: + return 0 + + # Fallback to generic error for unknown codes + exc_class, msg = _ERROR_INDEX.get( + status, + (SPQRError, "An unknown SPQR error occurred.") + ) + full_msg = f"{msg} (code {status:d})" + + if issubclass(exc_class, Warning): + warnings.warn(full_msg, exc_class, stacklevel=2) + else: + raise exc_class(full_msg) + + +# ------------------------------------------------------------------------------------- +# Info and Control +# ------------------------------------------------------------------------------------- +cdef dict _ordering_methods = { + "default": SPQR_ORDERING_DEFAULT, + "fixed": SPQR_ORDERING_FIXED, + "natural": SPQR_ORDERING_NATURAL, + "colamd": SPQR_ORDERING_COLAMD, + "cholmod": SPQR_ORDERING_CHOLMOD, + "amd": SPQR_ORDERING_AMD, + "metis": SPQR_ORDERING_METIS, + "best": SPQR_ORDERING_BEST, + "bestamd": SPQR_ORDERING_BESTAMD, +} + + +cdef dict _ordering_methods_inv = {v: k for k, v in _ordering_methods.items()} + + +cdef inline int _qmult_int_from_str(str method) except -1: + """Return the SPQR qmult method constant from string.""" + if method == 'QX': + return SPQR_QX + elif method == 'QTX': + return SPQR_QTX + elif method == 'XQ': + return SPQR_XQ + elif method == 'XQT': + return SPQR_XQT + else: + raise ValueError( + f"Invalid method '{method}'. " + "Expected one of ['QX', 'QTX', 'XQ', 'XQT']." + ) + + +@cython.dataclasses.dataclass(frozen=True) +cdef class SPQRInfo: + """A dataclass to hold SPQR info statistics. + + Attributes + ---------- + nnzR_upper_bound : int + Bound on the number of nonzeros in ``R``. + nnzH_upper_bound : int + Bound on the number of nonzeros in ``H``. + nf : int + Number of frontal matrices. + rank_A_estimate : int + Estimated rank of ``A``. + n1cols : int + Number of singleton columns. + n1rows : int + Number of singleton rows. + ordering : str + Ordering method used. + memory : int + Memory usage in bytes. + flops_upper_bound : int + Upper bound on flop count (excluding backsolve). + tol : float + Column norm tolerance used. + norm_E_fro : float + Norm of dropped diagonal of R. + analyze_time : float + Time taken for the symbolic analysis in seconds. + factorize_time : int + Time take for the numeric factorization (including applying ``Q.T``) + solve_time : int + Time taken for the backsolve only :math:`R x = Q^T b` in seconds. + total_time : int + Total time in seconds. + flops : int + Actual flops for the factorization and solve (including backsolve). + """ + nnzR_upper_bound : int | None = None + nnzH_upper_bound : int | None = None + nf : int | None = None + rank_A_estimate : int | None = None + n1cols : int | None = None + n1rows : int | None = None + ordering : str | None = None + memory : int | None = None + flops_upper_bound : int | None = None + tol : float | None = None + norm_E_fro : float | None = None + analyze_time : float | None = None + factorize_time : float | None = None + solve_time : float | None = None + total_time : float | None = None + flops : int | None = None + + # __init__ can't take a C pointer, so we use a separate method + cdef int _init_from_common(self, cholmod_common* cm) except -1: + """Initialize SPQRInfo from a cholmod_common object.""" + assert cm is not NULL + self.nnzR_upper_bound = cm.SPQR_istat[SPQR_ISTAT_NNZR_UPPER] + self.nnzH_upper_bound = cm.SPQR_istat[SPQR_ISTAT_NNZH_UPPER] + self.nf = cm.SPQR_istat[SPQR_ISTAT_NFRONTAL] + self.rank_A_estimate = cm.SPQR_istat[SPQR_ISTAT_EST_RANKA] + self.n1cols = cm.SPQR_istat[SPQR_ISTAT_COL_SINGLETONS] + self.n1rows = cm.SPQR_istat[SPQR_ISTAT_ROW_SINGLETONS] + cdef int order = cm.SPQR_istat[SPQR_ISTAT_ORDERING] + self.ordering = _ordering_methods_inv.get(order, f"unknown {order}") + self.memory = cm.memory_usage + self.flops_upper_bound = cm.SPQR_flopcount_bound + self.tol = cm.SPQR_tol_used + self.norm_E_fro = cm.SPQR_norm_E_fro + self.analyze_time = cm.SPQR_analyze_time + self.factorize_time = cm.SPQR_factorize_time + self.solve_time = cm.SPQR_solve_time + self.total_time = self.analyze_time + self.factorize_time + self.solve_time + self.flops = cm.SPQR_flopcount + + +# ------------------------------------------------------------------------------------- +# Copy Functions +# ------------------------------------------------------------------------------------- +cdef inline void* _malloc_copy( + const void* src, + size_t n, + size_t size +): + """Allocate memory and copy data from src to the new memory.""" + if src is NULL: + return NULL + + cdef void* dest = malloc(n * size) + + if dest is NULL: + return NULL + + if n > 0: + memcpy(dest, src, n * size) + + return dest + + +cdef int _copy_spqr_symbolic_base( + symbolic_t* dest, + const symbolic_t* src, + index_t _dummy_idx=0, +) except -1: + """Deep copy a SuiteSparseQR_symbolic object.""" + assert dest is not NULL + assert src is not NULL + + # Prune invalid type combinations + if not ( + (symbolic_t is spqr_symbolic_i and index_t is int32_t) + or (symbolic_t is spqr_symbolic_l and index_t is int64_t) + ): + assert False + return 0 + + dest.m = src.m + dest.n = src.n + dest.anz = src.anz + + dest.Sp = _malloc_copy(src.Sp, src.m + 1, sizeof(index_t)) + dest.Sj = _malloc_copy(src.Sj, src.anz, sizeof(index_t)) + + dest.Qfill = _malloc_copy(src.Qfill, src.n, sizeof(index_t)) + dest.PLinv = _malloc_copy(src.PLinv, src.m, sizeof(index_t)) + dest.Sleft = _malloc_copy(src.Sleft, src.n + 2, sizeof(index_t)) + + dest.nf = src.nf + dest.maxfn = src.maxfn + + dest.Parent = _malloc_copy(src.Parent, src.nf + 1, sizeof(index_t)) + dest.Child = _malloc_copy(src.Child, src.nf + 1, sizeof(index_t)) + dest.Childp = _malloc_copy(src.Childp, src.nf + 2, sizeof(index_t)) + + dest.Super = _malloc_copy(src.Super, src.nf + 1, sizeof(index_t)) + + dest.Rp = _malloc_copy(src.Rp, src.nf + 1, sizeof(index_t)) + dest.Rj = _malloc_copy(src.Rj, src.rjsize, sizeof(index_t)) + dest.Post = _malloc_copy(src.Post, src.nf + 1, sizeof(index_t)) + + dest.rjsize = src.rjsize + dest.do_rank_detection = src.do_rank_detection + dest.maxstack = src.maxstack + dest.hisize = src.hisize + dest.keepH = src.keepH + + dest.Hip = _malloc_copy(src.Hip, src.nf + 1, sizeof(index_t)) + + dest.ntasks = src.ntasks + dest.ns = src.ns + + if dest.ntasks > 1: + raise NotImplementedError("SPQR task parallelism and GPU not yet supported.") + + dest.TaskChildp = NULL + dest.TaskChild = NULL + + dest.TaskStack = NULL + + dest.TaskFront = NULL + dest.TaskFrontp = NULL + + dest.On_stack = NULL + + dest.Stack_maxstack = NULL + dest.Fm = NULL + dest.Cm = NULL + + # Values used in GPU factorization + dest.maxcsize = src.maxcsize + dest.maxesize = src.maxesize + dest.ColCount = NULL + + # Not yet supported + dest.QRgpu = NULL + + return 0 + + +cdef inline int _copy_spqr_symbolic( + symbolic_t* dest, + const symbolic_t* src +) except -1: + """Deep copy a spqr_symbolic struct.""" + if symbolic_t is spqr_symbolic_i: + return _copy_spqr_symbolic_base[spqr_symbolic_i, int32_t](dest, src) + else: # symbolic_t is spqr_symbolic_l + return _copy_spqr_symbolic_base[spqr_symbolic_l, int64_t](dest, src) + + +cdef int _copy_spqr_numeric_base( + numeric_t* dest, + const numeric_t* src, + value_t _dummy_val=0, + index_t _dummy_idx=0, +) except -1: + """Deep copy a SuiteSparseQR_numeric object.""" + assert dest is not NULL + assert src is not NULL + + # Prune invalid type combinations + if not ( + (numeric_t is spqr_numeric_di and index_t is int32_t and value_t is double) + or (numeric_t is spqr_numeric_dl and index_t is int64_t and value_t is double) + or (numeric_t is spqr_numeric_zi and index_t is int32_t and value_t is cdouble) + or (numeric_t is spqr_numeric_zl and index_t is int64_t and value_t is cdouble) + ): + assert False + return 0 + + dest.Stacks = malloc(src.ns * sizeof(value_t*)) + dest.Stack_size = _malloc_copy( + src.Stack_size, src.ns, sizeof(index_t) + ) + + # Deep copy each stack + cdef size_t k + + if ( + dest.Stacks is not NULL + and src.Stacks is not NULL + and src.Stack_size is not NULL + ): + for k in range(src.ns): + dest.Stacks[k] = _malloc_copy( + src.Stacks[k], src.Stack_size[k], sizeof(value_t) + ) + + # Point each Rblock to the copied stacks + # See: SPQR/Source/spqr_kernel.cpp:186 for Rblock assignment logic + dest.Rblock = malloc(src.nf * sizeof(value_t*)) + + cdef: + size_t s # index for stacks + size_t stack_size # size of stack in bytes + size_t offset # byte offset within stack + int f # stack index that Rblock points to + value_t *stack_start # pointer to start of stack + value_t *rblock_ptr # pointer to Rblock[k] + + if dest.Rblock is not NULL and src.Rblock is not NULL: + for k in range(src.nf): + rblock_ptr = src.Rblock[k] + if rblock_ptr is NULL: + continue + + # Find which stack this Rblock points to + f = -1 + for s in range(src.ns): + # Check if the pointer is in this stack + stack_start = src.Stacks[s] + if stack_start is NULL: + continue + + stack_size = src.Stack_size[s] * sizeof(value_t) + + # Check if the Rblock poitner falls within the memory range of stack s + if (rblock_ptr >= stack_start) and ( + rblock_ptr < stack_start + stack_size + ): + f = s + break + + # Compute the offset within the stack + if f == -1: + raise SPQRError("Failed to copy Rblock pointers.") + else: + # Compute the byte offset to Rblock[k] within the stack + offset = rblock_ptr - src.Stacks[f] + # Apply the same offset to the copied stack + dest.Rblock[k] = (dest.Stacks[f] + offset) + + dest.hisize = src.hisize + dest.m = src.m + dest.n = src.n + dest.nf = src.nf + dest.ntasks = src.ntasks + dest.ns = src.ns + dest.maxstack = src.maxstack + + dest.Rdead = _malloc_copy(src.Rdead, src.n, sizeof(char)) + + dest.rank = src.rank + dest.rank1 = src.rank1 + dest.maxfrank = src.maxfrank + dest.norm_E_fro = src.norm_E_fro + + dest.keepH = src.keepH + dest.rjsize = src.rjsize + + dest.HStair = _malloc_copy(src.HStair, src.rjsize, sizeof(index_t)) + dest.HTau = _malloc_copy(src.HTau, src.rjsize, sizeof(value_t)) + + dest.Hii = _malloc_copy(src.Hii, src.hisize, sizeof(index_t)) + dest.HPinv = _malloc_copy(src.HPinv, src.m, sizeof(index_t)) + + dest.Hm = _malloc_copy(src.Hm, src.nf, sizeof(index_t)) + dest.Hr = _malloc_copy(src.Hr, src.nf, sizeof(index_t)) + + dest.maxfm = src.maxfm + + return 0 + + +cdef inline int _copy_spqr_numeric( + numeric_t* dest, + const numeric_t* src, +) except -1: + """Deep copy a spqr_numeric struct.""" + if numeric_t is spqr_numeric_di: + return _copy_spqr_numeric_base[spqr_numeric_di, double, int32_t](dest, src) + elif numeric_t is spqr_numeric_dl: + return _copy_spqr_numeric_base[spqr_numeric_dl, double, int64_t](dest, src) + elif numeric_t is spqr_numeric_zi: + return _copy_spqr_numeric_base[spqr_numeric_zi, cdouble, int32_t](dest, src) + else: # numeric_t is spqr_numeric_zl + return _copy_spqr_numeric_base[spqr_numeric_zl, cdouble, int64_t](dest, src) + + +cdef int _copy_spqr_factor_base( + factor_t* dest, + const factor_t* src, + value_t _dummy_val=0, + index_t _dummy_idx=0, +) except -1: + """Deep copy a SuiteSparseQR_factorization object.""" + assert dest is not NULL + assert src is not NULL + + # Validate types to ensure correct instantiation. Invalid combos will be pruned. + if not ( + (factor_t is spqr_factor_di and index_t is int32_t and value_t is double) + or (factor_t is spqr_factor_dl and index_t is int64_t and value_t is double) + or (factor_t is spqr_factor_zi and index_t is int32_t and value_t is cdouble) + or (factor_t is spqr_factor_zl and index_t is int64_t and value_t is cdouble) + ): + assert False + return 0 + + dest.tol = src.tol + + # Deep copy symbolic factorization (if present) + dest.QRsym = NULL + + if src.QRsym is not NULL: + if index_t is int32_t: + dest.QRsym = malloc(sizeof(spqr_symbolic_i)) + else: + dest.QRsym = malloc(sizeof(spqr_symbolic_l)) + + _copy_spqr_symbolic(dest.QRsym, src.QRsym) + + # Deep copy numeric factorization (if present) + dest.QRnum = NULL + + if src.QRnum is not NULL: + if factor_t is spqr_factor_di: + dest.QRnum = malloc(sizeof(spqr_numeric_di)) + elif factor_t is spqr_factor_dl: + dest.QRnum = malloc(sizeof(spqr_numeric_dl)) + elif factor_t is spqr_factor_zi: + dest.QRnum = malloc(sizeof(spqr_numeric_zi)) + else: # factor_t is spqr_factor_zl + dest.QRnum = malloc(sizeof(spqr_numeric_zl)) + + _copy_spqr_numeric(dest.QRnum, src.QRnum) + + dest.R1p = _malloc_copy(src.R1p, src.n1rows + 1, sizeof(index_t)) + dest.R1j = _malloc_copy(src.R1j, src.r1nz, sizeof(index_t)) + dest.R1x = _malloc_copy(src.R1x, src.r1nz, sizeof(value_t)) + dest.r1nz = src.r1nz + + cdef size_t m = src.narows + cdef size_t n = src.nacols + + dest.Q1fill = _malloc_copy(src.Q1fill, n + src.bncols, sizeof(index_t)) + dest.P1inv = _malloc_copy(src.P1inv, m, sizeof(index_t)) + dest.HP1inv = _malloc_copy(src.HP1inv, m, sizeof(index_t)) + + dest.Rmap = _malloc_copy(src.Rmap, n, sizeof(index_t)) + dest.RmapInv = _malloc_copy(src.RmapInv, n, sizeof(index_t)) + + dest.n1rows = src.n1rows + dest.n1cols = src.n1cols + dest.narows = src.narows + dest.nacols = src.nacols + dest.bncols = src.bncols + dest.rank = src.rank + dest.allow_tol = src.allow_tol + + return 0 + + +cdef inline int _copy_spqr_factor( + factor_t* dest, + const factor_t* src, +) except -1: + """Deep copy a SuiteSparseQR_factorization struct.""" + if factor_t is spqr_factor_di: + return _copy_spqr_factor_base[spqr_factor_di, double, int32_t](dest, src) + elif factor_t is spqr_factor_dl: + return _copy_spqr_factor_base[spqr_factor_dl, double, int64_t](dest, src) + elif factor_t is spqr_factor_zi: + return _copy_spqr_factor_base[spqr_factor_zi, cdouble, int32_t](dest, src) + else: # factor_t is spqr_factor_zl + return _copy_spqr_factor_base[spqr_factor_zl, cdouble, int64_t](dest, src) + + +# ------------------------------------------------------------------------------------- +# SPQR Factor Class +# ------------------------------------------------------------------------------------- +cdef class SPQRFactor: + r"""The main object used for creating and manipulating SPQR factorizations. + + The constructor computes the sybolic analysis of the matrix and determines + a fill-reducing ordering such that: + + .. math:: + + Q R = A E + + where :math:`E` is a column permutation matrix, :math:`Q` is an orthogonal + matrix, and :math:`R` is an upper-triangular matrix. + + The numerical factorization is computed in one of two ways: + + 1. by setting ``use_singletons=True`` in the constructor, which computes + both the symbolic and numeric factorizations at once, or + 2. by calling :meth:`SPQRFactor.factorize`, which computes the numeric + factorization after symbolic analysis has been performed. + + The first method is useful when factoring a single matrix, but solving multiple + right-hand sides. + + The second method is useful when factoring multiple matrices with the same sparsity + pattern but different numerical values. + + Parameters + ---------- + A : (M, N) array_like or sparse array + An array convertible to a sparse matrix. + use_singletons : bool, optional + If True, directly compute the numeric factorization to exploit singleton rows. + Otherwise, only perform symbolic analysis. Default is False. + order : str, optional + The column ordering strategy to use. Let :math:`S` be the matrix :math:`A` with + singleton rows/columns removed, the ordering options are: + + * ``default``: COLAMD(S), + * ``fixed``: identity permutation (*i.e.* no singletons removed), + * ``natural``: singletons removed, but no fill-reducing ordering applied, + * ``colamd``: COLAMD(S), + * ``amd``: AMD(:math:`S^{\top} S`), + * ``metis``: METIS(:math:`S^{\top} S`), + * ``best``: try all of ``amd``, ``colamd``, ``metis`` and pick the best, + * ``cholmod``: Same as ``best``, + * ``bestamd``: try ``amd`` and ``colamd`` and pick the best. + + tol : float, optional + If the 2-norm of a column in ``A`` is less than ``tol``, that column is + considered to be a zero column. If ``tol = 0``, no columns are treated as zero. + If ``None``, the default tolerance is used. The default is + ``tol =`` :math:`20 \epsilon (M + N) \sqrt{\max{\mathrm{diag}(A^{\top} A)}}`, + where :math:`\epsilon` is the machine precision. + + + Attributes + ---------- + is_numeric : bool + Whether the numeric factorization has been computed. + shape : tuple + The shape of the input matrix (M, N). + itype : ~numpy.dtype + The integer type used for indices (``int32`` or ``int64``). + dtype : ~numpy.dtype + The data type of the matrix (``float64`` or ``complex128``). + rank : int + The rank of the matrix as determined by SPQR. + perm : ~numpy.ndarray of int + The combined singleton and fill-reducing column permutation vector. + + See Also + -------- + spqr_factor, spqr, spqr_qmult, spqr_solve + + Notes + ----- + This object is an interface to the SuiteSparse SPQR library [#spqr_url]_. + + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#spqr_url] SuiteSparse SPQR + https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/SPQR + """ + + cdef: + cholmod_common _common + cholmod_common *_cm + spqr_factor_di *_factor_di + spqr_factor_dl *_factor_dl + spqr_factor_zi *_factor_zi + spqr_factor_zl *_factor_zl + bint _use_int32 + bint _is_real + int _M + int _N + readonly object itype + readonly object dtype + double _tol + + def __init__( + self, + object A, + *, + bint use_singletons=False, + object order=None, + object tol=None, + ): + """Initialize the SPQRFactor object and perform symbolic analysis.""" + A, _, _ = validate_csc_input(A) + + # Promote single to double precision + if not ( + np.issubdtype(A.dtype, np.float64) or np.issubdtype(A.dtype, np.complex128) + ): + if np.issubdtype(A.dtype, np.floating): + A = A.astype(np.promote_types(A.dtype, np.float64)) + elif np.issubdtype(A.dtype, np.complexfloating): + A = A.astype(np.promote_types(A.dtype, np.complex128)) + + # Validate inputs + cdef int ordering + + if order is None: + ordering = SPQR_ORDERING_DEFAULT + else: + try: + ordering = _ordering_methods[order] + except KeyError: + raise ValueError( + "Unknown ordering method: {ordering}. " + f"Must be one of {set(_ordering_methods.keys())}." + ) + + self._tol = tol if tol is not None else SPQR_DEFAULT_TOL + + # Get the input matrix into CHOLMOD format + cdef cholmod_sparse Amatrix + cdef cholmod_sparse *Ac = &Amatrix + cdef int stype = 0 # assume matrix is not symmetric + + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + + self._use_int32 = (Ac.itype == CHOLMOD_INT) + self._is_real = (Ac.xtype == CHOLMOD_REAL) + + # Initialize the common object + self._cm = &self._common + + if self._use_int32: + cholmod_start(self._cm) + else: + cholmod_l_start(self._cm) + + cdef bint allow_tol = True # if False, do not perform rank detection + + if use_singletons: + # Perform both symbolic and numeric factorization + if self._is_real: + if self._use_int32: + self._factor_di = SuiteSparseQR_factorize[double, int32_t]( + ordering, self._tol, Ac, self._cm + ) + else: + self._factor_dl = SuiteSparseQR_factorize[double, int64_t]( + ordering, self._tol, Ac, self._cm + ) + else: + if self._use_int32: + self._factor_zi = SuiteSparseQR_factorize[cdouble, int32_t]( + ordering, self._tol, Ac, self._cm + ) + else: + self._factor_zl = SuiteSparseQR_factorize[cdouble, int64_t]( + ordering, self._tol, Ac, self._cm + ) + else: + # Perform symbolic analysis only + if self._is_real: + if self._use_int32: + self._factor_di = SuiteSparseQR_symbolic[double, int32_t]( + ordering, allow_tol, Ac, self._cm + ) + else: + self._factor_dl = SuiteSparseQR_symbolic[double, int64_t]( + ordering, allow_tol, Ac, self._cm + ) + else: + if self._use_int32: + self._factor_zi = SuiteSparseQR_symbolic[cdouble, int32_t]( + ordering, allow_tol, Ac, self._cm + ) + else: + self._factor_zl = SuiteSparseQR_symbolic[cdouble, int64_t]( + ordering, allow_tol, Ac, self._cm + ) + + _handle_errors(self._cm.status) + + self.itype = np.dtype(np.int32 if self._use_int32 else np.int64) + self.dtype = np.dtype(np.float64 if self._is_real else np.complex128) + self._M = Ac.nrow + self._N = Ac.ncol + + def __dealloc__(self): + """Free the SPQR factorization and common objects.""" + if self._cm is NULL: + return + + if self._is_real: + if self._use_int32: + assert SuiteSparseQR_free[double, int32_t](&self._factor_di, self._cm) + else: + assert SuiteSparseQR_free[double, int64_t](&self._factor_dl, self._cm) + else: + if self._use_int32: + assert SuiteSparseQR_free[cdouble, int32_t](&self._factor_zi, self._cm) + else: + assert SuiteSparseQR_free[cdouble, int64_t](&self._factor_zl, self._cm) + + if self._use_int32: + cholmod_finish(self._cm) + else: + cholmod_l_finish(self._cm) + + + def __repr__(self): + cls_name = self.__class__.__name__ + factor_type = 'numeric' if self.is_numeric else 'symbolic' + return ( + f"<{cls_name} {factor_type} factor of dtype '{self.dtype}' " + f"with '{self.itype}' indices\n" + f" A: shape={self.shape}, rank={self.rank}>" + ) + + def __str__(self): + return self.__repr__() + + # --------------------------------------------------------------------------------- + # Properties + # --------------------------------------------------------------------------------- + @property + def is_numeric(self): + try: + self._require_numeric() + return True + except AssertionError: + return False + + @property + def shape(self): + return (self._M, self._N) + + @property + def rank(self): + if not self.is_numeric: + return None + + if self._is_real: + if self._use_int32: + return self._factor_di.rank + else: + return self._factor_dl.rank + else: + if self._use_int32: + return self._factor_zi.rank + else: + return self._factor_zl.rank + + @property + def perm(self): + self._require_symbolic() + + cdef void* ptr + if self._is_real: + if self._use_int32: + ptr = self._factor_di.Q1fill + else: + ptr = self._factor_dl.Q1fill + else: + if self._use_int32: + ptr = self._factor_zi.Q1fill + else: + ptr = self._factor_zl.Q1fill + + return _ndarray_copy_from_intptr(ptr, self._N, self._use_int32) + + @property + def info(self): + cdef SPQRInfo info = SPQRInfo() + info._init_from_common(self._cm) + return info + + # --------------------------------------------------------------------------------- + # Public API + # --------------------------------------------------------------------------------- + def copy(self): + """Return a deep copy of the SPQRFactor object.""" + cdef SPQRFactor dest = SPQRFactor.__new__(SPQRFactor) + + dest._cm = &dest._common + + if self._use_int32: + cholmod_start(dest._cm) + else: + cholmod_l_start(dest._cm) + + _copy_cholmod_common(dest._cm, self._cm) + + dest._use_int32 = self._use_int32 + dest._is_real = self._is_real + dest._M = self._M + dest._N = self._N + dest.itype = self.itype + dest.dtype = self.dtype + dest._tol = self._tol + + # Deep copy the factorization + if self._is_real: + if self._use_int32: + dest._factor_di = malloc(sizeof(spqr_factor_di)) + _copy_spqr_factor(dest._factor_di, self._factor_di) + else: + dest._factor_dl = malloc(sizeof(spqr_factor_dl)) + _copy_spqr_factor(dest._factor_dl, self._factor_dl) + else: + if self._use_int32: + dest._factor_zi = malloc(sizeof(spqr_factor_zi)) + _copy_spqr_factor(dest._factor_zi, self._factor_zi) + else: + dest._factor_zl = malloc(sizeof(spqr_factor_zl)) + _copy_spqr_factor(dest._factor_zl, self._factor_zl) + + return dest + + def factorize(self, object A, *, object tol=None): + r"""Compute the numeric factorization of the matrix. + + Parameters + ---------- + A : (M, N) array_like or sparse array, optional + An array convertible to a sparse matrix. If None, the numeric factorization + is computed for the matrix used in the constructor. If ``A`` is provided, + it must have the same sparsity pattern as the matrix used in the + constructor. + tol : float, optional + If the 2-norm of a column in ``A`` is less than ``tol``, that column is + considered to be a zero column. If ``tol = 0``, no columns are treated as + zero. If ``None``, the default tolerance is used. The default is + ``tol =`` + :math:`20 \epsilon (M + N) \sqrt{\max{\mathrm{diag}(A^{\top} A)}}`, + where :math:`\epsilon` is the machine precision. + + Returns + ------- + :class:`SPQRFactor` + The current object with the numeric factorization computed. + """ + A, _, itype = validate_csc_input(A) + self._check_input_matrix(A, itype) + + cdef cholmod_sparse Amatrix + cdef cholmod_sparse *Ac = &Amatrix + cdef int stype = 0 # assume matrix is not symmetric + + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + + if tol is not None: + if self.is_numeric and tol != self._tol: + warnings.warn( + "The tolerance has been changed from the one used " + "during a previous numeric factorization. This may lead to " + "inconsistent rank determination.", + UserWarning, + ) + self._tol = tol + + # Perform numeric factorization + if self._is_real: + if self._use_int32: + SuiteSparseQR_numeric[double, int32_t]( + self._tol, Ac, self._factor_di, self._cm + ) + else: + SuiteSparseQR_numeric[double, int64_t]( + self._tol, Ac, self._factor_dl, self._cm + ) + else: + if self._use_int32: + SuiteSparseQR_numeric[cdouble, int32_t]( + self._tol, Ac, self._factor_zi, self._cm + ) + else: + SuiteSparseQR_numeric[cdouble, int64_t]( + self._tol, Ac, self._factor_zl, self._cm + ) + + _handle_errors(self._cm.status) + + return self + + def qmult(self, object X, method="QX"): + self._require_numeric() + + if not (isinstance(X, np.ndarray) or issparse(X)): + raise ValueError("X must be an ndarray or sparse matrix.") + + if X.dtype != self.dtype: + raise ValueError( + f"Input and factor dtypes do not match. {self.dtype=} and {X.dtype=}" + ) + + if X.ndim not in (1, 2): + raise ValueError("X must be a 1D or 2D array.") + + cdef int c_method = _qmult_int_from_str(method) + + # Check shape compatibility with Q + cdef Py_ssize_t X_dim = ( + X.shape[0] + if method == "QTX" or method == "QX" + else X.shape[1] + ) + + if self._M != X_dim: + raise ValueError( + "Input X must have compatible shape with Q. " + f"Expected {self._M}, got {X_dim}." + ) + + cdef bint return_1D = X.ndim == 1 + cdef bint return_sparse = issparse(X) + + if return_sparse: + Y = self._qmult_sparse(c_method, X) + else: + # cholmod_dense expects column-oriented + X = np.asfortranarray(X) + Y = self._qmult_dense(c_method, X) + + if return_1D: + Y = Y[:, 0] + + return Y + + cdef object _qmult_sparse(self, int method, object X): + """Multiply a sparse matrix by Q.""" + cdef cholmod_sparse Xsparse + cdef cholmod_sparse *Xs = &Xsparse + cdef int stype = 0 # assume unsymmetric + _cholmod_sparse_from_csc( + X.shape, X.indptr, X.indices, X.data, stype, Xs + ) + + cdef cholmod_sparse *Ys + + if self._is_real: + if self._use_int32: + Ys = SuiteSparseQR_qmult_fs[double, int32_t]( + method, self._factor_di, Xs, self._cm + ) + else: + Ys = SuiteSparseQR_qmult_fs[double, int64_t]( + method, self._factor_dl, Xs, self._cm + ) + else: + if self._use_int32: + Ys = SuiteSparseQR_qmult_fs[cdouble, int32_t]( + method, self._factor_zi, Xs, self._cm + ) + else: + Ys = SuiteSparseQR_qmult_fs[cdouble, int64_t]( + method, self._factor_zl, Xs, self._cm + ) + + _handle_errors(self._cm.status) + + return _csc_from_cholmod_sparse(Ys, self._cm) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _qmult_dense(self, int method, value_t[::1, :] X): + """Multiply a dense matrix by Q.""" + cdef cholmod_dense Xdense + cdef cholmod_dense *Xd = &Xdense + _cholmod_dense_from_ndarray(X, Xd) + + cdef cholmod_dense *Yd + + if self._is_real: + if self._use_int32: + Yd = SuiteSparseQR_qmult_fd[double, int32_t]( + method, self._factor_di, Xd, self._cm + ) + else: + Yd = SuiteSparseQR_qmult_fd[double, int64_t]( + method, self._factor_dl, Xd, self._cm + ) + else: + if self._use_int32: + Yd = SuiteSparseQR_qmult_fd[cdouble, int32_t]( + method, self._factor_zi, Xd, self._cm + ) + else: + Yd = SuiteSparseQR_qmult_fd[cdouble, int64_t]( + method, self._factor_zl, Xd, self._cm + ) + + _handle_errors(self._cm.status) + + return _ndarray_from_cholmod_dense(Yd, self._use_int32, self._cm) + + def solve(self, object b, *, bint transpose=False): + self._require_numeric() + + if not (isinstance(b, np.ndarray) or issparse(b)): + raise ValueError("b must be an ndarray or sparse matrix.") + + if b.dtype != self.dtype: + raise ValueError( + f"LHS and RHS dtypes do not match. {self.dtype=} and {b.dtype=}" + ) + + if b.ndim not in (1, 2): + raise ValueError("b must be a 1D or 2D array.") + + if ( + (not transpose and b.shape[0] != self._M) + or (transpose and b.shape[0] != self._N) + ): + raise ValueError( + "Right-hand side b must have compatible shape with A. " + f"Got {b.shape=}, but A.shape={self.shape} ({transpose=})." + ) + + # Check the rank of A and warn if rank deficient + if self.rank < min(self._M, self._N): + warnings.warn( + f"Matrix is rank deficient: rank={self.rank}, A.shape={self.shape}. " + "The solution may not be unique.", + SPQRRankDeficiencyWarning, + ) + + cdef bint return_1D = b.ndim == 1 + cdef bint return_sparse = issparse(b) + + # CHOLMOD routines require a 2D array + if b.ndim == 1: + if not transpose: + b = b.reshape((self._M, 1)) + else: + b = b.reshape((self._N, 1)) + + # The SuiteSparseQR_solve "sparse" routine just converts b to + # cholmod_dense internally. + if issparse(b): + b = b.toarray() + + # Ensure columns are contiguous for multiple RHS + b = np.asfortranarray(b) + + x = self._solve(b, transpose) + + if return_sparse: + x = csc_array(x, dtype=b.dtype) + x.indptr = x.indptr.astype(self.itype) + x.indices = x.indices.astype(self.itype) + + if return_1D: + x = x[:, 0] + + return x + + @cython.boundscheck(False) + @cython.wraparound(False) + def _solve(self, value_t[::1, :] b, bint transpose): + """Solve a linear system with a dense right-hand side.""" + # Get the b vector or matrix into CHOLMOD format + cdef cholmod_dense Bmatrix + cdef cholmod_dense *Bd = &Bmatrix + + _cholmod_dense_from_ndarray(b, Bd) + + # System is Ax = b -> (QRE.T)x = b + # But "solve" does not touch Q, so -> (RE.T)x = (Q.T)b + if not transpose: + # pre-multiply by Q.T + if self._is_real: + if self._use_int32: + Bd = SuiteSparseQR_qmult_fd[double, int32_t]( + SPQR_QTX, self._factor_di, Bd, self._cm + ) + else: + Bd = SuiteSparseQR_qmult_fd[double, int64_t]( + SPQR_QTX, self._factor_dl, Bd, self._cm + ) + else: + if self._use_int32: + Bd = SuiteSparseQR_qmult_fd[cdouble, int32_t]( + SPQR_QTX, self._factor_zi, Bd, self._cm + ) + else: + Bd = SuiteSparseQR_qmult_fd[cdouble, int64_t]( + SPQR_QTX, self._factor_zl, Bd, self._cm + ) + + _handle_errors(self._cm.status) + + # Solve the system + cdef int system = SPQR_RETX_EQUALS_B if not transpose else SPQR_RTX_EQUALS_ETB + cdef cholmod_dense *Xd + + if self._is_real: + if self._use_int32: + Xd = SuiteSparseQR_solve[double, int32_t]( + system, self._factor_di, Bd, self._cm + ) + else: + Xd = SuiteSparseQR_solve[double, int64_t]( + system, self._factor_dl, Bd, self._cm + ) + else: + if self._use_int32: + Xd = SuiteSparseQR_solve[cdouble, int32_t]( + system, self._factor_zi, Bd, self._cm + ) + else: + Xd = SuiteSparseQR_solve[cdouble, int64_t]( + system, self._factor_zl, Bd, self._cm + ) + + _handle_errors(self._cm.status) + + # System is A.T x = b -> (QRE.T).Tx = b -> (E R.T Q.T) x = b + # But "solve" does not touch Q, so -> (E R.T) (Q.T x) = b + if transpose: + # post-multiply by Q.T + if self._is_real: + if self._use_int32: + Xd = SuiteSparseQR_qmult_fd[double, int32_t]( + SPQR_QX, self._factor_di, Xd, self._cm + ) + else: + Xd = SuiteSparseQR_qmult_fd[double, int64_t]( + SPQR_QX, self._factor_dl, Xd, self._cm + ) + else: + if self._use_int32: + Xd = SuiteSparseQR_qmult_fd[cdouble, int32_t]( + SPQR_QX, self._factor_zi, Xd, self._cm + ) + else: + Xd = SuiteSparseQR_qmult_fd[cdouble, int64_t]( + SPQR_QX, self._factor_zl, Xd, self._cm + ) + + _handle_errors(self._cm.status) + + return _ndarray_from_cholmod_dense(Xd, self._use_int32, self._cm) + + # --------------------------------------------------------------------------------- + # Private API + # --------------------------------------------------------------------------------- + cdef inline int _require_symbolic(self) except -1: + """Raise an error if the symbolic factorization has not been computed yet.""" + if self._is_real: + if self._use_int32: + assert self._factor_di is not NULL and self._factor_di.QRsym is not NULL + else: + assert self._factor_dl is not NULL and self._factor_dl.QRsym is not NULL + else: + if self._use_int32: + assert self._factor_zi is not NULL and self._factor_zi.QRsym is not NULL + else: + assert self._factor_zl is not NULL and self._factor_zl.QRsym is not NULL + + cdef inline int _require_numeric(self) except -1: + """Raise an error if the numeric factorization has not been computed yet.""" + self._require_symbolic() + if self._is_real: + if self._use_int32: + assert self._factor_di.QRnum is not NULL + else: + assert self._factor_dl.QRnum is not NULL + else: + if self._use_int32: + assert self._factor_zi.QRnum is not NULL + else: + assert self._factor_zl.QRnum is not NULL + + def _check_input_matrix(self, object A, object itype): + """Check that the input matrix matches the existing factorization.""" + if A.shape != self.shape: + raise ValueError( + "The shape of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected {self.shape}, got {A.shape}." + ) + + if itype != self.itype: + raise ValueError( + "The integer size of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected '{self.itype}', got '{itype}'." + ) + + if A.dtype != self.dtype: + raise ValueError( + "The data type of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected '{self.dtype}', got '{A.dtype}'." + ) + + +# ------------------------------------------------------------------------------------- +# Convenience Functions +# ------------------------------------------------------------------------------------- +def spqr_factor(A, *, use_singletons=False, order=None, tol=None): + r"""Compute the SPQR factorization of a sparse matrix. + + Compute the numeric factorization of the matrix and determine a fill-reducing + ordering such that: + + .. math:: + + Q R = A E + + where :math:`E` is a column permutation matrix, :math:`Q` is an orthogonal + matrix, and :math:`R` is an upper-triangular matrix. + + This function returns a :class:`SPQRFactor` object that contains the SPQR + factorization of the input matrix. It is not currently possible to extract the + individual factors :math:`Q` and :math:`R` explicitly, but the object provides + methods to reuse the factorization to solve linear systems or multiply by + :math:`Q`. + + Parameters + ---------- + A : (M, N) array_like or sparse array + An array convertible to a sparse matrix. + use_singletons : bool, optional + If True, directly compute the numeric factorization to exploit singleton rows. + Otherwise, only perform symbolic analysis. Default is False, so that the factor + can be reused efficiently for multiple numeric factorizations. + order : str, optional + The column ordering strategy to use. Let :math:`S` be the matrix :math:`A` with + singleton rows/columns removed, the ordering options are: + + * ``default``: COLAMD(S), + * ``fixed``: identity permutation (*i.e.* no singletons removed), + * ``natural``: singletons removed, but no fill-reducing ordering applied, + * ``colamd``: COLAMD(S), + * ``amd``: AMD(:math:`S^{\top} S`), + * ``metis``: METIS(:math:`S^{\top} S`), + * ``best``: try all of ``amd``, ``colamd``, ``metis`` and pick the best, + * ``cholmod``: Same as ``best``, + * ``bestamd``: try ``amd`` and ``colamd`` and pick the best. + + tol : float, optional + If the 2-norm of a column in ``A`` is less than ``tol``, that column is + considered to be a zero column. If ``tol = 0``, no columns are treated as zero. + If ``None``, the default tolerance is used. The default is + ``tol =`` :math:`20 \epsilon (M + N) \sqrt{\max{\mathrm{diag}(A^{\top} A)}}`, + where :math:`\epsilon` is the machine precision. + + Returns + ------- + :class:`SPQRFactor` + The SPQR factorization of the input matrix. + + See Also + -------- + SPQRFactor, spqr, spqr_qmult, spqr_solve + + Notes + ----- + This function is part of an interface to the SuiteSparse SPQR library [#spqr_url]_. + + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#spqr_url] SuiteSparse SPQR + https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/SPQR + """ + if use_singletons: + return SPQRFactor(A, use_singletons=True, order=order, tol=tol) + else: + return SPQRFactor(A, use_singletons=False, order=order, tol=tol).factorize(A) + + +def spqr_solve(A, b, *, transpose=False, min2norm=True): + A, _, _ = validate_csc_input(A) + M, N = A.shape + + if M < N and min2norm: + return SPQRFactor(A.T.tocsc(), use_singletons=True).solve(b, transpose=True) + else: + return SPQRFactor(A, use_singletons=True).solve(b, transpose=transpose) + + +# ------------------------------------------------------------------------------------- +# SPQR +# ------------------------------------------------------------------------------------- +cdef inline int _spqr_noQ( + bint is_real, + bint use_int32, + int ordering, + double tol, + size_t econ, + cholmod_sparse *Ac, + cholmod_sparse **Rs, + void **Es, + cholmod_common *cm +) except -1: + if is_real: + if use_int32: + SuiteSparseQR_noQ[double, int32_t]( + ordering, tol, econ, Ac, Rs, Es, cm + ) + else: + SuiteSparseQR_noQ[double, int64_t]( + ordering, tol, econ, Ac, Rs, Es, cm + ) + else: + if use_int32: + SuiteSparseQR_noQ[cdouble, int32_t]( + ordering, tol, econ, Ac, Rs, Es, cm + ) + else: + SuiteSparseQR_noQ[cdouble, int64_t]( + ordering, tol, econ, Ac, Rs, Es, cm + ) + + _handle_errors(cm.status) + return 0 + + +cdef inline int _spqr_full( + bint is_real, + bint use_int32, + int ordering, + double tol, + size_t econ, + cholmod_sparse *Ac, + cholmod_sparse **Qs, + cholmod_sparse **Rs, + void **Es, + cholmod_common *cm +) except -1: + if is_real: + if use_int32: + SuiteSparseQR_full[double, int32_t]( + ordering, tol, econ, Ac, Qs, Rs, Es, cm + ) + else: + SuiteSparseQR_full[double, int64_t]( + ordering, tol, econ, Ac, Qs, Rs, Es, cm + ) + else: + if use_int32: + SuiteSparseQR_full[cdouble, int32_t]( + ordering, tol, econ, Ac, Qs, Rs, Es, cm + ) + else: + SuiteSparseQR_full[cdouble, int64_t]( + ordering, tol, econ, Ac, Qs, Rs, Es, cm + ) + + _handle_errors(cm.status) + return 0 + + +cdef inline int _spqr_householder( + bint is_real, + bint use_int32, + int ordering, + double tol, + size_t econ, + cholmod_sparse *Ac, + cholmod_sparse **Rs, + void **Es, + cholmod_sparse **Hs, + void **HPinv, + cholmod_dense **HTau, + cholmod_common *cm +) except -1: + if is_real: + if use_int32: + SuiteSparseQR_householder[double, int32_t]( + ordering, tol, econ, Ac, + Rs, Es, Hs, HPinv, HTau, cm + ) + else: + SuiteSparseQR_householder[double, int64_t]( + ordering, tol, econ, Ac, + Rs, Es, Hs, HPinv, HTau, cm + ) + else: + if use_int32: + SuiteSparseQR_householder[cdouble, int32_t]( + ordering, tol, econ, Ac, + Rs, Es, Hs, HPinv, HTau, cm + ) + else: + SuiteSparseQR_householder[cdouble, int64_t]( + ordering, tol, econ, Ac, + Rs, Es, Hs, HPinv, HTau, cm + ) + + _handle_errors(cm.status) + return 0 + + +class SPQRHouseholder(NamedTuple): + """A class to hold the Householder representation of Q. + + Attributes + ---------- + H : ~scipy.sparse.csc_array + The Householder vectors stored in a sparse matrix. + tau : ~numpy.ndarray of float + The Householder coefficients. + perm : ~numpy.ndarray of int + The column permutation vector. + """ + H: ~scipy.sparse.csc_array + tau: ~numpy.ndarray + perm: ~numpy.ndarray + + +def spqr(A, *, mode="full", order=None, tol=None): + r"""Compute the QR factorization. + + This function computes the QR factorization of a sparse matrix :math:`A` such that + + .. math:: + Q R = A E + + where :math:`Q` is an orthogonal matrix and :math:`R` is an upper-triangular + matrix. :math:`E` is a column permutation matrix that reduces fill-in during + the factorization. + + Parameters + ---------- + A : (M, N) array_like or sparse array + An array convertible to a sparse matrix. + mode : {'full', 'r', 'economic', 'householder'}, optional + The mode of the returned Q and R matrices. Options are: + + * ``full``: ``Q`` is size ``(M, M)``, ``R`` is size ``(M, N)``. + * ``economic``: ``Q`` is size ``(M, K)``, ``R`` is size ``(K, N)``, where + ``K = min(M, N)``. + * ``r``: Only return the upper-triangular matrix ``R``. + * ``householder``: Return the Householder vectors and coefficients used to + build ``Q``. This option is similar to ``mode='raw'`` in + :func:`scipy.linalg.qr`. + + order : str, optional + The ordering strategy to use. + tol : float, optional + If the 2-norm of a column in ``A`` is less than ``tol``, that column is + considered to be a zero column. If ``None``, the default tolerance is used. + + Returns + ------- + Q : csc_array + The orthogonal matrix :math:`Q`. Shape (M, M) or (M, K) if ``mode='economic'``. + Not returned if ``mode='r'``. + Replaced by :class:`SPQRHouseholder` if ``mode='householder'``. + R : csc_array + The upper-triangular matrix :math:`R`. Shape (M, N) or (K, N) if ``mode in + ['economic', 'householder']``, where K = min(M, N). + P : ndarray of int + The permutation vector of shape (N,). + + See Also + -------- + SPQRFactor, spqr_factor, spqr_qmult, spqr_solve + + Notes + ----- + This function is part of an interface to the SuiteSparse SPQR library [#spqr_url]_. + + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#spqr_url] SuiteSparse SPQR + https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/SPQR + """ + A, _, _ = validate_csc_input(A) + + cdef Py_ssize_t N = A.shape[1] + + allowed_modes = ("full", "economic", "r", "householder") + if mode not in allowed_modes: + raise ValueError( + f"Invalid mode '{mode}'. Expected one of {allowed_modes}." + ) + + # Promote single to double precision + if not ( + np.issubdtype(A.dtype, np.float64) or np.issubdtype(A.dtype, np.complex128) + ): + if np.issubdtype(A.dtype, np.floating): + A = A.astype(np.promote_types(A.dtype, np.float64)) + elif np.issubdtype(A.dtype, np.complexfloating): + A = A.astype(np.promote_types(A.dtype, np.complex128)) + + cdef int ordering + + if order is None: + ordering = SPQR_ORDERING_DEFAULT + else: + try: + ordering = _ordering_methods[order] + except KeyError: + raise ValueError( + f"Unknown ordering method: {ordering}. " + f"Must be one of {set(_ordering_methods.keys())}." + ) + + cdef double c_tol = tol if tol is not None else SPQR_DEFAULT_TOL + + # Get the input matrix into CHOLMOD format + cdef cholmod_sparse Amatrix + cdef cholmod_sparse *Ac = &Amatrix + cdef int stype = 0 # assume matrix is not symmetric + + _cholmod_sparse_from_csc( + A.shape, A.indptr, A.indices, A.data, stype, Ac + ) + + cdef bint is_real = (Ac.xtype == CHOLMOD_REAL) + cdef bint use_int32 = (Ac.itype == CHOLMOD_INT) + + # Initialize the common object + cdef cholmod_common common + cdef cholmod_common *cm = &common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + # --------------------------------------------------------------------------------- + # Perform the factorization + # --------------------------------------------------------------------------------- + cdef: + cholmod_sparse *Qs = NULL + cholmod_sparse *Rs = NULL + void *Es = NULL + cholmod_sparse *Hs = NULL + void *HPinv = NULL + cholmod_dense *HTau = NULL + size_t econ = Ac.nrow if mode != "economic" else Ac.ncol + + if mode == "r": + _spqr_noQ(is_real, use_int32, ordering, c_tol, econ, Ac, &Rs, &Es, cm) + elif mode in ["full", "economic"]: + _spqr_full(is_real, use_int32, ordering, c_tol, econ, Ac, &Qs, &Rs, &Es, cm) + elif mode == "householder": + _spqr_householder( + is_real, use_int32, ordering, c_tol, econ, Ac, + &Rs, &Es, &Hs, &HPinv, &HTau, cm + ) + else: + raise NotImplementedError(f"{mode=} is not supported.") + + # Get Python objects from the cholmod structs + Q = R = E = H = tau = v = None + cdef Py_ssize_t Mh + + if Qs is not NULL: + Q = _csc_from_cholmod_sparse(Qs, cm) + + if Rs is not NULL: + R = _csc_from_cholmod_sparse(Rs, cm) + + if Es is not NULL: + E = _ndarray_copy_from_intptr(Es, N, use_int32) + + if Hs is not NULL: + H = _csc_from_cholmod_sparse(Hs, cm) + + if HPinv is not NULL and H is not None: + Mh = H.shape[0] + v = _ndarray_copy_from_intptr(HPinv, Mh, use_int32) + + if HTau is not NULL: + tau = _ndarray_from_cholmod_dense(HTau, use_int32, cm).squeeze() + + if use_int32: + cholmod_free(N, sizeof(int32_t), Es, cm) + cholmod_free(Mh, sizeof(int32_t), HPinv, cm) + cholmod_finish(cm) + else: + cholmod_l_free(N, sizeof(int64_t), Es, cm) + cholmod_l_free(Mh, sizeof(int64_t), HPinv, cm) + cholmod_l_finish(cm) + + if mode == "r": + return R, E + elif mode in ["full", "economic"]: + return Q, R, E + else: # mode == "householder" + return SPQRHouseholder(H=H, tau=tau, perm=v), R, E + + +# ------------------------------------------------------------------------------------- +# Qmult +# ------------------------------------------------------------------------------------- +cdef object _qmult_sparse( + int method, + cholmod_sparse *H, + cholmod_dense *HTau, + index_t[::1] HPinv, + object X, + cholmod_common *cm, +): + """Multiply a sparse matrix by Q.""" + cdef cholmod_sparse Xsparse + cdef cholmod_sparse *Xs = &Xsparse + cdef int stype = 0 # assume unsymmetric + _cholmod_sparse_from_csc( + X.shape, X.indptr, X.indices, X.data, stype, Xs + ) + + cdef bint is_real = (Xs.xtype == CHOLMOD_REAL) + cdef bint use_int32 = (Xs.itype == CHOLMOD_INT) + + cdef cholmod_sparse *Ys + + if is_real: + if use_int32: + Ys = SuiteSparseQR_qmult_Hs[double, int32_t]( + method, H, HTau, &HPinv[0], Xs, cm + ) + else: + Ys = SuiteSparseQR_qmult_Hs[double, int64_t]( + method, H, HTau, &HPinv[0], Xs, cm + ) + else: + if use_int32: + Ys = SuiteSparseQR_qmult_Hs[cdouble, int32_t]( + method, H, HTau, &HPinv[0], Xs, cm + ) + else: + Ys = SuiteSparseQR_qmult_Hs[cdouble, int64_t]( + method, H, HTau, &HPinv[0], Xs, cm + ) + + _handle_errors(cm.status) + + return _csc_from_cholmod_sparse(Ys, cm) + + +cdef object _qmult_dense( + int method, + cholmod_sparse *H, + cholmod_dense *HTau, + index_t[::1] HPinv, + value_t[::1, :] X, + cholmod_common *cm, +): + """Multiply a dense matrix by Q.""" + cdef cholmod_dense Xdense + cdef cholmod_dense *Xd = &Xdense + _cholmod_dense_from_ndarray(X, Xd) + + cdef bint is_real = (H.xtype == CHOLMOD_REAL) + cdef bint use_int32 = (H.itype == CHOLMOD_INT) + + cdef cholmod_dense *Yd + + if is_real: + if use_int32: + Yd = SuiteSparseQR_qmult_Hd[double, int32_t]( + method, H, HTau, &HPinv[0], Xd, cm + ) + else: + Yd = SuiteSparseQR_qmult_Hd[double, int64_t]( + method, H, HTau, &HPinv[0], Xd, cm + ) + else: + if use_int32: + Yd = SuiteSparseQR_qmult_Hd[cdouble, int32_t]( + method, H, HTau, &HPinv[0], Xd, cm + ) + else: + Yd = SuiteSparseQR_qmult_Hd[cdouble, int64_t]( + method, H, HTau, &HPinv[0], Xd, cm + ) + + _handle_errors(cm.status) + + return _ndarray_from_cholmod_dense(Yd, use_int32, cm) + + +@cython.boundscheck(False) +@cython.wraparound(False) +def _qmult( + int method, + object H, + value_t[::1, :] tau, + index_t[::1] v, + object X, +): + """Dispatch the correct typed qmult function.""" + cdef bint use_int32 = (index_t is int32_t) + + # Initialize the common object + cdef cholmod_common common + cdef cholmod_common *cm = &common + + if use_int32: + cholmod_start(cm) + else: + cholmod_l_start(cm) + + # Make cholmod objects from H, tau, v + cdef cholmod_sparse Hsparse + cdef cholmod_sparse *Hs = &Hsparse + cdef int stype = 0 # not symmetric + _cholmod_sparse_from_csc( + H.shape, H.indptr, H.indices, H.data, stype, Hs + ) + + cdef cholmod_dense HTau_dense + cdef cholmod_dense *HTau = &HTau_dense + _cholmod_dense_from_ndarray(tau, HTau) + + # If X is dense, get a memoryview of the same type as tau + cdef value_t[::1, :] X_view + + # Compute the multiplication + if issparse(X): + Y = _qmult_sparse(method, Hs, HTau, v, X, cm) + else: + X_view = X # assign the view onto X + Y = _qmult_dense(method, Hs, HTau, v, X_view, cm) + + if use_int32: + cholmod_finish(cm) + else: + cholmod_l_finish(cm) + + return Y + + +def spqr_qmult(house, X, method="QX"): + try: + H, tau, v = house + H, _, itype = validate_csc_input(H) + tau = np.asfortranarray(tau).reshape((1, -1)) # for cholmod_dense + assert tau.shape == (1, H.shape[1]) + assert tau.dtype == H.dtype + v = np.asfortranarray(v) + assert v.shape == (H.shape[0],) + assert v.dtype == itype + except Exception: + raise ValueError( + "house must be a tuple of (H, tau, v) representing the " + "Householder vectors, coefficients, and permutation. " + f"Got {house}." + ) + + if not issparse(X): + try: + X = np.asfortranarray(X) + except Exception: + raise ValueError("X must be an ndarray or sparse matrix.") + + if X.ndim not in (1, 2): + raise ValueError("X must be a 1D or 2D array.") + + cdef int c_method = _qmult_int_from_str(method) + + # Check shape compatibility with Q + cdef Py_ssize_t X_dim = ( + X.shape[0] + if method == "QTX" or method == "QX" + else X.shape[1] + ) + + M = H.shape[0] + if M != X_dim: + raise ValueError( + "Input X must have compatible shape with Q. " + f"Expected {M}, got {X_dim}." + ) + + cdef bint return_1D = X.ndim == 1 + + # cholmod_sparse/dense expects a 2D array + if X.ndim == 1: + X = X.reshape((-1, 1)) + + # Perform the multiplication + Y = _qmult(c_method, H, tau, v, X) + + if return_1D: + Y = Y[:, 0] + + return Y + + +# ------------------------------------------------------------------------------------- +# Docstrings +# ------------------------------------------------------------------------------------- +_SOLVE_DOC_TEMPLATE = r""" +Solve a linear system using the SPQR factorization. + +Solve a linear system for :math:`x` given the right-hand side +:math:`b` as either a vector or a matrix with multiple right-hand sides. + +If ``transpose=False``, solve + +.. math:: + A x = b + +or, if ``transpose=True``, solve + +.. math:: + A^{{\top}} x = b. + +Parameters +---------- +{A_doc} +b : (M,) or (M, K) numpy.ndarray + The right-hand side vector or matrix. ``M`` should be the number of rows in + ``A`` if ``transpose=False``, otherwise the number of columns. +transpose : bool, optional + Whether to solve the transposed system. Default is False. +{min2norm} + +Returns +------- +x : (N,) or (N, K) numpy.ndarray or sparse array + The solution vector or matrix. If ``b`` is a 1D array, then ``x`` is + returned as a 1D array. If ``b`` is a 2D array with ``K`` columns, + then ``x`` is returned as a 2D array with ``K`` columns. If ``b`` + is a sparse array, then ``x`` is also returned as a sparse array. + ``N`` is the number of columns in ``A`` if ``transpose=False``, + otherwise the number of rows. + +See Also +-------- +SPQRFactor, spqr_factor, spqr, spqr_qmult + +Notes +----- +Part of an interface to the SuiteSparse SPQR library [#spqr_url]_. + + +.. versionadded:: 0.5.0 + +References +---------- +.. [#spqr_url] SuiteSparse SPQR + https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/SPQR +""" + + +_A_doc= """A : (M, N) array_like or sparse array + An array convertible to a sparse matrix.""" + +_min2norm_doc = """min2norm : bool, optional + If True, compute the minimum 2-norm solution when ``A`` is underdetermined. + ``transpose`` is ignored in this case. Default is True. If False, the + solution of an underdetermined system is not guaranteed to be the minimum + 2-norm solution.""" + +# Format the docstrings +SPQRFactor.solve.__doc__ = _SOLVE_DOC_TEMPLATE.format(A_doc="", min2norm="") +spqr_solve.__doc__ = _SOLVE_DOC_TEMPLATE.format( + A_doc=_A_doc, + min2norm=_min2norm_doc +) + + +_QMULT_DOC_TEMPLATE = r""" +Multiply by `Q` using the Householder representation. + +Parameters +---------- +{house_doc} +X : (M, N) numpy.ndarray or sparse array + The matrix to be multiplied. Must have compatible shape with ``Q``. +method : str , optional + The multiplication method. Options are: + + * ``QX`` : compute :math:`Q X` + * ``QTX`` : compute :math:`Q^{{\top}} X` + * ``XQ`` : compute :math:`X Q` + * ``XQT`` : compute :math:`X Q^{{\top}}` + + Default is ``QX``. The transpose is the conjugate transpose for complex data. + +Returns +------- +Y : (M, N) numpy.ndarray or sparse array + The result of the multiplication. If ``X`` is a sparse array, then ``Y`` is + also returned as a sparse array. + +See Also +-------- +SPQRFactor, spqr_factor, spqr, spqr_solve{see_also} + +Notes +----- +This function is part of an interface to the SuiteSparse SPQR library [#spqr_url]_. + + +.. versionadded:: 0.5.0 + +References +---------- +.. [#spqr_url] SuiteSparse SPQR + https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/SPQR +""" + + +_qmult_house_doc = """house : SPQRHouseholder or tuple + A tuple ``(H, tau, v)`` representing the Householder vectors ``H``, + coefficients ``tau``, and the column permutation vector ``v``. Typically, + these are created from ``Ht, R, p = spqr(A, mode='householder')``.""" + +SPQRFactor.qmult.__doc__ = _QMULT_DOC_TEMPLATE.format( + house_doc="", + see_also=", spqr_qmult" +) + +spqr_qmult.__doc__ = _QMULT_DOC_TEMPLATE.format( + house_doc=_qmult_house_doc, + see_also="" +) diff --git a/src/sksparse/umfpack.pxd b/src/sksparse/umfpack.pxd new file mode 100644 index 00000000..ed7099aa --- /dev/null +++ b/src/sksparse/umfpack.pxd @@ -0,0 +1,609 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 the scikit-sparse developers. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: umfpack.pxd +# Created: 2025-10-16 11:17 +# ============================================================================= +# distutils: language = c + +from libc.stdint cimport int32_t, int64_t + + +cdef extern from "umfpack.h": + # Get all #define constants + enum: + # length of Control and Info arrays + UMFPACK_INFO + UMFPACK_CONTROL + + # ------------------------------------------------------------------------- + # Info Parameters + # ------------------------------------------------------------------------- + # return status and Info for all routines + UMFPACK_STATUS + UMFPACK_NROW + UMFPACK_NCOL + UMFPACK_NZ + + # computed in _symbolic and _numeric + UMFPACK_SIZE_OF_UNIT + + # computed in _symbolic + UMFPACK_SIZE_OF_INT + UMFPACK_SIZE_OF_LONG + UMFPACK_SIZE_OF_POINTER + UMFPACK_SIZE_OF_ENTRY + UMFPACK_NDENSE_ROW + UMFPACK_NEMPTY_ROW + UMFPACK_NDENSE_COL + UMFPACK_NEMPTY_COL + UMFPACK_SYMBOLIC_DEFRAG + UMFPACK_SYMBOLIC_PEAK_MEMORY + UMFPACK_SYMBOLIC_SIZE + UMFPACK_SYMBOLIC_TIME + UMFPACK_SYMBOLIC_WALLTIME + UMFPACK_STRATEGY_USED + UMFPACK_ORDERING_USED + UMFPACK_QFIXED + UMFPACK_DIAG_PREFERRED + UMFPACK_PATTERN_SYMMETRY + UMFPACK_NZ_A_PLUS_AT + UMFPACK_NZDIAG + + # AMD statistics + UMFPACK_SYMMETRIC_LUNZ + UMFPACK_SYMMETRIC_FLOPS + UMFPACK_SYMMETRIC_NDENSE + UMFPACK_SYMMETRIC_DMAX + + # singleton pruning + UMFPACK_COL_SINGLETONS + UMFPACK_ROW_SINGLETONS + UMFPACK_N2 + UMFPACK_S_SYMMETRIC + + # estimates in _symbolic + UMFPACK_NUMERIC_SIZE_ESTIMATE + UMFPACK_PEAK_MEMORY_ESTIMATE + UMFPACK_FLOPS_ESTIMATE + UMFPACK_LNZ_ESTIMATE + UMFPACK_UNZ_ESTIMATE + UMFPACK_VARIABLE_INIT_ESTIMATE + UMFPACK_VARIABLE_PEAK_ESTIMATE + UMFPACK_VARIABLE_FINAL_ESTIMATE + UMFPACK_MAX_FRONT_SIZE_ESTIMATE + UMFPACK_MAX_FRONT_NROWS_ESTIMATE + UMFPACK_MAX_FRONT_NCOLS_ESTIMATE + + # exact values in _numeric + UMFPACK_NUMERIC_SIZE + UMFPACK_PEAK_MEMORY + UMFPACK_FLOPS + UMFPACK_LNZ + UMFPACK_UNZ + UMFPACK_VARIABLE_INIT + UMFPACK_VARIABLE_PEAK + UMFPACK_VARIABLE_FINAL + UMFPACK_MAX_FRONT_SIZE + UMFPACK_MAX_FRONT_NROWS + UMFPACK_MAX_FRONT_NCOLS + + # computed in _numeric + UMFPACK_NUMERIC_DEFRAG + UMFPACK_NUMERIC_REALLOC + UMFPACK_NUMERIC_COSTLY_REALLOC + UMFPACK_COMPRESSED_PATTERN + UMFPACK_LU_ENTRIES + UMFPACK_NUMERIC_TIME + UMFPACK_UDIAG_NZ + UMFPACK_RCOND + UMFPACK_WAS_SCALED + UMFPACK_RSMIN + UMFPACK_RSMAX + UMFPACK_UMIN + UMFPACK_UMAX + UMFPACK_ALLOC_INIT_USED + UMFPACK_FORCED_UPDATES + UMFPACK_NUMERIC_WALLTIME + UMFPACK_NOFF_DIAG + + UMFPACK_ALL_LNZ + UMFPACK_ALL_UNZ + UMFPACK_NZDROPPED + + # computed in _solve + UMFPACK_IR_TAKEN + UMFPACK_IR_ATTEMPTED + UMFPACK_OMEGA1 + UMFPACK_OMEGA2 + UMFPACK_SOLVE_FLOPS + UMFPACK_SOLVE_TIME + UMFPACK_SOLVE_WALLTIME + + # ------------------------------------------------------------------------- + # Control parameters for all routines + # ------------------------------------------------------------------------- + UMFPACK_PRL + + # used in _symbolic only + UMFPACK_DENSE_ROW + UMFPACK_DENSE_COL + UMFPACK_BLOCK_SIZE + UMFPACK_STRATEGY + UMFPACK_ORDERING + UMFPACK_FIXQ + UMFPACK_AMD_DENSE + UMFPACK_AGGRESSIVE + UMFPACK_SINGLETONS + + # used in _numeric only + UMFPACK_PIVOT_TOLERANCE + UMFPACK_ALLOC_INIT + UMFPACK_SYM_PIVOT_TOLERANCE + UMFPACK_SCALE + UMFPACK_FRONT_ALLOC_INIT + UMFPACK_DROPTOL + + # used in _solve only + UMFPACK_IRSTEP + + # compile-time + UMFPACK_COMPILED_WITH_BLAS + + # new in v6.0.0 + UMFPACK_STRATEGY_THRESH_SYM + UMFPACK_STRATEGY_THRESH_NNZDIAG + + # Control[UMFPACK_STRATEGY] values + UMFPACK_STRATEGY_AUTO + UMFPACK_STRATEGY_UNSYMMETRIC + UMFPACK_STRATEGY_OBSOLETE + UMFPACK_STRATEGY_SYMMETRIC + + UMFPACK_SCALE_NONE + UMFPACK_SCALE_SUM + UMFPACK_SCALE_MAX + + UMFPACK_ORDERING_CHOLMOD + UMFPACK_ORDERING_AMD + UMFPACK_ORDERING_GIVEN + UMFPACK_ORDERING_METIS + UMFPACK_ORDERING_BEST + UMFPACK_ORDERING_NONE + UMFPACK_ORDERING_USER + UMFPACK_ORDERING_METIS_GUARD + + # # Defaults + # UMFPACK_DEFAULT_PRL + # UMFPACK_DEFAULT_DENSE_ROW + # UMFPACK_DEFAULT_DENSE_COL + # UMFPACK_DEFAULT_PIVOT_TOLERANCE + # UMFPACK_DEFAULT_SYM_PIVOT_TOLERANCE + # UMFPACK_DEFAULT_BLOCK_SIZE + # UMFPACK_DEFAULT_ALLOC_INIT + # UMFPACK_DEFAULT_FRONT_ALLOC_INIT + # UMFPACK_DEFAULT_IRSTEP + # UMFPACK_DEFAULT_SCALE + # UMFPACK_DEFAULT_STRATEGY + # UMFPACK_DEFAULT_AMD_DENSE + # UMFPACK_DEFAULT_FIXQ + # UMFPACK_DEFAULT_AGGRESSIVE + # UMFPACK_DEFAULT_DROPTOL + # UMFPACK_DEFAULT_ORDERING + # UMFPACK_DEFAULT_SINGLETONS + # UMFPACK_DEFAULT_STRATEGY_THRESH_SYM + # UMFPACK_DEFAULT_STRATEGY_THRESH_NNZDIAG + + # Output status values + UMFPACK_OK + + UMFPACK_WARNING_singular_matrix + UMFPACK_WARNING_determinant_underflow + UMFPACK_WARNING_determinant_overflow + + UMFPACK_ERROR_out_of_memory + UMFPACK_ERROR_invalid_Numeric_object + UMFPACK_ERROR_invalid_Symbolic_object + UMFPACK_ERROR_argument_missing + UMFPACK_ERROR_n_nonpositive + UMFPACK_ERROR_invalid_matrix + UMFPACK_ERROR_different_pattern + UMFPACK_ERROR_invalid_system + UMFPACK_ERROR_invalid_permutation + UMFPACK_ERROR_internal_error + UMFPACK_ERROR_file_IO + UMFPACK_ERROR_ordering_failed + UMFPACK_ERROR_invalid_blob + + # Solve system types + UMFPACK_A + UMFPACK_At + UMFPACK_Aat + + UMFPACK_Pt_L + UMFPACK_L + UMFPACK_Lt_P + UMFPACK_Lat_P + UMFPACK_Lt + UMFPACK_Lat + + UMFPACK_U_Qt + UMFPACK_U + UMFPACK_Q_Ut + UMFPACK_Q_Uat + UMFPACK_Ut + UMFPACK_Uat + + # ------------------------------------------------------------------------- + # Functions + # ------------------------------------------------------------------------- + void umfpack_di_defaults( + double Control[UMFPACK_CONTROL] + ) + + int umfpack_di_symbolic( + int32_t n_row, + int32_t n_col, + const int32_t Ap[], + const int32_t Ai[], + const double Ax[], + void **Symbolic, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_dl_symbolic( + int64_t n_row, + int64_t n_col, + const int64_t Ap[], + const int64_t Ai[], + const double Ax[], + void **Symbolic, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_zi_symbolic( + int32_t n_row, + int32_t n_col, + const int32_t Ap[], + const int32_t Ai[], + const double Ax[], + const double Az[], + void **Symbolic, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_zl_symbolic( + int64_t n_row, + int64_t n_col, + const int64_t Ap[], + const int64_t Ai[], + const double Ax[], + const double Az[], + void **Symbolic, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_di_numeric( + const int32_t Ap[], + const int32_t Ai[], + const double Ax[], + void *Symbolic, + void **Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_dl_numeric( + const int64_t Ap[], + const int64_t Ai[], + const double Ax[], + void *Symbolic, + void **Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_zi_numeric( + const int32_t Ap[], + const int32_t Ai[], + const double Ax[], + const double Az[], + void *Symbolic, + void **Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_zl_numeric( + const int64_t Ap[], + const int64_t Ai[], + const double Ax[], + const double Az[], + void *Symbolic, + void **Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + + int umfpack_di_solve( + int sys, + const int32_t Ap[], + const int32_t Ai[], + const double Ax[], + double X[], + const double B[], + void *Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_dl_solve( + int sys, + const int64_t Ap[], + const int64_t Ai[], + const double Ax[], + double X[], + const double B[], + void *Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_zi_solve( + int sys, + const int32_t Ap[], + const int32_t Ai[], + const double Ax[], + const double Az[], + double Xx[], + double Xz[], + const double Bx[], + const double Bz[], + void *Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_zl_solve( + int sys, + const int64_t Ap[], + const int64_t Ai[], + const double Ax[], + const double Az[], + double Xx[], + double Xz[], + const double Bx[], + const double Bz[], + void *Numeric, + const double Control[UMFPACK_CONTROL], + double Info[UMFPACK_INFO] + ) + + int umfpack_di_get_numeric( + int32_t Lp[], + int32_t Lj[], + double Lx[], + int32_t Up[], + int32_t Ui[], + double Ux[], + int32_t P[], + int32_t Q[], + double Dx[], + int32_t *do_recip, + double Rs[], + void *Numeric + ) + + int umfpack_dl_get_numeric( + int64_t Lp[], + int64_t Lj[], + double Lx[], + int64_t Up[], + int64_t Ui[], + double Ux[], + int64_t P[], + int64_t Q[], + double Dx[], + int64_t *do_recip, + double Rs[], + void *Numeric + ) + + int umfpack_zi_get_numeric( + int32_t Lp[], + int32_t Lj[], + double Lx[], + double Lz[], + int32_t Up[], + int32_t Ui[], + double Ux[], + double Uz[], + int32_t P[], + int32_t Q[], + double Dx[], + double Dz[], + int32_t *do_recip, + double Rs[], + void *Numeric + ) + + int umfpack_zl_get_numeric( + int64_t Lp[], + int64_t Lj[], + double Lx[], + double Lz[], + int64_t Up[], + int64_t Ui[], + double Ux[], + double Uz[], + int64_t P[], + int64_t Q[], + double Dx[], + double Dz[], + int64_t *do_recip, + double Rs[], + void *Numeric + ) + + int umfpack_di_get_determinant( + double *Mx, + double *Ex, + void *Numeric, + double Info[UMFPACK_INFO] + ) + + int umfpack_dl_get_determinant( + double *Mx, + double *Ex, + void *Numeric, + double Info[UMFPACK_INFO] + ) + + int umfpack_zi_get_determinant( + double *Mx, + double *Mz, + double *Ex, + void *Numeric, + double Info[UMFPACK_INFO] + ) + + int umfpack_zl_get_determinant( + double *Mx, + double *Mz, + double *Ex, + void *Numeric, + double Info[UMFPACK_INFO] + ) + + int umfpack_di_copy_symbolic( + void **Symbolic, + void *Original + ) + + int umfpack_dl_copy_symbolic( + void **Symbolic, + void *Original + ) + + int umfpack_zi_copy_symbolic( + void **Symbolic, + void *Original + ) + + int umfpack_zl_copy_symbolic( + void **Symbolic, + void *Original + ) + + int umfpack_di_copy_numeric( + void **Numeric, + void *Original + ) + + int umfpack_dl_copy_numeric( + void **Numeric, + void *Original + ) + + int umfpack_zi_copy_numeric( + void **Numeric, + void *Original + ) + + int umfpack_zl_copy_numeric( + void **Numeric, + void *Original + ) + + void umfpack_di_free_symbolic( + void **Symbolic + ) + + void umfpack_dl_free_symbolic( + void **Symbolic + ) + + void umfpack_zi_free_symbolic( + void **Symbolic + ) + + void umfpack_zl_free_symbolic( + void **Symbolic + ) + + void umfpack_di_free_numeric( + void **Numeric + ) + + void umfpack_dl_free_numeric( + void **Numeric + ) + + void umfpack_zi_free_numeric( + void **Numeric + ) + + void umfpack_zl_free_numeric( + void **Numeric + ) + + # ------------------------------------------------------------------------- + # Debugging + # ------------------------------------------------------------------------- + void umfpack_di_report_info( + const double Control[UMFPACK_CONTROL], + const double Info[UMFPACK_INFO] + ) + + void umfpack_di_report_control( + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_di_report_symbolic( + void *Symbolic, + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_dl_report_symbolic( + void *Symbolic, + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_zi_report_symbolic( + void *Symbolic, + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_zl_report_symbolic( + void *Symbolic, + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_di_report_numeric( + void *Numeric, + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_dl_report_numeric( + void *Numeric, + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_zi_report_numeric( + void *Numeric, + const double Control[UMFPACK_CONTROL] + ) + + int umfpack_zl_report_numeric( + void *Numeric, + const double Control[UMFPACK_CONTROL] + ) + diff --git a/src/sksparse/umfpack.pyx b/src/sksparse/umfpack.pyx new file mode 100644 index 00000000..5c99e9b7 --- /dev/null +++ b/src/sksparse/umfpack.pyx @@ -0,0 +1,2199 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: umfpack.pyx +# Created: 2025-10-16 11:35 +# ============================================================================= + +""" +=================================================================== +Unsymmetric Multifrontal LU Decomposition (:mod:`sksparse.umfpack`) +=================================================================== + +.. currentmodule:: sksparse.umfpack + +.. versionadded:: 0.5.0 + + +An interface to the SuiteSparse `UMFPACK +`_ +package, which computes the LU factorization and solves systems of equations +for sparse, possibly non-symmetric, indefinite matrices. + + +Function Interface +------------------ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + umf_solve - Solve a linear system using the UMFPACK factorization. + + +Object Interface +---------------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + umf_factor - Compute the LU factorization of a sparse matrix. + UMFFactor - An object-oriented interface to UMFPACK. + UMFInfo - A dataclass to return UMFPACK info. + UMFControl - A dataclass to set UMFPACK control parameters. + + +.. umfpack-exceptions: + +Warnings and Exceptions +----------------------- + +.. autosummary:: + :toctree: generated/ + + UMFPACKWarning + UMFPACKSingularMatrixWarning + UMFPACKDeterminantUnderflowWarning + UMFPACKDeterminantOverflowWarning + + UMFPACKError + UMFPACKOutOfMemoryError + UMFPACKInvalidNumericObjectError + UMFPACKInvalidSymbolicObjectError + UMFPACKArgumentMissingError + UMFPACKNonpositiveError + UMFPACKInvalidMatrixError + UMFPACKDifferentPatternError + UMFPACKInvalidSystemError + UMFPACKInvalidPermutationError + UMFPACKInternalError + UMFPACKFileIOError + UMFPACKOrderingFailedError + UMFPACKInvalidBlobError + + +References +---------- +* `SuiteSparse homepage `_ +* `SuiteSparse UMFPACK `_ +""" + +cimport cython +cimport numpy as cnp + +import numpy as np +from scipy.sparse import issparse, csr_array, csc_array +import warnings + +from .utils import validate_csc_input + + +# ----------------------------------------------------------------------------- +# Define types +# ----------------------------------------------------------------------------- +ctypedef fused index_t: + int32_t + int64_t + + +ctypedef fused value_t: + double + double complex + + +# ----------------------------------------------------------------------------- +# Warnings and Errors +# ----------------------------------------------------------------------------- +class UMFPACKWarning(Warning): + """A warning occurred in a UMFPACK routine.""" + pass + + +class UMFPACKSingularMatrixWarning(UMFPACKWarning): + """A singular matrix was encountered in a UMFPACK routine.""" + pass + + +class UMFPACKDeterminantUnderflowWarning(UMFPACKWarning): + """A determinant underflow was encountered in a UMFPACK routine.""" + pass + + +class UMFPACKDeterminantOverflowWarning(UMFPACKWarning): + """A determinant overflow was encountered in a UMFPACK routine.""" + pass + + +class UMFPACKError(Exception): + """An error occurred in a UMFPACK routine.""" + pass + + +class UMFPACKOutOfMemoryError(MemoryError, UMFPACKError): + """UMFPACK ran out of memory.""" + pass + + +class UMFPACKInvalidNumericObjectError(UMFPACKError): + """An invalid Numeric object was passed to a UMFPACK routine.""" + pass + + +class UMFPACKInvalidSymbolicObjectError(UMFPACKError): + """An invalid Symbolic object was passed to a UMFPACK routine.""" + pass + + +class UMFPACKArgumentMissingError(UMFPACKError): + """A required argument was missing in a UMFPACK routine.""" + pass + + +class UMFPACKNonpositiveError(UMFPACKError): + """A non-positive value for n was passed to a UMFPACK routine.""" + pass + + +class UMFPACKInvalidMatrixError(UMFPACKError): + """An invalid matrix was passed to a UMFPACK routine.""" + pass + + +class UMFPACKDifferentPatternError(UMFPACKError): + """A matrix with a different nonzero pattern was passed to a UMFPACK routine.""" + pass + + +class UMFPACKInvalidSystemError(UMFPACKError): + """An invalid system type was passed to a UMFPACK routine.""" + pass + + +class UMFPACKInvalidPermutationError(UMFPACKError): + """An invalid permutation was passed to a UMFPACK routine.""" + pass + + +class UMFPACKInternalError(UMFPACKError): + """An internal error occurred in a UMFPACK routine.""" + pass + + +class UMFPACKFileIOError(UMFPACKError): + """A file I/O error occurred in a UMFPACK routine.""" + pass + + +class UMFPACKOrderingFailedError(UMFPACKError): + """The ordering algorithm failed in a UMFPACK routine.""" + pass + + +class UMFPACKInvalidBlobError(UMFPACKError): + """An invalid blob was passed to a UMFPACK routine.""" + pass + + +# Known Errors +cdef dict _ERROR_INDEX = { + UMFPACK_WARNING_singular_matrix: ( + UMFPACKSingularMatrixWarning, + "Matrix is singular." + ), + UMFPACK_WARNING_determinant_underflow: ( + UMFPACKDeterminantUnderflowWarning, + "Determinant underflow." + ), + UMFPACK_WARNING_determinant_overflow: ( + UMFPACKDeterminantOverflowWarning, + "Determinant overflow." + ), + UMFPACK_ERROR_out_of_memory: ( + UMFPACKOutOfMemoryError, + "Out of memory." + ), + UMFPACK_ERROR_invalid_Numeric_object: ( + UMFPACKInvalidNumericObjectError, + "Invalid Numeric object." + ), + UMFPACK_ERROR_invalid_Symbolic_object: ( + UMFPACKInvalidSymbolicObjectError, + "Invalid Symbolic object." + ), + UMFPACK_ERROR_argument_missing: ( + UMFPACKArgumentMissingError, + "A required argument is missing." + ), + UMFPACK_ERROR_n_nonpositive: ( + UMFPACKNonpositiveError, + "Input N is non-positive." + ), + UMFPACK_ERROR_invalid_matrix: ( + UMFPACKInvalidMatrixError, + "Invalid matrix." + ), + UMFPACK_ERROR_different_pattern: ( + UMFPACKDifferentPatternError, + ("Matrix has different nonzero pattern than the matrix that was used" + "for the symbolic analysis.") + ), + UMFPACK_ERROR_invalid_system: ( + UMFPACKInvalidSystemError, + "Invalid system type argument, or the matrix is not square." + ), + UMFPACK_ERROR_invalid_permutation: ( + UMFPACKInvalidPermutationError, + "Invalid permutation." + ), + UMFPACK_ERROR_internal_error: ( + UMFPACKInternalError, + "An internal error occurred." + ), + UMFPACK_ERROR_file_IO: ( + UMFPACKFileIOError, + "A file I/O error occurred." + ), + UMFPACK_ERROR_ordering_failed: ( + UMFPACKOrderingFailedError, + "The ordering algorithm failed." + ), + UMFPACK_ERROR_invalid_blob: ( + UMFPACKInvalidBlobError, + "Invalid blob." + ), +} + + +cdef int _handle_errors(int status) except -1 with gil: + """Handle UMFPACK errors by raising Python exceptions or warnings. + + This function should be called with the return ``status`` after any UMFPACK + C function that may fail. + + Parameters + ---------- + status : int + The UMFPACK exit status code. + + Returns + ------- + None + + Raises + ------ + :exc:`UMFPACKWarning` + Raises a warning for non-critical issues. + :exc:`UMFPACKError` or subclass + Raises an appropriate Python exception based on the UMFPACK status code. + """ + if status == UMFPACK_OK: + return 0 + + # Fallback to generic error for unknown codes + exc_class, msg = _ERROR_INDEX.get( + status, + (UMFPACKError, "An unknown UMFPACK error occurred.") + ) + full_msg = f"{msg} (code {status:d})" + + if issubclass(exc_class, Warning): + warnings.warn(full_msg, exc_class, stacklevel=2) + else: + raise exc_class(full_msg) + + +# ----------------------------------------------------------------------------- +# Parameter Mappings +# ----------------------------------------------------------------------------- +cdef dict _INFO_INDEX = { + "status": UMFPACK_STATUS, + "n_row": UMFPACK_NROW, + "n_col": UMFPACK_NCOL, + "nz": UMFPACK_NZ, + "size_of_unit": UMFPACK_SIZE_OF_UNIT, + "size_of_int": UMFPACK_SIZE_OF_INT, + "size_of_long": UMFPACK_SIZE_OF_LONG, + "size_of_pointer": UMFPACK_SIZE_OF_POINTER, + "size_of_entry": UMFPACK_SIZE_OF_ENTRY, + "ndense_row": UMFPACK_NDENSE_ROW, + "nempty_row": UMFPACK_NEMPTY_ROW, + "ndense_col": UMFPACK_NDENSE_COL, + "nempty_col": UMFPACK_NEMPTY_COL, + "symbolic_defrag": UMFPACK_SYMBOLIC_DEFRAG, + "symbolic_peak_memory": UMFPACK_SYMBOLIC_PEAK_MEMORY, + "symbolic_size": UMFPACK_SYMBOLIC_SIZE, + "symbolic_time": UMFPACK_SYMBOLIC_TIME, + "symbolic_walltime": UMFPACK_SYMBOLIC_WALLTIME, + "strategy_used": UMFPACK_STRATEGY_USED, + "ordering_used": UMFPACK_ORDERING_USED, + "qfixed": UMFPACK_QFIXED, + "diag_preferred": UMFPACK_DIAG_PREFERRED, + "pattern_symmetry": UMFPACK_PATTERN_SYMMETRY, + "nz_a_plus_at": UMFPACK_NZ_A_PLUS_AT, + "nzdiag": UMFPACK_NZDIAG, + "symmetric_lunz": UMFPACK_SYMMETRIC_LUNZ, + "symmetric_flops": UMFPACK_SYMMETRIC_FLOPS, + "symmetric_ndense": UMFPACK_SYMMETRIC_NDENSE, + "symmetric_dmax": UMFPACK_SYMMETRIC_DMAX, + "col_singletons": UMFPACK_COL_SINGLETONS, + "row_singletons": UMFPACK_ROW_SINGLETONS, + "n2": UMFPACK_N2, + "s_symmetric": UMFPACK_S_SYMMETRIC, + "numeric_size_estimate": UMFPACK_NUMERIC_SIZE_ESTIMATE, + "peak_memory_estimate": UMFPACK_PEAK_MEMORY_ESTIMATE, + "flops_estimate": UMFPACK_FLOPS_ESTIMATE, + "lnz_estimate": UMFPACK_LNZ_ESTIMATE, + "unz_estimate": UMFPACK_UNZ_ESTIMATE, + "variable_init_estimate": UMFPACK_VARIABLE_INIT_ESTIMATE, + "variable_peak_estimate": UMFPACK_VARIABLE_PEAK_ESTIMATE, + "variable_final_estimate": UMFPACK_VARIABLE_FINAL_ESTIMATE, + "max_front_size_estimate": UMFPACK_MAX_FRONT_SIZE_ESTIMATE, + "max_front_nrows_estimate": UMFPACK_MAX_FRONT_NROWS_ESTIMATE, + "max_front_ncols_estimate": UMFPACK_MAX_FRONT_NCOLS_ESTIMATE, + "numeric_size": UMFPACK_NUMERIC_SIZE, + "peak_memory": UMFPACK_PEAK_MEMORY, + "flops": UMFPACK_FLOPS, + "lnz": UMFPACK_LNZ, + "unz": UMFPACK_UNZ, + "variable_init": UMFPACK_VARIABLE_INIT, + "variable_peak": UMFPACK_VARIABLE_PEAK, + "variable_final": UMFPACK_VARIABLE_FINAL, + "max_front_size": UMFPACK_MAX_FRONT_SIZE, + "max_front_nrows": UMFPACK_MAX_FRONT_NROWS, + "max_front_ncols": UMFPACK_MAX_FRONT_NCOLS, + "numeric_defrag": UMFPACK_NUMERIC_DEFRAG, + "numeric_realloc": UMFPACK_NUMERIC_REALLOC, + "numeric_costly_realloc": UMFPACK_NUMERIC_COSTLY_REALLOC, + "compressed_pattern": UMFPACK_COMPRESSED_PATTERN, + "lu_entries": UMFPACK_LU_ENTRIES, + "numeric_time": UMFPACK_NUMERIC_TIME, + "nz_udiag": UMFPACK_UDIAG_NZ, + "rcond": UMFPACK_RCOND, + "was_scaled": UMFPACK_WAS_SCALED, + "rsmin": UMFPACK_RSMIN, + "rsmax": UMFPACK_RSMAX, + "umin": UMFPACK_UMIN, + "umax": UMFPACK_UMAX, + "alloc_init_used": UMFPACK_ALLOC_INIT_USED, + "forced_updates": UMFPACK_FORCED_UPDATES, + "numeric_walltime": UMFPACK_NUMERIC_WALLTIME, + "noff_diag": UMFPACK_NOFF_DIAG, + "all_lnz": UMFPACK_ALL_LNZ, + "all_unz": UMFPACK_ALL_UNZ, + "nzdropped": UMFPACK_NZDROPPED, + "ir_taken": UMFPACK_IR_TAKEN, + "ir_attempted": UMFPACK_IR_ATTEMPTED, + "omega1": UMFPACK_OMEGA1, + "omega2": UMFPACK_OMEGA2, + "solve_flops": UMFPACK_SOLVE_FLOPS, + "solve_time": UMFPACK_SOLVE_TIME, + "solve_walltime": UMFPACK_SOLVE_WALLTIME, +} + + +cdef list _INFO_INT_NAMES = [ + "status", + "n_row", + "n_col", + "nz", + "size_of_unit", + "size_of_int", + "size_of_long", + "size_of_pointer", + "size_of_entry", + "ndense_row", + "nempty_row", + "ndense_col", + "nempty_col", + "symbolic_defrag", + "symbolic_peak_memory", + "symbolic_size", + "strategy_used", + "ordering_used", + "qfixed", + "diag_preferred", + "nz_a_plus_at", + "nzdiag", + "symmetric_lunz", + "symmetric_flops", + "symmetric_ndense", + "symmetric_dmax", + "col_singletons", + "row_singletons", + "n2", + "s_symmetric", + "numeric_size_estimate", + "peak_memory_estimate", + "flops_estimate", + "lnz_estimate", + "unz_estimate", + "variable_init_estimate", + "variable_peak_estimate", + "variable_final_estimate", + "max_front_size_estimate", + "max_front_nrows_estimate", + "max_front_ncols_estimate", + "numeric_size", + "peak_memory", + "flops", + "lnz", + "unz", + "variable_init", + "variable_peak", + "variable_final", + "max_front_size", + "max_front_nrows", + "max_front_ncols", + "numeric_defrag", + "numeric_realloc", + "numeric_costly_realloc", + "compressed_pattern", + "lu_entries", + "nz_udiag", + "forced_updates", + "noff_diag", + "all_lnz", + "all_unz", + "nzdropped", + "ir_taken", + "ir_attempted", + "solve_flops", +] + +cdef list _INFO_BOOL_NAMES = [ + "qfixed", + "diag_preferred", + "aggressive", +] + + +cdef dict _CONTROL_INDEX = { + "print_level": UMFPACK_PRL, + "dense_row": UMFPACK_DENSE_ROW, + "dense_col": UMFPACK_DENSE_COL, + "blas3_block_size": UMFPACK_BLOCK_SIZE, + "strategy": UMFPACK_STRATEGY, + "ordering_method": UMFPACK_ORDERING, + "fixQ": UMFPACK_FIXQ, + "amd_dense": UMFPACK_AMD_DENSE, + "aggressive": UMFPACK_AGGRESSIVE, + "singletons": UMFPACK_SINGLETONS, + "pivot_tol": UMFPACK_PIVOT_TOLERANCE, + "alloc_init": UMFPACK_ALLOC_INIT, + "sym_pivot_tol": UMFPACK_SYM_PIVOT_TOLERANCE, + "row_scale": UMFPACK_SCALE, + "front_alloc_init": UMFPACK_FRONT_ALLOC_INIT, + "droptol": UMFPACK_DROPTOL, + "ir_steps": UMFPACK_IRSTEP, + "compiled_with_blas": UMFPACK_COMPILED_WITH_BLAS, + "sym_thresh": UMFPACK_STRATEGY_THRESH_SYM, + "nnzdiag_thresh": UMFPACK_STRATEGY_THRESH_NNZDIAG, +} + + +cdef list _CONTROL_INT_NAMES = [ + "print_level", + "blas3_block_size", + "fixQ", + "amd_dense", + "ir_steps", +] + + +cdef list _CONTROL_BOOL_NAMES = [ + "aggressive", + "singletons", + "compiled_with_blas", +] + + +cdef dict _CONTROL_STRATEGY_INDEX = { + "auto": UMFPACK_STRATEGY_AUTO, + "unsymmetric": UMFPACK_STRATEGY_UNSYMMETRIC, + "obsolete": UMFPACK_STRATEGY_OBSOLETE, + "symmetric": UMFPACK_STRATEGY_SYMMETRIC, +} + +cdef dict _CONTROL_STRATEGY_INVERSE_INDEX = { + v: k for k, v in _CONTROL_STRATEGY_INDEX.items() +} + + +cdef dict _CONTROL_SCALE_INDEX = { + "none": UMFPACK_SCALE_NONE, + "sum": UMFPACK_SCALE_SUM, + "max": UMFPACK_SCALE_MAX, +} + +cdef dict _CONTROL_SCALE_INVERSE_INDEX = { + v: k for k, v in _CONTROL_SCALE_INDEX.items() +} + + +cdef dict _CONTROL_ORDERING_INDEX = { + "cholmod": UMFPACK_ORDERING_CHOLMOD, + "amd": UMFPACK_ORDERING_AMD, + "given": UMFPACK_ORDERING_GIVEN, + "none": UMFPACK_ORDERING_NONE, + "metis": UMFPACK_ORDERING_METIS, + "best": UMFPACK_ORDERING_BEST, + "user": UMFPACK_ORDERING_USER, + "metis_guard": UMFPACK_ORDERING_METIS_GUARD, +} + +cdef dict _CONTROL_ORDERING_INVERSE_INDEX = { + v: k for k, v in _CONTROL_ORDERING_INDEX.items() +} + + +cdef dict _CONTROL_DISPATCH = { + "strategy": _CONTROL_STRATEGY_INVERSE_INDEX, + "row_scale": _CONTROL_SCALE_INVERSE_INDEX, + "ordering_method": _CONTROL_ORDERING_INVERSE_INDEX, +} + + +cdef dict _INFO_DISPATCH = { + "strategy_used": _CONTROL_STRATEGY_INVERSE_INDEX, + "was_scaled": _CONTROL_SCALE_INVERSE_INDEX, + "ordering_used": _CONTROL_ORDERING_INVERSE_INDEX, +} + + +# ----------------------------------------------------------------------------- +# Info and Control Classes +# ----------------------------------------------------------------------------- +cdef class UMFInfo: + """A data class to store UMFPACK info. + + Attributes + ---------- + status : int + Return status of the last UMFPACK call. + n_row : int + Number of rows in the input matrix. + n_col : int + Number of columns in the input matrix. + nz : int + Number of nonzeros in the input matrix. + size_of_unit : int + Size of a unit in bytes. + size_of_int : int + Size of an `int32_t` in bytes. + size_of_long : int + Size of an `int64_t` in bytes. + size_of_pointer : int + Size of a `void *` pointer in bytes. + size_of_entry : int + Size of an entry in bytes, real or complex. + ndense_row : int + Number of dense rows in the input matrix. + nempty_row : int + Number of empty rows in the input matrix. + ndense_col : int + Number of dense columns in the input matrix. + nempty_col : int + Number of empty columns in the input matrix. + symbolic_defrag : int + Number of memory compactions performed. + symbolic_peak_memory : int + Peak memory usage during symbolic factorization. + symbolic_size : int + Size of symbolic factorization, in Units. + symbolic_time : float + Time spent in symbolic factorization, in seconds. + symbolic_walltime : float + Wall-clock time spent in symbolic factorization, in seconds. + strategy_used : str + Strategy used in the factorization. One of: + ``{"auto", "unsymmetric", "symmetric"}``. + ordering_used : str + Ordering method used in the factorization. One of: + ``{"cholmod", "amd", "given", "none", "metis", "best", "user", "metis_guard"}`` + qfixed : bool + Whether the column permutation Q was fixed. + diag_preferred : bool + Whether diagonal pivoting was preferred. + pattern_symmetry : float + Symmetry of the nonzero pattern of the input matrix, excluding dense + rows and columns (aka :math:`S`). + nz_a_plus_at : int + Number of nonzeros in :math:`S + S^{\\top}`, excluding the diagonal. + nzdiag : int + Number of nonzeros on the diagonal of :math:`S`. + symmetric_lunz : int + Number of non-zeros in :math:`L + U`, if AMD ordering was used. + symmetric_flops : int + Number of floating-point operations for the factorization, if AMD + ordering was used. + symmetric_ndense : int + Number of dense rows and columns in :math:`S + S^{\\top}`. + symmetric_dmax : int + Maximum number of entries in any column of :math:`L`, for AMD. + col_singletons : int + Number of column singletons. + row_singletons : int + Number of row singletons. + n2 : int + Size of :math:`S`. + s_symmetric : int + 1 if :math:`S` is square and symmetrically permuted. + numeric_size_estimate : int + Estimated size of numeric factorization, in Units. + peak_memory_estimate : int + Estimated peak memory usage during numeric factorization. + flops_estimate : int + Estimated number of floating-point operations for the factorization. + lnz_estimate : int + Estimated number of nonzeros in :math:`L`. + unz_estimate : int + Estimated number of nonzeros in :math:`U`. + variable_init_estimate : int + Initial size of memory usage in numeric factorization. + variable_peak_estimate : int + Peak size of memory usage in numeric factorization. + variable_final_estimate : int + Final size of memory usage in numeric factorization. + max_front_size_estimate : int + Maximum frontal matrix size, estimated. + max_front_nrows_estimate : int + Maximum number of rows in any frontal matrix, estimated. + max_front_ncols_estimate : int + Maximum number of columns in any frontal matrix, estimated. + numeric_size : int + Size of numeric factorization, in Units. + peak_memory : int + Peak memory usage during symbolic and numeric factorization. + flops : int + Number of floating-point operations for the factorization. + lnz : int + Number of nonzeros in :math:`L`. + unz : int + Number of nonzeros in :math:`U`. + variable_init : int + Initial size of memory usage in numeric factorization. + variable_peak : int + Peak size of memory usage in numeric factorization. + variable_final : int + Final size of memory usage in numeric factorization. + max_front_size : int + Maximum frontal matrix size. + max_front_nrows : int + Maximum number of rows in any frontal matrix. + max_front_ncols : int + Maximum number of columns in any frontal matrix. + numeric_defrag : int + Number of memory compactions performed. + numeric_realloc : int + Number of memory reallocations performed. + numeric_costly_realloc : int + Number of costly memory reallocations performed. + compressed_pattern : int + Number of integers in LU pattern. + lu_entries : int + Number of real entries in :math:`L` and :math:`U`. + numeric_time : float + Time spent in numeric factorization, in seconds. + nz_udiag : int + Number of nonzeros on the diagonal of :math:`U`. + rcond : float + Estimate of the reciprocal of the condition number of :math:`A`. + was_scaled : str + Scaling method used. One of: ``{"none", "sum", "max"}``. + rsmin : float + `min(max(row))` or `min(sum(row))`, depending on the scaling method. + rsmax : float + `max(max(row))` or `max(sum(row))`, depending on the scaling method. + umin : float + Minimum absolute value of a diagonal entry of :math:`U`. + umax : float + Maximum absolute value of a diagonal entry of :math:`U`. + alloc_init_used : float + Initial memory allocation used, as a fraction of total numeric memory. + forced_updates : int + Number of forced updates during numeric factorization. + numeric_walltime : float + Wall-clock time spent in numeric factorization, in seconds. + noff_diag : int + Number of off-diagonal pivots. + all_lnz : int + Total number of entries in :math:`L`, if no dropped entries. + all_unz : int + Total number of entries in :math:`U`, if no dropped entries. + nzdropped : int + Number of dropped entries in :math:`L` and :math:`U`. + ir_taken : int + Number of iterative refinement steps taken. + ir_attempted : int + Number of iterative refinement steps attempted. + omega1 : int + Factor for sparse backdward error estimate. + omega2 : int + Factor for sparse backdward error estimate. + solve_flops : int + Number of floating-point operations for `solve`. + solve_time : float + Time spent in `solve`, in seconds. + solve_walltime : float + Wall-clock time spent in `solve`, in seconds. + + + .. versionadded:: 0.5.0 + """ + + cdef double data[UMFPACK_INFO] + + # NOTE no __cinit__ needed, as this object is only created within the + # UMFFactor object and initialized/filled by UMFPACK umfpack_symbolic. + + def __getattr__(self, name): + try: + value = self.data[_INFO_INDEX[name]] + except KeyError: + raise AttributeError( + f"{self.__class__.__name__} object has no attribute '{name}'" + ) + + mapper = _INFO_DISPATCH.get(name, None) + if mapper is not None: + value = mapper[value] + elif name in _INFO_INT_NAMES: + value = int(value) + elif name in _INFO_BOOL_NAMES: + value = bool(value) + + return value + + def __setattr__(self, name, value): + # Info values are read-only + raise AttributeError(f"cannot assign to '{name}'.") + + def __iter__(self): + cdef int idx + for key, idx in _INFO_INDEX.items(): + yield (key, getattr(self, key)) + + def __repr__(self): + params = ",\n ".join(f"{k}={repr(v)}" for k, v in self) + return f"{self.__class__.__name__}(\n {params}\n)" + + def __str__(self): + return self.__repr__() + + +cdef class UMFControl: + """The class used to manage UMFPACK control parameters. + + Attributes + ---------- + print_level : int + The verbosity level. Values vary depending on the + function called, but typically "0" means no printing, and higher values + mean more verbose printing. Default value is 1. + dense_row, dense_col : int + A row or column is considered to be dense if it has more than ``max(16, + dense_[row|col] * 16 * sqrt(n_[row|col])`` entries. Default 0.2. + blas3_block_size : int + The block size to use in Level-3 BLAS operations. Default value is 32. + strategy : str + The strategy to use in the factorization. Default value is ``auto``. + Possible values are: + + * ``auto``: choose the strategy automatically + * ``unsymmetric``: order the columns of :math:`A` with COLAMD + * ``symmetric``: Order the matrix :math:`A + A^{\\top}` with AMD + + ordering_method : str + The ordering method to use. Default value is ``amd``. Possible values + are: + + * ``cholmod``: use AMD/COLAMD, then METIS + * ``amd``: just use AMD or COLAMD + * ``given``: use the user-provided ordering + * ``none``: no ordering + * ``metis``: use METIS on :math:`A + A^{\\top}` or :math:`A^{\\top} A` + * ``best``: try AMD/COLAMD, METIS and NESDIS + * ``user``: use the user-provided function to compute the ordering + * ``metis_guard``: use METIS for symmetric strategy, try METIS for + unsymmetric and fall back to COLAMD if :math:`A` has many dense rows. + + fixQ : int + Default 0. Possible values: + + * -1: possibly modify :math:`Q` during numeric factorization. + * 0: automatic. Modify :math:`Q` only if strategy is unsymmetric. + * 1: do not modify :math:`Q` during numeric factorization. + + amd_dense : int + Rows/columns in :math:`A + A^{\top}` with more than ``max(16, + amd_dense * sqrt(n))`` entries (where ``n = n_row + = n_col``) are ignored in the AMD pre-ordering. Default 10. + aggressive : bool + If True, use aggressive absorption in AMD. Default True. + singletons : bool + If True, remove singletons prior to factorization. Default True. + pivot_tol : float + The relative pivot tolerance for partial pivoting with row + interchanges. The absolute value of the entry must be >= ``pivot_tol + *`` largest absolute value in that column. ``pivot_tol=1.0`` gives true + partial pivoting. If ``pivot_tol <= 0.0``, then any non-zero entry is + acceptable as a pivot. Default value is 0.1. + sym_pivot_tol : float + The relative pivot tolerance for symmetric strategy. Default 0.001. + row_scale : str or None + The row scaling to use. Default value is ``'sum'``. Possible values are: + + * None or ``'none'``: no row scaling + * ``'sum'``: divide each row by ``sum(abs(A[i,:]))`` + * ``'max'``: divide each row by ``max(abs(A[i,:]))`` + + alloc_init : float + Estimated space for the memory to allocate for numeric factorization. + Default 0.7. + front_alloc_init : float + Estimated space for the memory to allocate for frontal matrices. + Default 0.5. + droptol : float + Drop tolerance for small entries in :math:`L` and :math:`U`. Default + value is 0.0 (no dropping). + ir_steps : int + Number of iterative refinement steps to perform. Default value is 2. + compiles_with_blas : bool + True if UMFPACK was compiled with BLAS support. Read-only. + sym_thresh : float + Threshold for choosing symmetric strategy. Default 0.3. + nnzdiag_thresh : float + Threshold for choosing unsymmetric strategy based on the number of + diagonal entries. Default 0.9. + + + .. versionadded:: 0.5.0 + """ + + cdef double data[UMFPACK_CONTROL] + + def __cinit__(self, **kwargs): + # NOTE the 4 functions ([dz][il]_defaults) all set the same default + # values, so just pick one of them. + umfpack_di_defaults(self.data) + + # Update with user-provided values + for key, value in kwargs.items(): + try: + setattr(self, key, value) + except KeyError: + raise KeyError( + f"Invalid control parameter: {key}. " + f"Expected one of {list(_CONTROL_INDEX.keys())}" + ) + + def __getattr__(self, name): + try: + value = self.data[_CONTROL_INDEX[name]] + except KeyError: + raise AttributeError(f"UMFControl object has no attribute '{name}'") + + # Convert types where appropriate + mapper = _CONTROL_DISPATCH.get(name, None) + if mapper is not None: + value = mapper[value] + elif name in _CONTROL_INT_NAMES: + value = int(value) + elif name in _CONTROL_BOOL_NAMES: + value = bool(value) + + return value + + def __setattr__(self, name, value): + try: + idx = _CONTROL_INDEX[name] + except KeyError: + raise AttributeError( + f"{self.__class__.__name__} object has no attribute '{name}'" + ) + + # Validate values + if idx == UMFPACK_STRATEGY: + try: + value = _CONTROL_STRATEGY_INDEX[value] + except KeyError: + raise ValueError( + f"Invalid value for strategy: {value}. " + f"Expected one of {list(_CONTROL_STRATEGY_INDEX.keys())}" + ) + + if value == UMFPACK_STRATEGY_OBSOLETE: + raise ValueError("'obsolete' value is, well, obsolete.") + + elif idx == UMFPACK_ORDERING: + try: + value = _CONTROL_ORDERING_INDEX[value] + except KeyError: + raise ValueError( + f"Invalid value for ordering_method: {value}. " + f"Expected one of {list(_CONTROL_ORDERING_INDEX.keys())}" + ) + + elif idx == UMFPACK_SCALE: + try: + value = _CONTROL_SCALE_INDEX[value] + except KeyError: + raise ValueError( + f"Invalid value for row_scale: {value}. " + f"Expected one of {list(_CONTROL_SCALE_INDEX.keys())}" + ) + + elif idx == UMFPACK_COMPILED_WITH_BLAS: + raise AttributeError(f"'{name}' is read-only") + + # Set the value + self.data[idx] = value + + def __iter__(self): + cdef int idx + for key, idx in _CONTROL_INDEX.items(): + yield (key, getattr(self, key)) + + def __repr__(self): + params = ",\n ".join(f"{k}={repr(v)}" for k, v in self) + return f"{self.__class__.__name__}(\n {params}\n)" + + def __str__(self): + return self.__repr__() + + def report(self): + """Print a report of the control structure to stdout. + + This method provides more internal details from UMFPACK itself than the + string representation. + + .. note:: + + This method temporarily sets the print level to 2 (print all + information) and restores the previous value afterwards, so the + report will *always* show "print level: 2". Use + ``UMFControl.print_level`` or ``print(UMFControl)`` to see the + actual print level. + """ + cdef int old_pl = self.print_level + self.print_level = 2 # print all info + + # NOTE the 4 functions ([dz][il]_report_control) all print the same. + umfpack_di_report_control(self.data) + + # restore old print level + self.print_level = old_pl + + +# ----------------------------------------------------------------------------- +# UMFPACK Class Interface +# ----------------------------------------------------------------------------- +# TODO include all solver options? +cdef dict _TRANS_INDEX = { + "N": UMFPACK_A, # Ax = b + "T": UMFPACK_Aat, # A^T x = b + "H": UMFPACK_At, # A^H x = b +} + + +cdef class UMFFactor: + """The main object used for creating and using an LU factorization. + + The constructor computes the symbolic analysis of a sparse matrix :math:`A` + and determines a fill-reducing ordering such that: + + .. math:: + L U = P R A Q. + + The numeric factorization is not computed until :meth:`.factorize` is called. + + Attributes + ---------- + is_numeric : bool + Whether the numeric factorization has been computed. + lnz, unz : int + Number of nonzeros in :math:`L` and :math:`U`, respectively. + nnz : int + Total number of nonzeros in :math:`L` and :math:`U`. + n_row, n_col : int + Number of rows and columns in the input matrix. + nz_udiag : int + Number of nonzeros on the diagonal of :math:`U`. + dtype : numpy.dtype + The data type of the matrix entries (``float64`` or ``complex128``). + itype : numpy.dtype + The integer type used for indexing (``int32`` or ``int64``). + L : scipy.sparse.csr_array + The :math:`L` factor as a sparse CSR matrix. + U : scipy.sparse.csc_array + The :math:`U` factor as a sparse CSC matrix. + perm_r, perm_c : numpy.ndarray + The row and column permutation arrays, :math:`P` and :math:`Q`. + R : numpy.ndarray + The row scaling diagonal matrix as a 1D array. + info : :class:`UMFInfo` + An object containing information about the factorization. + control : :class:`UMFControl` + An object containing settings for the factorization. + + See Also + -------- + UMFControl, umf_factor, umf_solve + + Notes + ----- + This object is an interface to the SuiteSparse UMFPACK library [#umfpack_url]_. + + + .. versionadded:: 0.5.0 + + References + ---------- + .. [#umfpack_url] SuiteSparse UMFPACK + https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/UMFPACK + """ + + cdef: + void *_symbolic + void *_numeric + public UMFControl control + readonly UMFInfo info + bint _use_int32 + bint _is_real + size_t _M + size_t _N + size_t _N_inner # min(M, N) inner dimension of LU + readonly object itype + readonly object dtype + # Store A matrix for use in factorize and solve + cnp.ndarray _Ap + cnp.ndarray _Ai + cnp.ndarray _Ax + # cached "output" arrays, only extracted from _numeric upon request + object _L, _U, _P, _Q, _Rs + + def __init__(self, object A, object control=None): + """Compute the symbolic analysis. + + Parameters + ---------- + A : numpy.ndarray or sparse array + The input matrix. Any object that can be converted to + a :class:`~scipy.sparse.csc_array` is accepted. + control : :class:`UMFControl`, optional + An object containing settings for the factorization. Default values + will be used if not provided. + """ + A, _, _ = validate_csc_input(A) + + # Promote single to double precision + if not ( + np.issubdtype(A.dtype, np.float64) or np.issubdtype(A.dtype, np.complex128) + ): + if np.issubdtype(A.dtype, np.floating): + A = A.astype(np.promote_types(A.dtype, np.float64)) + elif np.issubdtype(A.dtype, np.complexfloating): + A = A.astype(np.promote_types(A.dtype, np.complex128)) + + # Cache the matrix data + self._Ap = A.indptr + self._Ai = A.indices + self._Ax = A.data + + # Initialize the control and info arrays + self.control = UMFControl() if control is None else control + self.info = UMFInfo() + + # Compute the symbolic analysis + self._init_symbolic(A.shape[0], A.shape[1], self._Ap, self._Ai, self._Ax) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _init_symbolic( + self, + int M, + int N, + index_t[::1] indptr, + index_t[::1] indices, + value_t[::1] data + ): + """Compute the symbolic factorization. + + Parameters + ---------- + M, N : int + Number of rows and columns of the matrix. + indptr : 1D array of index_t + The index pointer array of the CSC matrix. + indices : 1D array of index_t + The row indices array of the CSC matrix. + data : 1D array of value_t + The data array of the CSC matrix. + """ + cdef int status + + self._use_int32 = index_t is int32_t + self._is_real = value_t is double + + # Compute the symbolic factorization + # NOTE numpy complex arrays store real and imag parts interleaved, + # so we can just pass the pointer to the data as double* + if self._is_real: + if self._use_int32: + status = umfpack_di_symbolic( + M, + N, + &indptr[0], + &indices[0], + &data[0], + &self._symbolic, + self.control.data, + self.info.data + ) + else: + status = umfpack_dl_symbolic( + M, + N, + &indptr[0], + &indices[0], + &data[0], + &self._symbolic, + self.control.data, + self.info.data + ) + else: + if self._use_int32: + status = umfpack_zi_symbolic( + M, + N, + &indptr[0], + &indices[0], + &data[0], + NULL, + &self._symbolic, + self.control.data, + self.info.data + ) + else: + status = umfpack_zl_symbolic( + M, + N, + &indptr[0], + &indices[0], + &data[0], + NULL, + &self._symbolic, + self.control.data, + self.info.data + ) + + _handle_errors(status) + + # Store matrix shape (ensure non-negative before cast) + self._M = max(self.info.data[UMFPACK_NROW], 0) + self._N = max(self.info.data[UMFPACK_NCOL], 0) + self._N_inner = min(self._M, self._N) + + self.itype = np.dtype(np.int32 if self._use_int32 else np.int64) + self.dtype = np.dtype(np.float64 if self._is_real else np.complex128) + + def __dealloc__(self): + """Free UMFPACK symbolic and numeric objects.""" + if self._symbolic is not NULL: + if self._is_real: + if self._use_int32: + umfpack_di_free_symbolic(&self._symbolic) + else: + umfpack_dl_free_symbolic(&self._symbolic) + else: + if self._use_int32: + umfpack_zi_free_symbolic(&self._symbolic) + else: + umfpack_zl_free_symbolic(&self._symbolic) + + if self._numeric is not NULL: + if self._is_real: + if self._use_int32: + umfpack_di_free_numeric(&self._numeric) + else: + umfpack_dl_free_numeric(&self._numeric) + else: + if self._use_int32: + umfpack_zi_free_numeric(&self._numeric) + else: + umfpack_zl_free_numeric(&self._numeric) + + def __iter__(self): + for attr in ['L', 'U', 'perm_r', 'perm_c', 'rscale']: + yield getattr(self, attr) + + def __repr__(self): + cls_name = self.__class__.__name__ + factor_type = 'numeric' if self.is_numeric else 'symbolic' + L_shape = (self._M, self._N_inner) + U_shape = (self._N_inner, self._N) + return ( + f"<{cls_name} {factor_type} factor of dtype '{self.dtype}' " + f"with '{self.itype}' indices:\n" + f" L: {L_shape} with {self.lnz} stored elements\n" + f" U: {U_shape} with {self.unz} stored elements>" + ) + + def __str__(self): + return self.__repr__() + + # ------------------------------------------------------------------------- + # Properties + # ------------------------------------------------------------------------- + @property + def is_numeric(self): + return self._symbolic is not NULL and self._numeric is not NULL + + @property + def lnz(self): + val = self.info.data[UMFPACK_LNZ] + return int(val) if val >= 0 else None + + @property + def unz(self): + val = self.info.data[UMFPACK_UNZ] + return int(val) if val >= 0 else None + + @property + def nnz(self): + if self.lnz is None or self.unz is None: + return None + return int(self.lnz + self.unz) + + @property + def shape(self): + return (self._M, self._N) + + @property + def nz_udiag(self): + val = self.info.data[UMFPACK_UDIAG_NZ] + return int(val) if val >= 0 else None + + @property + def L(self): + if self._L is None: + self._get_numeric() + return self._L + + @property + def U(self): + if self._U is None: + self._get_numeric() + return self._U + + @property + def perm_r(self): + if self._P is None: + self._get_numeric() + return self._P + + @property + def perm_c(self): + if self._Q is None: + self._get_numeric() + return self._Q + + @property + def rscale(self): + if self._Rs is None: + self._get_numeric() + return self._Rs + + # ------------------------------------------------------------------------- + # Public Methods + # ------------------------------------------------------------------------- + def copy(self): + """Return a deep copy of the current UMFFactor object.""" + cdef UMFFactor umf = UMFFactor.__new__(UMFFactor) + + umf._use_int32 = self._use_int32 + umf._is_real = self._is_real + umf._M = self._M + umf._N = self._N + umf._N_inner = self._N_inner + umf.itype = self.itype + umf.dtype = self.dtype + + cdef int status + + if self._is_real: + if self._use_int32: + status = umfpack_di_copy_symbolic(&umf._symbolic, self._symbolic) + else: + status = umfpack_dl_copy_symbolic(&umf._symbolic, self._symbolic) + else: + if self._use_int32: + status = umfpack_zi_copy_symbolic(&umf._symbolic, self._symbolic) + else: + status = umfpack_zl_copy_symbolic(&umf._symbolic, self._symbolic) + + _handle_errors(status) + + if self._is_real: + if self._use_int32: + status = umfpack_di_copy_numeric(&umf._numeric, self._numeric) + else: + status = umfpack_dl_copy_numeric(&umf._numeric, self._numeric) + else: + if self._use_int32: + status = umfpack_zi_copy_numeric(&umf._numeric, self._numeric) + else: + status = umfpack_zl_copy_numeric(&umf._numeric, self._numeric) + + _handle_errors(status) + + umf.control = self.control + umf.info = self.info + + umf._Ap = None if self._Ap is None else self._Ap.copy() + umf._Ai = None if self._Ai is None else self._Ai.copy() + umf._Ax = None if self._Ax is None else self._Ax.copy() + + umf._L = None if self._L is None else self._L.copy() + umf._U = None if self._U is None else self._U.copy() + umf._P = None if self._P is None else self._P.copy() + umf._Q = None if self._Q is None else self._Q.copy() + umf._Rs = None if self._Rs is None else self._Rs.copy() + + return umf + + def factorize(self, object A=None): + """Compute the numeric factorization of a sparse matrix. + + Given the symbolic analysis performed in the constructor, + compute the numeric factorization of a sparse matrix :math:`A` + and determine a fill-reducing ordering such that: + + .. math:: + L U = P R A Q. + + If given, the matrix :math:`A` must have the same shape and nonzero + pattern as the one used to create this :class:`UMFFactor` object, but + need not have the same values. + + Parameters + ---------- + A : (M, N) numpy.ndarray or sparse array + The input matrix. Must have the same shape and nonzero pattern as + the matrix used to create this :class:`UMFFactor` object. If not + provided, the original matrix given to the constructor will be + used. + + Returns + ------- + :class:`UMFFactor` + The current object, for method chaining. + """ + assert self._symbolic is not NULL, ( + "Symbolic factorization not present. " + "Cannot perform numeric factorization." + ) + + if A is not None: + A, _, itype = validate_csc_input(A) + self._check_input_matrix(A, itype) + # Update cached matrix data + self._Ap = A.indptr + self._Ai = A.indices + self._Ax = A.data + + # TODO free any existing numeric factorization? + + # Clear cached factor objects + self._L = None + self._U = None + self._P = None + self._Q = None + self._Rs = None + + self._factorize(self._Ap, self._Ai, self._Ax) + + return self + + @cython.boundscheck(False) + @cython.wraparound(False) + def _factorize( + self, + index_t[::1] indptr, + index_t[::1] indices, + value_t[::1] data, + ): + """Compute the numeric factorization given the CSC arrays. + + Parameters + ---------- + indptr : contiguous 1D array of index_t + The index pointer array of the CSC matrix. + indices : contiguous 1D array of index_t + The row indices array of the CSC matrix. + data : contiguous 1D array of value_t + The data array of the CSC matrix. + """ + cdef int status + + # Free existing numeric factorization + if self._numeric is not NULL: + if self._is_real: + if self._use_int32: + umfpack_di_free_numeric(&self._numeric) + else: + umfpack_dl_free_numeric(&self._numeric) + else: + if self._use_int32: + umfpack_zi_free_numeric(&self._numeric) + else: + umfpack_zl_free_numeric(&self._numeric) + + # Compute the symbolic factorization + if self._is_real: + if self._use_int32: + status = umfpack_di_numeric( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + &self._numeric, + self.control.data, + self.info.data + ) + else: + status = umfpack_dl_numeric( + &indptr[0], + &indices[0], + &data[0], + self._symbolic, + &self._numeric, + self.control.data, + self.info.data + ) + else: + if self._use_int32: + status = umfpack_zi_numeric( + &indptr[0], + &indices[0], + &data[0], + NULL, + self._symbolic, + &self._numeric, + self.control.data, + self.info.data + ) + else: + status = umfpack_zl_numeric( + &indptr[0], + &indices[0], + &data[0], + NULL, + self._symbolic, + &self._numeric, + self.control.data, + self.info.data + ) + + _handle_errors(status) + + # TODO allow x as input? + def solve(self, object b, object A=None, *, object trans='N'): + """Solve a linear system using the LU factorization. + + This method solves one of the following linear systems: + + * :math:`A x = b` (if ``trans='N'``) + * :math:`A^{\\top} x = b` (if ``trans='T'`` and :math:`A` is real) + * :math:`A^{H} x = b` (if ``trans='H'`` and :math:`A` is complex) + + The matrix :math:`A` must have the same shape and nonzero pattern as + the one used to create this :class:`UMFFactor` object, but need not + have the same values. No check is performed to ensure that the + input matrix is compatible with the existing factorization. + + Parameters + ---------- + b : (N,) or (N, K) numpy.ndarray or sparse array + The right-hand side vector or martrix. + A : (N, N) numpy.ndarray or sparse array, optional + The input matrix. Must have the same shape and nonzero pattern as + the matrix used to create this :class:`UMFFactor` object. + trans : str, optional + The type of system to solve. Possible values are: + + * ``N``: solve :math:`A x = b` (default) + * ``T``: solve :math:`A^{\\top} x = b` + * ``H``: solve :math:`A^{H} x = b` + + .. note:: + + If :math:`A` is real, then ``T`` and ``H`` are equivalent. + + Returns + ------- + x : (N,) or (N, K) numpy.ndarray or sparse array + The solution vector or matrix. If ``b`` is a 1D array, then ``x`` is + returned as a 1D array. If ``b`` is a 2D array with ``K`` columns, + then ``x`` is returned as a 2D array with ``K`` columns. If ``b`` + is a sparse array, then ``x`` is also returned as a sparse array. + + Raises + ------ + :exc:`UMFPACKSingularMatrixWarning` + If the matrix is detected to be singular to working precision. + In that case, the solution will have infinite or NaN values, + but other entries may still be valid. + """ + if self._M != self._N: + raise ValueError( + "Matrix must be square to use the solve method. " + f" Got shape ({self._M=}, {self._N=})." + ) + + cdef int sys + try: + sys = _TRANS_INDEX[trans] + except KeyError: + raise ValueError( + f"Invalid value for trans: {trans}. " + f"Expected one of {list(_TRANS_INDEX.keys())}" + ) + + if not (isinstance(b, np.ndarray) or issparse(b)): + raise ValueError("b must be an ndarray or sparse matrix.") + + if A is not None: + A, _, itype = validate_csc_input(A, require_square=True) + self._check_input_matrix(A, itype) + # Update cached matrix data + self._Ap = A.indptr + self._Ai = A.indices + self._Ax = A.data + + if b.dtype != self.dtype: + raise ValueError( + f"LHS and RHS dtypes do not match. {self.dtype=} and {b.dtype=}" + ) + + if b.ndim not in (1, 2): + raise ValueError("b must be a 1D or 2D array.") + + cdef size_t N = self.info.data[UMFPACK_NROW] + cdef bint return_1D = b.ndim == 1 + + if b.shape[0] != N: + raise ValueError( + "Right-hand side b must have the same number of rows as A." + ) + + cdef bint return_sparse = issparse(b) + + if return_sparse: + b = b.toarray() + else: + b = np.asarray(b) + + if b.ndim == 1: + b = b.reshape((N, 1)) + + # TODO warn here? + # Prepare to solve the system + if self._numeric is NULL: + self.factorize(A) + + # Check the condition number + self._check_rcond() + + # Ensure columns are contiguous for multiple RHS + b = np.asfortranarray(b) + + # Allocate the output array + x = np.empty_like(b, order="F") + + self._solve(sys, b, self._Ap, self._Ai, self._Ax, x) + + if return_sparse: + x = csc_array(x, dtype=b.dtype) + x.indptr = x.indptr.astype(self.itype) + x.indices = x.indices.astype(self.itype) + + if return_1D: + x = x[:, 0] + + return x + + @cython.boundscheck(False) # for-loop guaranteed in-bounds + @cython.wraparound(False) + def _solve( + self, + int sys, + value_t[::1, :] b, + index_t[::1] indptr, + index_t[::1] indices, + value_t[::1] data, + value_t[::1, :] x + ): + """Solve multiple RHS systems. + + Parameters + ---------- + sys : int + The system type (UMFPACK_A, UMFPACK_Aat, UMFPACK_At). + b : 2D array of value_t, shape (N, K) + The right-hand side matrix. + indptr : contiguous 1D array of index_t + The index pointer array of the CSC matrix. + indices : contiguous 1D array of index_t + The row indices array of the CSC matrix. + data : contiguous 1D array of value_t + The data array of the CSC matrix. + x : 2D array of value_t, shape (N, K) + The output solution matrix. + """ + cdef: + Py_ssize_t k + Py_ssize_t K = b.shape[1] + double* data_ptr = &data[0] + double* x_ptr + double* b_ptr + + for k in range(K): + # NOTE numpy complex arrays store real and imag parts interleaved, + # so we can just pass the pointer to the data as double* + x_ptr = &x[0, k] + b_ptr = &b[0, k] + + # Solve the system + if self._is_real: + if self._use_int32: + status = umfpack_di_solve( + sys, + &indptr[0], + &indices[0], + data_ptr, + x_ptr, + b_ptr, + self._numeric, + self.control.data, + self.info.data + ) + else: + status = umfpack_dl_solve( + sys, + &indptr[0], + &indices[0], + data_ptr, + x_ptr, + b_ptr, + self._numeric, + self.control.data, + self.info.data + ) + else: + if self._use_int32: + status = umfpack_zi_solve( + sys, + &indptr[0], + &indices[0], + data_ptr, + NULL, + x_ptr, + NULL, + b_ptr, + NULL, + self._numeric, + self.control.data, + self.info.data + ) + else: + status = umfpack_zl_solve( + sys, + &indptr[0], + &indices[0], + data_ptr, + NULL, + x_ptr, + NULL, + b_ptr, + NULL, + self._numeric, + self.control.data, + self.info.data + ) + + _handle_errors(status) + + def slogdet(self): + """Return the determinant of the matrix as (sign, logabsdet). + + See Also + -------- + numpy.linalg.slogdet + """ + if not self.is_numeric: + raise ValueError( + "Numeric factorization not present. " + "Cannot compute determinant." + ) + + Mx = np.empty(1, dtype=self.dtype) + Ex = np.empty(1, dtype=np.float64) + + self._slogdet(Mx, Ex) + + # Compute the result + m = Mx[0] + e = Ex[0] + sign = np.sign(m) + # log(|det(A)|) = log(|m| * 10**e) = log(|m|) + e * log(10) + logabsdet = np.log(abs(m)) + e * np.log(10.0) + + return (sign, logabsdet) + + @cython.boundscheck(False) + @cython.wraparound(False) + def _slogdet(self, value_t[::1] Mx, double[::1] Ex): + """Compute the determinant of the matrix. + + Parameters + ---------- + Mx : 1D array of value_t, shape (1,) + The mantissa of the determinant. + Ex : 1D array of double, shape (1,) + The exponent of the determinant. + """ + cdef int status + cdef double* mx_ptr = &Mx[0] + cdef double* ex_ptr = &Ex[0] + + if self._is_real: + if self._use_int32: + status = umfpack_di_get_determinant( + mx_ptr, ex_ptr, self._numeric, self.info.data + ) + else: + status = umfpack_dl_get_determinant( + mx_ptr, ex_ptr, self._numeric, self.info.data + ) + else: + if self._use_int32: + status = umfpack_zi_get_determinant( + mx_ptr, NULL, ex_ptr, self._numeric, self.info.data + ) + else: + status = umfpack_zl_get_determinant( + mx_ptr, NULL, ex_ptr, self._numeric, self.info.data + ) + + _handle_errors(status) + + # ------------------------------------------------------------------------- + # Reporting + # ------------------------------------------------------------------------- + def report_info(self, object print_level=2): + """Print a report of the UMFInfo structure. + + This method provides more internal details from UMFPACK itself than the + string representation. + + Parameters + ---------- + print_level : int, optional + The verbosity level. Default value is 2. + + Accepted values are: + + * None: use current print level + * <= 0: no output + * 1: error messages only + * >= 2: error messages and print all of UMFInfo + + """ + cdef int pl + if print_level is None: + pl = self.control.print_level + else: + pl = print_level + + cdef int old_pl = self.control.print_level + self.control.print_level = pl + + umfpack_di_report_info(self.control.data, self.info.data) + + # restore old print level + self.control.print_level = old_pl + + def report_control(self): + self.control.report() + + def report_symbolic(self, object print_level=4): + cdef int pl + if print_level is None: + pl = self.control.print_level + else: + pl = print_level + + cdef int old_pl = self.control.print_level + self.control.print_level = pl + + if self._is_real: + if self._use_int32: + umfpack_di_report_symbolic(self._symbolic, self.control.data) + else: + umfpack_dl_report_symbolic(self._symbolic, self.control.data) + else: + if self._use_int32: + umfpack_zi_report_symbolic(self._symbolic, self.control.data) + else: + umfpack_zl_report_symbolic(self._symbolic, self.control.data) + + # restore old print level + self.control.print_level = old_pl + + def report_numeric(self, object print_level=4): + cdef int pl + if print_level is None: + pl = self.control.print_level + else: + pl = print_level + + cdef int old_pl = self.control.print_level + self.control.print_level = pl + + if self._is_real: + if self._use_int32: + umfpack_di_report_numeric(self._numeric, self.control.data) + else: + umfpack_dl_report_numeric(self._numeric, self.control.data) + else: + if self._use_int32: + umfpack_zi_report_numeric(self._numeric, self.control.data) + else: + umfpack_zl_report_numeric(self._numeric, self.control.data) + + # restore old print level + self.control.print_level = old_pl + + # ------------------------------------------------------------------------- + # Private Methods + # ------------------------------------------------------------------------- + def _check_input_matrix(self, object A, object itype): + """Check that the input matrix matches the existing factorization.""" + if A.shape != self.shape: + raise ValueError( + "The shape of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected {self.shape}, got {A.shape}." + ) + + if itype != self.itype: + raise ValueError( + "The integer size of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected '{self.itype}', got '{itype}'." + ) + + if A.dtype != self.dtype: + raise ValueError( + "The data type of the input matrix does not match " + "the one used for symbolic factorization. " + f"Expected '{self.dtype}', got '{A.dtype}'." + ) + + cdef int _check_rcond(self) except -1: + """Check the condition number.""" + cdef double rcond = self.info.data[UMFPACK_RCOND] + cdef double eps = np.finfo(np.float64).eps + + if rcond == 0: + raise UMFPACKError("Matrix is indefinite or singular to working precision.") + elif rcond < eps: + warnings.warn( + "Matrix is nearly singular." + f" Results may be inaccurate (rcond={rcond:.2e}).", + UMFPACKSingularMatrixWarning + ) + + cdef void _get_numeric(self) except *: + """Extract the numeric factorization data from UMFPACK.""" + if self._numeric is NULL: + raise UMFPACKError( + "Numeric factorization not present. " + "Run `UMFFactor.factorize(A)` first." + ) + + # Create output arrays + Lp = np.empty(self._M + 1, dtype=self.itype) + Lj = np.empty(self.lnz, dtype=self.itype) + Lx = np.empty(self.lnz, dtype=self.dtype) + + Up = np.empty(self._N + 1, dtype=self.itype) + Ui = np.empty(self.unz, dtype=self.itype) + Ux = np.empty(self.unz, dtype=self.dtype) + + self._P = np.empty(self._M, dtype=self.itype) + self._Q = np.empty(self._N, dtype=self.itype) + self._Rs = np.empty(self._M, dtype=np.float64) # always real + + self._dispatch_get_numeric( + Lp, Lj, Lx, + Up, Ui, Ux, + self._P, + self._Q, + self._Rs + ) + + self._L = csr_array((Lx, Lj, Lp), shape=(self._M, self._N_inner)) + self._U = csc_array((Ux, Ui, Up), shape=(self._N_inner, self._N)) + + + @cython.boundscheck(False) + @cython.wraparound(False) + def _dispatch_get_numeric( + self, + index_t[::1] Lp, index_t[::1] Lj, value_t[::1] Lx, + index_t[::1] Up, index_t[::1] Ui, value_t[::1] Ux, + index_t[::1] P, + index_t[::1] Q, + double[::1] Rs, + ): + """Call the appropriate UMFPACK get_numeric function. + + Parameters + ---------- + Lp, Lj, Lx : arrays for the L factor + The output arrays for the L factor in CSC format. + Up, Ui, Ux : arrays for the U factor + The output arrays for the U factor in CSC format. + P : array of index_t + The output row permutation array. + Q : array of index_t + The output column permutation array. + Rs : array of double + The output row scaling factors. + """ + cdef int status + cdef bint do_recip + + # Extract the numeric factorization + if self._is_real: + if self._use_int32: + status = umfpack_di_get_numeric( + &Lp[0], + &Lj[0], + &Lx[0], + &Up[0], + &Ui[0], + &Ux[0], + &P[0], + &Q[0], + NULL, # Dx + &do_recip, + &Rs[0], + self._numeric + ) + else: + status = umfpack_dl_get_numeric( + &Lp[0], + &Lj[0], + &Lx[0], + &Up[0], + &Ui[0], + &Ux[0], + &P[0], + &Q[0], + NULL, # Dx + &do_recip, + &Rs[0], + self._numeric + ) + else: + if self._use_int32: + status = umfpack_zi_get_numeric( + &Lp[0], + &Lj[0], + &Lx[0], + NULL, # Lz + &Up[0], + &Ui[0], + &Ux[0], + NULL, # Uz + &P[0], + &Q[0], + NULL, # Dx + NULL, # Dz + &do_recip, + &Rs[0], + self._numeric + ) + else: + status = umfpack_zl_get_numeric( + &Lp[0], + &Lj[0], + &Lx[0], + NULL, # Lz + &Up[0], + &Ui[0], + &Ux[0], + NULL, # Uz + &P[0], + &Q[0], + NULL, # Dx + NULL, # Dz + &do_recip, + &Rs[0], + self._numeric + ) + + _handle_errors(status) + + # From umfpack.h: + # If do_recip is TRUE (one), then the scale factors Rs [i] are to be used + # by multiplying row i by Rs [i]. Otherwise, the entries in row i are to + # be divided by Rs [i]. + # + # Always return R s.t. (R[:, np.newaxis] * A) scales the rows. + if not do_recip: + np.reciprocal(self._Rs, out=self._Rs) + + +# Set docstrings +_REPORT_DOC = """Print a report of the {kind} factorization. + +This method provides more internal details from UMFPACK itself than the +string representation. + +Parameters +---------- +print_level : int, optional + The verbosity level. Default value is 4. + + Accepted values are: + + * None: use current print level + * <= 2: no printing + * 3: fully check input, and print a short summary of its status + * 4: as 3, but print first few entries of the input + * 5: as 3, but print all of the input + +""" + +UMFFactor.report_symbolic.__doc__ = _REPORT_DOC.format(kind="symbolic") +UMFFactor.report_numeric.__doc__ = _REPORT_DOC.format(kind="numeric") +UMFFactor.report_control.__doc__ = UMFControl.report.__doc__ + +# ----------------------------------------------------------------------------- +# Convenience Functions +# ----------------------------------------------------------------------------- +def umf_factor(object A, *, object control=None, **kwargs): + """Compute the LU factorization of a sparse matrix using UMFPACK. + + This is a convenience function that creates a :class:`UMFFactor` object, + computes the numeric factorization, and returns the resulting object. + + Parameters + ---------- + A : (M, N) numpy.ndarray or sparse array + The input matrix to factorize. + control : :class:`UMFControl`, optional + The control parameters to use for the factorization. If not provided, + default parameters are used. + kwargs : keyword arguments, optional + Additional keyword arguments to pass to :class:`UMFControl` if + ``control`` is not provided. + + Returns + ------- + :class:`UMFFactor` + The LU factorization of the input matrix. + + Raises + ------ + :exc:`UMFPACKSingularMatrixWarning` + If the matrix is exactly singular. + + See Also + -------- + UMFFactor, UMFControl, umf_solve + + + .. versionadded:: 0.5.0 + """ + if control is None: + control = UMFControl(**kwargs) + return UMFFactor(A, control).factorize() + + +def umf_solve(object A, object b, *, object trans='N', object control=None, **kwargs): + """Solve a linear system using UMFPACK. + + This is a convenience function that creates a :class:`UMFFactor` object, + computes the numeric factorization, and solves the linear system. + + Parameters + ---------- + A : (N, N) numpy.ndarray or sparse array + The input matrix. + b : (N,) or (N, K) numpy.ndarray or sparse array + The right-hand side vector or matrix. + trans : str, optional + The type of system to solve. Possible values are: + + * ``N``: solve :math:`A x = b` (default) + * ``T``: solve :math:`A^{\\top} x = b` + * ``H``: solve :math:`A^{H} x = b` + + .. note:: + + If :math:`A` is real, then ``T`` and ``H`` are equivalent. + + control : :class:`UMFControl`, optional + The control parameters to use for the factorization. If not provided, + default parameters are used. + kwargs : keyword arguments, optional + Additional keyword arguments to pass to :class:`UMFControl` if + ``control`` is not provided. + + Returns + ------- + x : (N,) or (N, K) numpy.ndarray or sparse array + The solution vector or matrix of the same type and shape as the input + right-hand side ``b``. + + Raises + ------ + :exc:`UMFPACKSingularMatrixWarning` + If the matrix is detected to be singular to working precision. + In that case, the solution will have infinite or NaN values, + but other entries may still be valid. + + See Also + -------- + UMFFactor, UMFControl, umf_factor + + + .. versionadded:: 0.5.0 + """ + if control is None: + control = UMFControl(**kwargs) + + # factorize() and solve() will each warn for a singular matrix, + # so we catch the warnings from factorize() and re-raise only once. + with warnings.catch_warnings(record=True) as ws: + x = UMFFactor(A, control).factorize().solve(b, trans=trans) + + # Raise only the latest singular matrix warning from solve + if ws: + w = ws[-1] + warnings.warn(w.message, w.category) + + return x + + +# ============================================================================= +# ============================================================================= diff --git a/src/sksparse/utils.py b/src/sksparse/utils.py new file mode 100644 index 00000000..8d1b1d5f --- /dev/null +++ b/src/sksparse/utils.py @@ -0,0 +1,97 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: utils.py +# Created: 2025-08-12 21:15 +# ============================================================================= + +"""Utility functions for scikit-sparse.""" + +import warnings + +import numpy as np +from scipy.sparse import SparseEfficiencyWarning, csc_array, issparse + + +def validate_csc_input(A, require_square=False): + """Validate and convert input matrix to CSC format. + + Parameters + ---------- + A : (M, N) array_like + Input matrix to be validated and converted to CSC format, if possible. + If `A` is already a `csc_array`, and not in canonical format, it will + be converted to canonical format in-place (a copy is not made). + require_square : bool, optional + If True, the input matrix must be square (M == N). Default is False. + + Returns + ------- + A : (M, N) csc_array + The input matrix converted to canonical CSC format. + use_int32 : bool + Indicates whether the index arrays use int32 (True) or int64 (False). + out_itype : dtype + The data type of the output matrix indices, which is determined based + on the input matrix's index data type. + + Raises + ------ + SparseEfficiencyWarning + If the input matrix is not in CSC format and is converted to CSC. + ValueError + If the input matrix is not 2D, or cannot be converted to CSC format, or + if it is not square when `require_square` is True. + + .. versionadded:: 0.5.0 + """ + # Convert dense to sparse CSC + if not issparse(A): + A = np.asarray(A) + + if A.ndim != 2: + raise ValueError("Input must be 2D.") + + M, N = A.shape + + if require_square and M != N: + raise ValueError("Input must be square.") + + try: + if not isinstance(A, csc_array): + warnings.warn( + f"Input matrix ({type(A)}) not in CSC array format. Converting to CSC.", + SparseEfficiencyWarning, + stacklevel=3, + ) + A = csc_array(A) + except ValueError: + raise ValueError("Input must be convertible to CSC format.") + + # A copy will reset the flags, and avoid modifying the input. Generally, + # users would not expect the input matrix to be modified. + # A = A.copy() + + # Coerce bool or int data to float + if np.issubdtype(A.dtype, np.bool_) or np.issubdtype(A.dtype, np.integer): + dtype = np.result_type(A.dtype, np.float32) + A = A.astype(dtype) + + # NOTE as of scipy 1.16.2, A.has_sorted_indices and A.has_canonical_format + # are not always set correctly! + # Manually set the flags to False to force fixing the format. + A.has_sorted_indices = False + A.has_canonical_format = False + A.sum_duplicates() # sort indices and sum duplicates + + assert A.has_sorted_indices + assert A.has_canonical_format + + # Choose index width: int32 or int64 + use_int32 = A.indptr.dtype == np.int32 and A.indices.dtype == np.int32 + out_itype = np.dtype(np.int32 if use_int32 else np.int64) + + return A, use_int32, out_itype diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..0b900d0e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: conftest.py +# Created: 2025-09-18 16:27 +# ============================================================================= + +"""Common test fixtures for the scikit-sparse project unit tests.""" + +import numpy as np +import pytest +from scipy import sparse + + +# See: Davis, Timothy A. (2006). Direct Methods for Sparse Linear Systems, +# pp 708 (Equation 2.1). +@pytest.fixture +def davis_example_chol(): + """Return a small example matrix from Davis (2006).""" + N = 11 + rows = np.array( + [5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10], dtype=np.int32 + ) + cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9], dtype=np.int32) + # vals = np.ones(len(rows), dtype=np.float64) + rng = np.random.default_rng(565656) + vals = rng.random(len(rows), dtype=np.float64) + L = sparse.coo_array((vals, (rows, cols)), shape=(N, N)) + A = L + L.T # make it symmetric + A.setdiag(N) # make it strongly positive definite + return A.tocsc() + + +# See: Davis, Timothy A. (2006). Direct Methods for Sparse Linear Systems, +# p 74 (Figure 5.1) +@pytest.fixture +def davis_example_qr(): + """Return a small example matrix from Davis (2006).""" + N = 8 + rows = np.array( + [0, 1, 2, 3, 4, 5, 6, 3, 6, 1, 6, 0, 2, 5, 7, 4, 7, 0, 1, 3, 7, 5, 6], + dtype=np.int32, + ) + cols = np.array( + [0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7], + dtype=np.int32, + ) + # rng = np.random.default_rng(565656) + # vals = rng.random(len(rows), dtype=np.float64) + vals = np.ones(len(rows), dtype=np.float64) + vals[:7] = np.arange(1, 8, dtype=np.float64) # make diagonal entries non-unit + A = sparse.csc_array((vals, (rows, cols)), shape=(N, N)) + return A diff --git a/tests/data/can_24 b/tests/data/can_24 new file mode 100644 index 00000000..38158a15 --- /dev/null +++ b/tests/data/can_24 @@ -0,0 +1,160 @@ + 1 1 1 + 6 1 1 + 7 1 1 +13 1 1 +14 1 1 +18 1 1 +19 1 1 +20 1 1 +22 1 1 + 2 2 1 + 9 2 1 +10 2 1 +14 2 1 +15 2 1 +18 2 1 + 3 3 1 + 7 3 1 +12 3 1 +21 3 1 +22 3 1 +23 3 1 + 4 4 1 + 8 4 1 +11 4 1 +16 4 1 +19 4 1 +20 4 1 + 5 5 1 + 8 5 1 +10 5 1 +15 5 1 +16 5 1 +17 5 1 + 1 6 1 + 6 6 1 + 7 6 1 +13 6 1 +14 6 1 +18 6 1 + 1 7 1 + 3 7 1 + 6 7 1 + 7 7 1 +12 7 1 +13 7 1 +20 7 1 +22 7 1 +24 7 1 + 4 8 1 + 5 8 1 + 8 8 1 +10 8 1 +15 8 1 +16 8 1 +17 8 1 +18 8 1 +19 8 1 + 2 9 1 + 9 9 1 +10 9 1 +15 9 1 + 2 10 1 + 5 10 1 + 8 10 1 + 9 10 1 +10 10 1 +14 10 1 +15 10 1 +18 10 1 +19 10 1 + 4 11 1 +11 11 1 +19 11 1 +20 11 1 +21 11 1 +22 11 1 + 3 12 1 + 7 12 1 +12 12 1 +13 12 1 +22 12 1 +24 12 1 + 1 13 1 + 6 13 1 + 7 13 1 +12 13 1 +13 13 1 +24 13 1 + 1 14 1 + 2 14 1 + 6 14 1 +10 14 1 +14 14 1 +18 14 1 + 2 15 1 + 5 15 1 + 8 15 1 + 9 15 1 +10 15 1 +15 15 1 + 4 16 1 + 5 16 1 + 8 16 1 +16 16 1 +17 16 1 +19 16 1 + 5 17 1 + 8 17 1 +16 17 1 +17 17 1 + 1 18 1 + 2 18 1 + 6 18 1 + 8 18 1 +10 18 1 +14 18 1 +18 18 1 +19 18 1 +20 18 1 + 1 19 1 + 4 19 1 + 8 19 1 +10 19 1 +11 19 1 +16 19 1 +18 19 1 +19 19 1 +20 19 1 + 1 20 1 + 4 20 1 + 7 20 1 +11 20 1 +18 20 1 +19 20 1 +20 20 1 +21 20 1 +22 20 1 + 3 21 1 +11 21 1 +20 21 1 +21 21 1 +22 21 1 +23 21 1 + 1 22 1 + 3 22 1 + 7 22 1 +11 22 1 +12 22 1 +20 22 1 +21 22 1 +22 22 1 +23 22 1 + 3 23 1 +21 23 1 +22 23 1 +23 23 1 + 7 24 1 +12 24 1 +13 24 1 +24 24 1 diff --git a/sksparse/test_data/illc1033.mtx.gz b/tests/data/illc1033.mtx.gz similarity index 100% rename from sksparse/test_data/illc1033.mtx.gz rename to tests/data/illc1033.mtx.gz diff --git a/sksparse/test_data/illc1033_rhs1.mtx.gz b/tests/data/illc1033_rhs1.mtx.gz similarity index 100% rename from sksparse/test_data/illc1033_rhs1.mtx.gz rename to tests/data/illc1033_rhs1.mtx.gz diff --git a/sksparse/test_data/illc1850.mtx.gz b/tests/data/illc1850.mtx.gz similarity index 100% rename from sksparse/test_data/illc1850.mtx.gz rename to tests/data/illc1850.mtx.gz diff --git a/sksparse/test_data/illc1850_rhs1.mtx.gz b/tests/data/illc1850_rhs1.mtx.gz similarity index 100% rename from sksparse/test_data/illc1850_rhs1.mtx.gz rename to tests/data/illc1850_rhs1.mtx.gz diff --git a/sksparse/test_data/well1033.mtx.gz b/tests/data/well1033.mtx.gz similarity index 100% rename from sksparse/test_data/well1033.mtx.gz rename to tests/data/well1033.mtx.gz diff --git a/sksparse/test_data/well1033_rhs1.mtx.gz b/tests/data/well1033_rhs1.mtx.gz similarity index 100% rename from sksparse/test_data/well1033_rhs1.mtx.gz rename to tests/data/well1033_rhs1.mtx.gz diff --git a/sksparse/test_data/well1850.mtx.gz b/tests/data/well1850.mtx.gz similarity index 100% rename from sksparse/test_data/well1850.mtx.gz rename to tests/data/well1850.mtx.gz diff --git a/sksparse/test_data/well1850_rhs1.mtx.gz b/tests/data/well1850_rhs1.mtx.gz similarity index 100% rename from sksparse/test_data/well1850_rhs1.mtx.gz rename to tests/data/well1850_rhs1.mtx.gz diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..e04f751d --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,161 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: helpers.py +# Created: 2025-07-31 11:42 +# ============================================================================= + +"""Helper functions for the scikit-sparse project unit tests.""" + +import operator + +import numpy as np +import pytest +from scipy import sparse + + +def is_valid_permutation(p, N=None): + """Check if a vector is a valid permutation.""" + if N is None: + N = len(p) + return np.array_equal(np.sort(p), np.arange(N)) + + +def _get_dims(op_str, N_max, rng): + """Compute random dimensions M, N with given relationship. + + Parameters + ---------- + op_str : str + A string representing the relationship between M and N. + N_max : int + Maximum size for M and N. + rng : np.random.Generator + Random number generator. + """ + if op_str == "==": + M = N = rng.integers(1, N_max, endpoint=True) + elif op_str in [">", "<"]: + if N_max < 2: + raise ValueError( + "'N_max' must be at least 2 for over/underdetermined shapes." + ) + dim_small = rng.integers(1, N_max - 1, endpoint=True) + dim_large = rng.integers(dim_small + 1, N_max, endpoint=True) + + if op_str == ">": + M, N = dim_large, dim_small + else: # op == "<" + M, N = dim_small, dim_large + + else: # op in ["any", ">=", "<=", "!="] + M, N = rng.integers(1, N_max, size=2, endpoint=True) + + return M, N + + +def generate_random_matrices( + seed=565656, + N_trials=100, + N_max=10, + shape_kind="square", + d_scale=1, + pos_def_only=False, + dtype=float, +): + """Generate a list of random sparse matrices of maximum size N x N. + + Parameters + ---------- + seed : int + The random seed for reproducibility. + N_trials : int + Number of random matrices to generate. + N_max : int + Maximum size of the matrix (M, N) will be at most ``N_max`` x ``N_max``. + shape_kind : str, optional + The shape of the generated matrices. Default is 'square'. Options are: + + * ``any``: M and N are chosen independently. + * ``square`` or ``M == N`` + * ``overdetermined`` or ``M > N`` + * ``underdetermined`` or ``M < N`` + * Combinations ``M >= N``, ``M <= N`` and ``M != N`` are also accepted. + + d_scale : float + Scale factor for the density of the sparse matrix. The density will + be a random value between 0 and ``d_scale``. + pos_def_only : bool + If True, generate only positive definite matrices. This requires + that the matrix is square and symmetric. + + Returns + ------- + generator + A generator yielding pytest parameters for random sparse matrices. + """ + rng = np.random.default_rng(seed) + + _valid_op_strs = { + "any": "any", + "square": "==", + "M == N": "==", + "overdetermined": ">", + "M > N": ">", + "underdetermined": "<", + "M < N": "<", + "M >= N": ">=", + "M <= N": "<=", + "M != N": "!=", + } + + _ops = { + "==": operator.eq, + ">": operator.gt, + "<": operator.lt, + ">=": operator.ge, + "<=": operator.le, + "!=": operator.ne, + } + + for trial in range(N_trials): + try: + op_str = _valid_op_strs[shape_kind] + except KeyError as e: + raise ValueError(f"Invalid shape_kind: {shape_kind}") from e + + if pos_def_only and op_str != "==": + raise ValueError( + "Positive definite matrices must be square. " + "Set shape_kind to 'square' or 'M == N'." + ) + + if op_str == "any": + M, N = _get_dims(op_str, N_max, rng) + else: + op = _ops[op_str] + # Keep generating until the condition is met + MAX_TRIES = 10 + for _ in range(MAX_TRIES): + M, N = _get_dims(op_str, N_max, rng) + if op(M, N): + break + + d = d_scale * rng.random() # density + + A = sparse.random_array((M, N), density=d, format="csc", dtype=dtype, rng=rng) + + if pos_def_only: + # Ensure the matrix is positive definite + A = A.T.conj() @ A + A = 0.5 * (A + A.T.conj()) # make it strictly Hermitian + # Add a small value to the diagonal to ensure positive definiteness + A += sparse.diags_array(np.full(N, 1e-6, dtype=dtype)) + A = A.tocsc() + + yield pytest.param( + A, id=f"random_{trial:02d}::{A.shape}::{A.nnz}::{dtype.__name__}" + ) diff --git a/tests/test_amd.py b/tests/test_amd.py new file mode 100644 index 00000000..b716ea79 --- /dev/null +++ b/tests/test_amd.py @@ -0,0 +1,161 @@ +# Test cases for the sksparse.amd module. +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_amd.py +# Created: 2025-07-28 13:34 +# ============================================================================= + +"""Test cases for the sksparse.amd module.""" + +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.amd import AMDInfo, amd, amd_default_control + +from .helpers import generate_random_matrices, is_valid_permutation + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + assert_array_equal(amd(empty_A), np.array([], dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_zero_input(itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + assert_array_equal(amd(zero_A), np.arange(N, dtype=itype), strict=True) + + +def test_singleton_matrix(): + singleton_A = sparse.csc_array([[1]]) + assert_array_equal(amd(singleton_A), np.array([0], dtype=np.int32), strict=True) + + +@pytest.mark.parametrize( + "A", + list( + generate_random_matrices( + N_trials=100, N_max=200, d_scale=0.05, square_only=True + ) + ), +) +class TestRandomSquareMatrices: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_itype(self, A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + p = amd(A) + assert p.dtype == itype + assert p.shape == (A.shape[0],) + assert is_valid_permutation(p) + + @pytest.mark.parametrize("aggressive", [True, False]) + def test_aggressive(self, A, aggressive): + p = amd(A, aggressive=aggressive) + assert is_valid_permutation(p) + + +DENSE_THRESHOLDS = [None, 5, 2] + + +@pytest.mark.parametrize("dense_thresh", DENSE_THRESHOLDS) +def test_amd_with_dense_rows(dense_thresh): + N = 1000 + rng = np.random.default_rng(56) + A = sparse.random_array((N, N), density=0.001, format="lil", rng=rng) + + # Create a known number of dense rows above the threshold + # thresh is actually dense_thresh * sqrt(N) == dense_thresh * 10 + # max(A[i] for i in range(N)) is ~ 5 for N = 1000, density = 0.001 + AMD_DEFAULT_DENSE = 10 + thresh = int( + (dense_thresh if dense_thresh is not None else AMD_DEFAULT_DENSE) * np.sqrt(N) + ) + + N_dense_rows = 10 # arbitrary choice for number of dense rows + N_elems = min(2 * thresh, N) # arbitrary choice to ensure enough elements + + dense_row_idx = rng.choice(N, size=N_dense_rows, replace=False) + col_idx = rng.choice(N, size=N_elems, replace=False) + for i in dense_row_idx: + # Ensure the row is dense enough + A[i, col_idx] = rng.random(size=len(col_idx)) + + A = A + A.T + A = A.tocsc() + p = amd(A, dense_thresh=dense_thresh) + + assert is_valid_permutation(p) + + # Expect dense row at the end of the permutation, but maybe not in order + assert_array_equal(np.sort(p[-N_dense_rows:]), np.sort(dense_row_idx)) + + +def test_info_can_24(): + # The can_24 matrix is used in the SuiteSparse AMD MATLAB/amd_demo.m file. + expect_info = AMDInfo.from_array( + np.array([ + 0, # status + 24, # N + 160, # nz + 1, # symmetry + 24, # nzdiag + 136, # nz_A_plus_AT + 0, # Ndense + 3032, # memory + 0, # Ncmpa + 97, # Lnz + 97, # Ndiv + 275, # Nmultsubs_LDL + 453, # Nmultsubs_LU + 8, # dmax + ] + ) + ) + + # Load the can_24 matrix from a file + can_24_path = Path("tests") / "data" / "can_24" + with can_24_path.open() as fp: + can_24 = np.genfromtxt(fp, dtype=int) + + A = sparse.csc_array((can_24[:, 2], (can_24[:, 0] - 1, can_24[:, 1] - 1))) + p, info = amd(A, return_info=True) + + assert is_valid_permutation(p) + assert info == expect_info + + +def test_amd_default_control(): + """Test that AMD uses the default control settings.""" + # The default control settings are (from amd.h): + # - AMD_DEFAULT_DENSE -> dense_thresh: 10.0 + # - AMD_DEFAULT_AGGRESSIVE -> aggressive: True + expect_control = { + "dense_thresh": 10.0, + "aggressive": True, + } + control = amd_default_control() + assert control == expect_control + + A = sparse.csc_array([[1, 2], [3, 4]]) + p = amd(A, **control) + assert is_valid_permutation(p) + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_btf/__init__.py b/tests/test_btf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_btf/test_btf.py b/tests/test_btf/test_btf.py new file mode 100644 index 00000000..4afcd974 --- /dev/null +++ b/tests/test_btf/test_btf.py @@ -0,0 +1,87 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_btf_btf.py +# Created: 2025-08-06 18:59 +# ============================================================================= + +"""Test cases for the sksparse.btf.btf function.""" + +# from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.btf import btf, btf_q_permutation + +from ..helpers import generate_random_matrices, is_valid_permutation + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + p, q, r = btf(empty_A) + assert_array_equal(p, np.array([], dtype=itype), strict=True) + assert_array_equal(q, np.array([], dtype=itype), strict=True) + assert_array_equal(r, np.zeros(1, dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_zero_input(itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + p, q, r = btf(zero_A) + assert_array_equal(p, np.arange(N, dtype=itype), strict=True) + assert_array_equal(q, -np.arange(N, dtype=itype) - 2, strict=True) + assert_array_equal(r, np.arange(N + 1, dtype=itype), strict=True) + + +def test_singleton_matrix(): + singleton_A = sparse.csc_array([[1]]) + p, q, r = btf(singleton_A) + assert_array_equal(p, np.array([0], dtype=np.int32), strict=True) + assert_array_equal(q, np.array([0], dtype=np.int32), strict=True) + assert_array_equal(r, np.array([0, 1], dtype=np.int32), strict=True) + + +def test_q_permutation(): + """Test that the q permutation is correct for a simple case.""" + N = 10 + A = sparse.csc_array((N, N)) # empty array + _, q, _ = btf(A) + expect_q = -np.arange(N, dtype=np.int32) - 2 + assert_array_equal(q, expect_q, strict=True) + assert_array_equal(btf_q_permutation(q), np.abs(q + 1) - 1, strict=True) + + +@pytest.mark.parametrize( + "A", + list( + generate_random_matrices( + N_trials=100, N_max=200, d_scale=0.05, square_only=True + ) + ), +) +class TestRandomSquareMatrices: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_itype(self, A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + p, q, r = btf(A) + assert p.dtype == itype + assert p.shape == (A.shape[0],) + assert is_valid_permutation(p) + assert is_valid_permutation(btf_q_permutation(q)) + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_btf/test_maxtrans.py b/tests/test_btf/test_maxtrans.py new file mode 100644 index 00000000..85682459 --- /dev/null +++ b/tests/test_btf/test_maxtrans.py @@ -0,0 +1,78 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_btf.py +# Created: 2025-08-04 21:04 +# ============================================================================= + +"""Test cases for the sksparse.btf.maxtrans function.""" + +# from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.btf import maxtrans + +from ..helpers import generate_random_matrices, is_valid_permutation + + +def is_valid_match(p): + """Check if a maximum matching is valid.""" + if -1 not in p: + return is_valid_permutation(p) + else: + # Check uniqueness of non-negative entries + x = np.array(p) + x = np.sort(x[x >= 0]) + all_x_unique = np.all(x[:-1] < x[1:]) + # Check range of all entries [-1, len(p)) + return all((p >= -1) & (p < len(p))) and all_x_unique + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + assert_array_equal(maxtrans(empty_A), np.array([], dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_zero_input(itype): + M, N = 10, 8 # arbitrary + zero_A = sparse.csc_array((M, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + assert_array_equal(maxtrans(zero_A), np.full(M, -1, dtype=itype), strict=True) + + +def test_singleton_matrix(): + singleton_A = sparse.csc_array([[1]]) + assert_array_equal( + maxtrans(singleton_A), np.array([0], dtype=np.int32), strict=True + ) + + +@pytest.mark.parametrize( + "A", + list(generate_random_matrices(N_trials=100, N_max=200, d_scale=0.05)), +) +class TestRandomSquareMatrices: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_itype(self, A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + p = maxtrans(A) + assert p.dtype == itype + assert p.shape == (A.shape[0],) + assert is_valid_match(p) + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_btf/test_strongcomp.py b/tests/test_btf/test_strongcomp.py new file mode 100644 index 00000000..b44fd4df --- /dev/null +++ b/tests/test_btf/test_strongcomp.py @@ -0,0 +1,87 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_btf_strongcomp.py +# Created: 2025-08-06 15:11 +# ============================================================================= + +"""Test cases for the sksparse.btf.strongcomp function.""" + +# from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import sparse + +from sksparse.btf import strongcomp + +from ..helpers import generate_random_matrices, is_valid_permutation + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + p, r = strongcomp(empty_A) + assert_array_equal(p, np.array([], dtype=itype), strict=True) + assert_array_equal(r, np.zeros(1, dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_zero_input(itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + p, r = strongcomp(zero_A) + expect_r = np.zeros(N + 1, dtype=itype) + expect_r[-1] = N + assert_array_equal(p, np.arange(N, dtype=itype), strict=True) + assert_array_equal(r, expect_r, strict=True) + + +def test_singleton_matrix(): + singleton_A = sparse.csc_array([[1]]) + p, r = strongcomp(singleton_A) + assert_array_equal(p, np.array([0], dtype=np.int32), strict=True) + assert_array_equal(r, np.array([0, 1], dtype=np.int32), strict=True) + + +@pytest.mark.parametrize( + "A", + list( + generate_random_matrices( + N_trials=100, N_max=200, d_scale=0.05, square_only=True + ) + ), +) +class TestRandomSquareMatrices: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_itype(self, A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + p, r = strongcomp(A) + assert p.dtype == itype + assert p.shape == (A.shape[0],) + assert is_valid_permutation(p) + + +def test_column_permutation(): + rng = np.random.default_rng(565656) + N = 100 + A = sparse.random_array((N, N), density=0.2, format="csc", rng=rng) + qin = rng.permutation(N) + AQ = A[:, qin].tocsc() + p_, r_ = strongcomp(AQ) + p, q, r = strongcomp(A, qin) + assert_array_equal(q, qin[p_]) + assert_allclose(A[p][:, q].toarray(), AQ[p_][:, p_].toarray(), atol=1e-15) + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_camd.py b/tests/test_camd.py new file mode 100644 index 00000000..68a8be88 --- /dev/null +++ b/tests/test_camd.py @@ -0,0 +1,196 @@ +# Test cases for the sksparse.camd module. +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_camd.py +# Created: 2025-07-28 13:34 +# ============================================================================= + +"""Test cases for the sksparse.camd module.""" + +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.camd import CAMDInfo, camd, camd_default_control + +from .helpers import generate_random_matrices, is_valid_permutation + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + assert_array_equal(camd(empty_A), np.array([], dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_zero_input(itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + assert_array_equal(camd(zero_A), np.arange(N, dtype=itype), strict=True) + + +def test_singleton_matrix(): + singleton_A = sparse.csc_array([[1]]) + assert_array_equal(camd(singleton_A), np.array([0], dtype=np.int32), strict=True) + + +@pytest.mark.parametrize( + "A", + list( + generate_random_matrices( + N_trials=100, N_max=200, d_scale=0.05, square_only=True + ) + ), +) +class TestRandomSquareMatrices: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_itype(self, A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + p = camd(A) + assert p.dtype == itype + assert p.shape == (A.shape[0],) + assert is_valid_permutation(p) + + @pytest.mark.parametrize("aggressive", [True, False]) + def test_aggressive(self, A, aggressive): + p = camd(A, aggressive=aggressive) + assert is_valid_permutation(p) + + +@pytest.mark.parametrize("dense_thresh", [None, 8, 5]) +def test_camd_with_dense_rows(dense_thresh): + N = 1000 + rng = np.random.default_rng(56) + A = sparse.random_array((N, N), density=0.001, format="lil", rng=rng) + + # Create a known number of dense rows above the threshold + # thresh is actually dense_thresh * sqrt(N) == dense_thresh * 10 + # for N = 1000, density = 0.001: + # A.astype(bool).sum(axis=1).max() ~ 5 + # (A + A.T).astype(bool).sum(axis=1).max() ~ 10 + CAMD_DEFAULT_DENSE = 10 # default value from camd.h + thresh = int( + (dense_thresh if dense_thresh is not None else CAMD_DEFAULT_DENSE) * np.sqrt(N) + ) + + N_dense_rows = 10 # arbitrary choice for number of dense rows + N_elems = min(2 * thresh, N) # arbitrary choice to ensure enough elements + + dense_row_idx = rng.choice(N, size=N_dense_rows, replace=False) + col_idx = np.array( + [rng.choice(N, size=N_elems, replace=False) for _ in range(N_dense_rows)] + ) + + for i, js in zip(dense_row_idx, col_idx): + A[i, js] = rng.random(size=N_elems) + + # Make sure the matrix is symmetric + A = A + A.T + A = A.tocsc() + p = camd(A, dense_thresh=dense_thresh) + + assert is_valid_permutation(p) + + # Expect dense rows at the end of the permutation, but maybe not in order + assert_array_equal(np.sort(p[-N_dense_rows:]), np.sort(dense_row_idx)) + + +def test_info_can_24(): + # The can_24 matrix is used in the SuiteSparse CAMD MATLAB/camd_demo.m file. + expect_info = CAMDInfo.from_array( + np.array([ + 0, # status + 24, # N + 160, # nz + 1, # symmetry + 24, # nzdiag + 136, # nz_A_plus_AT + 0, # Ndense + 3288, # memory + 0, # Ncmpa + 97, # Lnz + 97, # Ndiv + 275, # Nmultsubs_LDL + 453, # Nmultsubs_LU + 8, # dmax + ]) + ) + + # Load the can_24 matrix from a file + can_24_path = Path("tests") / "data" / "can_24" + with can_24_path.open() as fp: + can_24 = np.genfromtxt(fp, dtype=int) + + A = sparse.csc_array((can_24[:, 2], (can_24[:, 0] - 1, can_24[:, 1] - 1))) + p, info = camd(A, return_info=True) + + assert is_valid_permutation(p) + assert info == expect_info + + +def test_camd_default_control(): + # The default control settings are (from camd.h): + # - CAMD_DEFAULT_DENSE -> dense_thresh: 10.0 + # - CAMD_DEFAULT_AGGRESSIVE -> aggressive: True + expect_control = { + "dense_thresh": 10.0, + "aggressive": True, + } + control = camd_default_control() + assert control == expect_control + + A = sparse.csc_array([[1, 2], [3, 4]]) + p = camd(A, **control) + assert is_valid_permutation(p) + + +@pytest.fixture(scope="class") +def rand_matrix_A(): + N = 10 + rng = np.random.default_rng(56) + A = sparse.random_array((N, N), density=0.4, format="lil", rng=rng) + A.setdiag(N) + A = A.tocsc() + return A, N, rng + + +class TestConstraints: + def test_complete_constraints(self, rand_matrix_A): + A, N, _ = rand_matrix_A + C = np.arange(N) + q = camd(A, constraints=C) + assert_array_equal(q, C) + + def test_general_constraints(self, rand_matrix_A): + A, N, rng = rand_matrix_A + # Set some constraints + k = 3 + C = np.full(N, 2, dtype=int) + all_idx = rng.permutation(N) + C[all_idx[:k]] = 0 + C[all_idx[k:2*k]] = 1 + + p = camd(A, constraints=C) + + assert is_valid_permutation(p) + # Check that the constraints are respected + assert all(C[p][:k] == 0) + assert all(C[p][k:2*k] == 1) + assert all(C[p][2*k:] == 2) + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_ccolamd.py b/tests/test_ccolamd.py new file mode 100644 index 00000000..f6478caa --- /dev/null +++ b/tests/test_ccolamd.py @@ -0,0 +1,327 @@ +# Test cases for the sksparse.ccolamd module. +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_ccolamd.py +# Created: 2025-07-31 11:42 +# ============================================================================= + +"""Test cases for the sksparse.ccolamd module.""" + +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.ccolamd import CCOLAMDStats, ccolamd, ccolamd_get_defaults, csymamd + +from .helpers import generate_random_matrices, is_valid_permutation + + +class _BasicInputMixin: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_empty_input(self, itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + assert_array_equal( + self.ccolamd_func(empty_A), np.array([], dtype=itype), strict=True + ) + + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_zero_input(self, itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + assert_array_equal( + self.ccolamd_func(zero_A), np.arange(N, dtype=itype), strict=True + ) + + def test_singleton_matrix(self): + singleton_A = sparse.csc_array([[1]]) + assert_array_equal( + self.ccolamd_func(singleton_A), np.array([0], dtype=np.int32), strict=True + ) + + +class TestColamdInput(_BasicInputMixin): + ccolamd_func = staticmethod(ccolamd) + + def test_2D_row_input(self): + N = 10 + A = sparse.csc_array(np.arange(N)[np.newaxis, :]) # (1, N) + q = self.ccolamd_func(A) + assert_array_equal(q, np.arange(N, dtype=np.int32), strict=True) + + def test_2D_col_input(self): + N = 10 + A = sparse.csc_array(np.arange(N)[:, np.newaxis]) # (N, 1) + q = self.ccolamd_func(A) + assert_array_equal(q, np.zeros(1, dtype=np.int32), strict=True) + + +class TestSymamdInput(_BasicInputMixin): + ccolamd_func = staticmethod(csymamd) + + +class _RandomInputMixin: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_itype(self, A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + q = self.ccolamd_func(A) + assert q.dtype == itype + assert q.shape == (A.shape[0],) + assert is_valid_permutation(q) + + @pytest.mark.parametrize("aggressive", [True, False]) + def test_aggressive(self, A, aggressive): + q = self.ccolamd_func(A, aggressive=aggressive) + assert is_valid_permutation(q) + + +@pytest.mark.parametrize( + "A", + list(generate_random_matrices(N_trials=100, N_max=200, d_scale=0.05)), +) +class TestColamdRandomInput(_RandomInputMixin): + ccolamd_func = staticmethod(ccolamd) + + +@pytest.mark.parametrize( + "A", + list( + generate_random_matrices( + N_trials=100, N_max=200, d_scale=0.05, square_only=True + ) + ), +) +class TestSymamdRandomInput(_RandomInputMixin): + ccolamd_func = staticmethod(csymamd) + + +CCOLAMD_DEFAULT_DENSE = 10 # NOTE depends on the default in ccolamd.c +DENSE_THRESHOLDS = [None, 5, 2] + + +@pytest.mark.parametrize("row_or_col", ["row", "col"]) +@pytest.mark.parametrize("dense_thresh", DENSE_THRESHOLDS) +def test_ccolamd_with_dense(dense_thresh, row_or_col): + M = 1000 # arbitrary size + N = 800 + rng = np.random.default_rng(56) + A = sparse.random_array((M, N), density=0.001, format="lil", rng=rng) + + max_N_rowcols, max_N_elems = (M, N) if row_or_col == "row" else (N, M) + + # Create a known number of dense rows/cols above the threshold + # thresh is actually dense_thresh * sqrt(N) == dense_thresh * 10 + # max(A[i] for i in range(N)) is ~ 5 for N = 1000, density = 0.001 + thresh = int( + (dense_thresh if dense_thresh is not None else CCOLAMD_DEFAULT_DENSE) + * np.sqrt(max_N_elems) + ) + + N_dense = 10 # arbitrary choice for number of dense rows/columns + N_elems = min(2 * thresh, max_N_elems) # arbitrary choice for enough elements + + dense_idx = rng.choice(max_N_rowcols, size=N_dense, replace=False) + other_idxs = np.array( + [rng.choice(max_N_elems, size=N_elems, replace=False) for _ in range(N_dense)] + ) + + for i, js in zip(dense_idx, other_idxs): + if row_or_col == "row": + A[i, js] = rng.random(size=N_elems) + else: + A[js, i] = rng.random(size=N_elems) + + A = A.tocsc() + + kwargs = {f"dense_{row_or_col}_thresh": dense_thresh, "return_info": True} + q, stats = ccolamd(A, **kwargs) + + assert is_valid_permutation(q) + + # DEBUG: plot the matrix before and after permutation + # fig, axs = plt.subplots(num=1, ncols=2, clear=True) + # axs[0].spy(A, markersize=1) + # axs[1].spy(A[:, q], markersize=1) + # plt.show() + + # Expect dense cols at the end of the permutation, but maybe not in order + # *empty* columns are also moved to the end of the matrix, + # so we need to check the stats.N_cols_ignored value + N_empty = (A.count_nonzero(axis=0) == 0).sum() + N_cols_ignored = N_dense + N_empty + + if row_or_col == "col": + assert_array_equal( + np.sort(q[-N_cols_ignored : -(N_cols_ignored - N_dense)]), + np.sort(dense_idx), + ) + + # NOTE in *c*colamd, the N_cols_ignored value is the number of dense + # columns, while in colamd, it is the number of dense + empty columns. This + # appears to be a bug in the CCOLAMD implementation, as the documentation + # in ccolamd.c states that the N_cols_ignored value should be the number of + # dense + empty columns. + # if row_or_col == "col": + # assert_array_equal( + # np.sort(q[-stats.N_cols_ignored:-(stats.N_cols_ignored - N_dense)]), + # np.sort(dense_idx) + # ) + + +@pytest.mark.parametrize("dense_thresh", DENSE_THRESHOLDS) +def test_csymamd_with_dense(dense_thresh): + N = 1000 # arbitrary size + rng = np.random.default_rng(56) + A = sparse.random_array((N, N), density=0.001, format="lil", rng=rng) + + # Create a known number of dense rows/cols above the threshold + # thresh is actually dense_thresh * sqrt(N) == dense_thresh * 10 + # max(A[i] for i in range(N)) is ~ 5 for N = 1000, density = 0.001 + thresh = int( + (dense_thresh if dense_thresh is not None else CCOLAMD_DEFAULT_DENSE) + * np.sqrt(N) + ) + + N_dense = 10 # arbitrary choice for number of dense rows/columns + N_elems = min(2 * thresh, N) # arbitrary choice for enough elements + + dense_idx = rng.choice(N, size=N_dense, replace=False) + other_idxs = np.array( + [rng.choice(N, size=N_elems, replace=False) for _ in range(N_dense)] + ) + + for i, js in zip(dense_idx, other_idxs): + A[i, js] = rng.random(size=N_elems) + + A = A + A.T # make it symmetric + A = A.tocsc() + + q, stats = csymamd(A, return_info=True) + + assert is_valid_permutation(q) + + # DEBUG: plot the matrix before and after permutation + # fig, axs = plt.subplots(num=1, ncols=2, clear=True) + # axs[0].spy(A, markersize=1) + # axs[1].spy(A[q][:, q], markersize=1) + # plt.show() + + # NOTE I am not sure what the expected behavior is here for csymamd (vs + # ccolamd) The test *almost* passes, but there are some empty rows/columns + # interspersed with the dense rows/columns at the end of the matrix. There + # may not be a deterministic order of the dense/empty rows/columns that + # applies to every matrix, so we cannot make a strong assertion here. + + # Check that the same number of rows/columns are ignored. + assert stats.N_rows_ignored == stats.N_cols_ignored + + # Expect dense cols at the end of the permutation, but maybe not in order. + # *empty* columns are also moved to the end of the matrix, so we need to + # check the stats.N_cols_ignored value + # assert_array_equal( + # np.sort(q[-stats.N_cols_ignored:-(stats.N_cols_ignored - N_dense)]), + # np.sort(dense_idx) + # ) + + +def test_info_can_24(): + # The can_24 matrix is used in the SuiteSparse AMD MATLAB/amd_demo.m file. + expect_info = CCOLAMDStats.from_array( + np.array( + [ + 0, # N_rows_ignored + 0, # N_cols_ignored + 1, # Ncmpa + 0, # status + -1, # info1 + -1, # info2 + 0, # info3 + ] + ) + ) + + # Load the can_24 matrix from a file + can_24_path = Path("tests") / "data" / "can_24" + with can_24_path.open() as fp: + can_24 = np.genfromtxt(fp, dtype=int) + + A = sparse.csc_array((can_24[:, 2], (can_24[:, 0] - 1, can_24[:, 1] - 1))) + q, info = ccolamd(A, return_info=True) + + assert is_valid_permutation(q) + assert info == expect_info + + +def test_ccolamd_defaults(): + """Test that CCOLAMD uses the default control settings.""" + # The default control settings are (from ccolamd.c:1095-1097): + # knobs[CCOLAMD_DENSE_ROW] = 10 ; + # knobs[CCOLAMD_DENSE_COL] = 10 ; + # knobs[CCOLAMD_AGGRESSIVE] = TRUE ; + # knobs[CCOLAMD_LU] = FALSE ; + expect_knobs = { + "dense_row_thresh": 10, + "dense_col_thresh": 10, + "aggressive": True, + "opt_lu": "cholesky", + } + knobs = ccolamd_get_defaults() + assert knobs == expect_knobs + + A = sparse.csc_array([[1, 2], [3, 4]]) + p = ccolamd(A, **knobs) + assert is_valid_permutation(p) + + +@pytest.fixture(scope="class") +def rand_matrix_A(): + M, N = 20, 17 + rng = np.random.default_rng(56) + A = sparse.random_array((M, N), density=0.4, format="lil", rng=rng) + A.setdiag(N) + A = A.tocsc() + return A, N, rng + + +class TestConstraints: + def test_complete_constraints(self, rand_matrix_A): + A, N, _ = rand_matrix_A + C = np.arange(N) + q = ccolamd(A, constraints=C) + assert_array_equal(q, C) + + def test_constraints(self, rand_matrix_A): + A, N, rng = rand_matrix_A + # Set some constraints + k = 3 + C = np.full(N, 2, dtype=int) + all_idx = rng.permutation(N) + C[all_idx[:k]] = 0 + C[all_idx[k:2*k]] = 1 + + q = ccolamd(A, constraints=C) + + assert is_valid_permutation(q) + # Check that the constraints are respected + print("Constraints:") + print(C) + print(C[q]) # should be [0, 0, 0, 1, 1, 1, 2, 2, ...] + assert all(C[q][:k] == 0) + assert all(C[q][k:2*k] == 1) + assert all(C[q][2*k:] == 2) + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_cholmod.py b/tests/test_cholmod.py deleted file mode 100644 index 0b445319..00000000 --- a/tests/test_cholmod.py +++ /dev/null @@ -1,290 +0,0 @@ -# Test code for the scikits.sparse CHOLMOD wrapper. - -# Copyright (C) 2008-2017 The scikit-sparse developers: -# -# 2008 David Cournapeau -# 2009-2015 Nathaniel Smith -# 2010 Dag Sverre Seljebotn -# 2014 Leon Barrett -# 2015 Yuri -# 2016-2017 Antony Lee -# 2016 Alex Grigorievskiy -# 2016-2017 Joscha Reimer -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. - -from functools import partial -import os.path - -from pytest import raises as assert_raises -import numpy as np -from numpy.testing import assert_allclose, assert_array_equal -from scipy import sparse -from sksparse.cholmod import ( - cholesky, - cholesky_AAt, - analyze, - analyze_AAt, - CholmodError, - CholmodNotPositiveDefiniteError, - _modes, - _ordering_methods, -) - -modes = tuple(_modes.keys()) -ordering_methods = tuple(_ordering_methods.keys()) - -# Match defaults of np.allclose, which were used before (and are needed). -assert_allclose = partial(assert_allclose, rtol=1e-5, atol=1e-8) - - -def test_cholesky_smoke_test(): - f = cholesky(sparse.eye(10, 10)) - d = np.arange(20).reshape(10, 2) - print("dense") - assert_allclose(f(d), d) - print("sparse") - s_csc = sparse.csc_matrix(np.eye(10)[:, :2]) - assert sparse.issparse(f(s_csc)) - assert_allclose(f(s_csc).todense(), s_csc.todense()) - print("csc_array") - sa_csc = sparse.csc_array(s_csc) - assert sparse.issparse(f(sa_csc)) - assert_allclose(f(sa_csc).todense(), sa_csc.todense()) - print("csr") - s_csr = s_csc.tocsr() - assert sparse.issparse(f(s_csr)) - assert_allclose(f(s_csr).todense(), s_csr.todense()) - print("extract") - assert np.all(f.P() == np.arange(10)) - - -def test_writeability(): - t = cholesky(sparse.eye(10, 10))(np.arange(10)) - assert t.flags["WRITEABLE"] - - -def real_matrix(): - return sparse.csc_matrix([[10, 0, 3, 0], [0, 5, 0, -2], [3, 0, 5, 0], [0, -2, 0, 2]]) - - -def complex_matrix(): - return sparse.csc_matrix([[10, 0, 3 - 1j, 0], [0, 5, 0, -2], [3 + 1j, 0, 5, 0], [0, -2, 0, 2]]) - - -def factor_of(factor, matrix): - return np.allclose( - (factor.L() * factor.L().T.conjugate()).todense(), matrix.todense()[factor.P()[:, np.newaxis], factor.P()[np.newaxis, :]] - ) - - -def convert_matrix_indices_to_long_indices(matrix): - matrix.indices = np.asarray(matrix.indices, dtype=np.int64) - matrix.indptr = np.asarray(matrix.indptr, dtype=np.int64) - return matrix - - -def test_complex(): - c = complex_matrix() - fc = cholesky(c) - r = real_matrix() - fr = cholesky(r) - - assert factor_of(fc, c) - - assert_allclose(fc(np.arange(4))[:, None], c.todense().I * np.arange(4)[:, None]) - assert_allclose(fc(np.arange(4) * 1j)[:, None], c.todense().I * (np.arange(4) * 1j)[:, None]) - assert_allclose(fr(np.arange(4))[:, None], r.todense().I * np.arange(4)[:, None]) - # If we did a real factorization, we can't do solves on complex arrays: - assert_raises(CholmodError, fr, np.arange(4) * 1j) - - -def test_beta(): - for matrix in [real_matrix(), complex_matrix()]: - for beta in [0, 1, 3.4]: - matrix_plus_beta = matrix + beta * sparse.eye(*matrix.shape) - for use_long in [False, True]: - if use_long: - matrix_plus_beta = convert_matrix_indices_to_long_indices(matrix_plus_beta) - for ordering_method in ordering_methods: - for mode in modes: - f = cholesky(matrix, beta=beta, mode=mode, ordering_method=ordering_method) - L = f.L() - assert factor_of(f, matrix_plus_beta) - - -def test_update_downdate(): - m = real_matrix() - f = cholesky(m) - L = f.L()[f.P(), :] - assert factor_of(f, m) - f.update_inplace(L) - assert factor_of(f, 2 * m) - f.update_inplace(L) - assert factor_of(f, 3 * m) - f.update_inplace(L, subtract=True) - assert factor_of(f, 2 * m) - f.update_inplace(L, subtract=True) - assert factor_of(f, m) - - -def test_solve_edge_cases(): - m = real_matrix() - f = cholesky(m) - # sparse matrices give a sparse back: - assert sparse.issparse(f(sparse.eye(*m.shape).tocsc())) - # dense matrices give a dense back: - assert not sparse.issparse(f(np.eye(*m.shape))) - # 1d dense matrices are accepted and a 1d vector is returned (this matches - # the behavior of np.dot): - assert f(np.arange(m.shape[0])).shape == (m.shape[0],) - # 2d dense matrices are also accepted: - assert f(np.arange(m.shape[0])[:, np.newaxis]).shape == (m.shape[0], 1) - # But not if the dimensions are wrong...: - assert_raises(CholmodError, f, np.arange(m.shape[0] + 1)[:, np.newaxis]) - assert_raises(CholmodError, f, np.arange(m.shape[0])[np.newaxis, :]) - assert_raises(CholmodError, f, np.arange(m.shape[0])[:, np.newaxis, np.newaxis]) - # And ditto for the sparse version: - assert_raises(CholmodError, f, sparse.eye(m.shape[0] + 1, m.shape[1]).tocsc()) - - -def mm_matrix(name): - from scipy.io import mmread - - # Supposedly, it is better to use resource_stream and pass the resulting - # open file object to mmread()... but for some reason this fails? - from pkg_resources import resource_filename - - filename = resource_filename(__name__, "test_data/%s.mtx.gz" % name) - matrix = mmread(filename) - if sparse.issparse(matrix): - matrix = matrix.tocsc() - return matrix - - -def test_cholesky_matrix_market(): - for problem in ("well1033", "illc1033", "well1850", "illc1850"): - X = mm_matrix(problem) - y = mm_matrix(problem + "_rhs1") - answer = np.linalg.lstsq(X.todense(), y)[0] - XtX = (X.T * X).tocsc() - Xty = X.T * y - for mode in modes: - assert_allclose(cholesky(XtX, mode=mode)(Xty), answer) - assert_allclose(cholesky_AAt(X.T, mode=mode)(Xty), answer) - assert_allclose(cholesky(XtX, mode=mode).solve_A(Xty), answer) - assert_allclose(cholesky_AAt(X.T, mode=mode).solve_A(Xty), answer) - - f1 = analyze(XtX, mode=mode) - f2 = f1.cholesky(XtX) - assert_allclose(f2(Xty), answer) - assert_raises(CholmodError, f1, Xty) - assert_raises(CholmodError, f1.solve_A, Xty) - assert_raises(CholmodError, f1.solve_LDLt, Xty) - assert_raises(CholmodError, f1.solve_LD, Xty) - assert_raises(CholmodError, f1.solve_DLt, Xty) - assert_raises(CholmodError, f1.solve_L, Xty) - assert_raises(CholmodError, f1.solve_D, Xty) - assert_raises(CholmodError, f1.apply_P, Xty) - assert_raises(CholmodError, f1.apply_Pt, Xty) - f1.P() - assert_raises(CholmodError, f1.L) - assert_raises(CholmodError, f1.LD) - assert_raises(CholmodError, f1.L_D) - assert_raises(CholmodError, f1.L_D) - f1.cholesky_inplace(XtX) - assert_allclose(f1(Xty), answer) - - f3 = analyze_AAt(X.T, mode=mode) - f4 = f3.cholesky(XtX) - assert_allclose(f4(Xty), answer) - assert_raises(CholmodError, f3, Xty) - f3.cholesky_AAt_inplace(X.T) - assert_allclose(f3(Xty), answer) - - print(problem, mode) - for f in (f1, f2, f3, f4): - pXtX = XtX.todense()[f.P()[:, np.newaxis], f.P()[np.newaxis, :]] - assert_allclose(np.prod(f.D()), np.linalg.det(XtX.todense())) - assert_allclose((f.L() * f.L().T).todense(), pXtX) - L, D = f.L_D() - assert_allclose((L * D * L.T).todense(), pXtX) - - b = np.arange(XtX.shape[0])[:, np.newaxis] - assert_allclose(f.solve_A(b), np.dot(XtX.todense().I, b)) - assert_allclose(f(b), np.dot(XtX.todense().I, b)) - assert_allclose(f.solve_LDLt(b), np.dot((L * D * L.T).todense().I, b)) - assert_allclose(f.solve_LD(b), np.dot((L * D).todense().I, b)) - assert_allclose(f.solve_DLt(b), np.dot((D * L.T).todense().I, b)) - assert_allclose(f.solve_L(b), np.dot(L.todense().I, b)) - assert_allclose(f.solve_Lt(b), np.dot(L.T.todense().I, b)) - assert_allclose(f.solve_D(b), np.dot(D.todense().I, b)) - - assert_allclose(f.apply_P(b), b[f.P(), :]) - assert_allclose(f.apply_P(b), b[f.P(), :]) - # Pt is the inverse of P, and argsort inverts permutation - # vectors: - assert_allclose(f.apply_Pt(b), b[np.argsort(f.P()), :]) - assert_allclose(f.apply_Pt(b), b[np.argsort(f.P()), :]) - - -def test_convenience(): - A_dense_seed = np.array([[10, 0, 3, 0], [0, 5, 0, -2], [3, 0, 5, 0], [0, -2, 0, 2]]) - for dtype in (float, complex): - A_dense = np.array(A_dense_seed, dtype=dtype) - A_sp = sparse.csc_matrix(A_dense) - for use_long in [False, True]: - if use_long: - A_sp = convert_matrix_indices_to_long_indices(A_sp) - for ordering_method in ordering_methods: - for mode in modes: - print("----") - print(dtype) - print(A_sp.indices.dtype) - print(use_long) - print(ordering_method) - print(mode) - print("----") - f = cholesky(A_sp, mode=mode, ordering_method=ordering_method) - print(f.D()) - assert_allclose(f.det(), np.linalg.det(A_dense)) - assert_allclose(f.logdet(), np.log(np.linalg.det(A_dense))) - assert_allclose(f.slogdet(), [1, np.log(np.linalg.det(A_dense))]) - assert_allclose((f.inv() * A_sp).todense(), np.eye(4)) - - -def test_CholmodNotPositiveDefiniteError(): - A = -sparse.eye(4).tocsc() - f = cholesky(A) - assert_raises(CholmodNotPositiveDefiniteError, f.L) - - -def test_natural_ordering_method(): - A = real_matrix() - f = cholesky(A, ordering_method="natural") - p = f.P() - assert_array_equal(p, np.arange(len(p))) diff --git a/tests/test_cholmod/__init__.py b/tests/test_cholmod/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_cholmod/test_analyze.py b/tests/test_cholmod/test_analyze.py new file mode 100644 index 00000000..6f87347b --- /dev/null +++ b/tests/test_cholmod/test_analyze.py @@ -0,0 +1,216 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_analyze.py +# Created: 2025-08-18 21:08 +# ============================================================================= + +"""Unit tests for the cholmod.analyze function.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.cholmod import ( + CholeskyFactor, + CholmodError, + CholmodNotPositiveDefiniteError, +) + +from ..helpers import generate_random_matrices, is_valid_permutation + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.fixture +def A_default(): + return sparse.csc_array([[1, 2], [3, 4]]) + + +def test_bad_kind(A_default): + with pytest.raises(ValueError, match="Unknown symmetry kind"): + CholeskyFactor(A_default, sym_kind="invalid") + + +def test_bad_supernodal(A_default): + with pytest.raises(ValueError, match="Unknown factorization mode"): + CholeskyFactor(A_default, supernodal_mode="invalid") + + +def test_bad_order(A_default): + with pytest.raises(ValueError, match="Unknown ordering method"): + CholeskyFactor(A_default, order="invalid") + + +def test_empty_input(): + empty_A = sparse.csc_array((0, 0)) + f = CholeskyFactor(empty_A) + p = f.perm + count = f.colcount + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(p, empty_p, strict=True) + assert_array_equal(count, empty_p, strict=True) + + +def test_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + with pytest.raises(CholmodNotPositiveDefiniteError, match="not positive definite"): + CholeskyFactor(zero_A) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + f = CholeskyFactor(singleton_A) + p = f.perm + count = f.colcount + expect_p = np.array([0], dtype=singleton_A.indptr.dtype) + expect_count = np.array([1], dtype=singleton_A.indptr.dtype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(count, expect_count, strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_itype(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + f = CholeskyFactor(A) + p = f.perm + count = f.colcount + expect_count = np.array([3, 3, 4, 3, 3, 4, 4, 3, 3, 2, 1], dtype=itype) + # expect_count = sum(lchol(A) != 0, 1) in MATLAB (natural ordering) + assert p.dtype == itype + assert count.dtype == itype + assert is_valid_permutation(p) + assert len(count) == N + assert np.all(count >= 0) + assert np.all(count <= N) + assert_array_equal(count, expect_count, strict=True) + assert f.nnz == 33 # == nnz(lchol(A)) in MATLAB (natural ordering) + + +def test_solve_symbolic(davis_example_chol): + A = davis_example_chol + f = CholeskyFactor(A) + b = np.arange(A.shape[0], dtype=A.dtype) + with pytest.raises(CholmodError, match="is symbolic"): + f.solve(b) + + +def test_update_symbolic(davis_example_chol): + A = davis_example_chol + f = CholeskyFactor(A) + with pytest.raises(CholmodError, match="is symbolic"): + f.update(A[:, :1]) + + +def test_resymbol_symbolic(davis_example_chol): + A = davis_example_chol + f = CholeskyFactor(A) + with pytest.raises(CholmodError, match="is symbolic"): + f.resymbol(A) + + +@pytest.mark.parametrize("method", ["slogdet", "logdet", "det"]) +def test_det_symbolic(davis_example_chol, method): + A = davis_example_chol + f = CholeskyFactor(A) + with pytest.raises(CholmodError, match="is symbolic"): + f.__getattribute__(method)() + + +ORDERS = [ + None, + "default", + "best", + "natural", + "amd", + "metis", + "nesdis", + "colamd", + "postordered", +] + + +@pytest.mark.parametrize("order", ORDERS) +def test_ordering(davis_example_chol, order): + f = CholeskyFactor(davis_example_chol, order=order) + assert f.order in ORDERS + if order is None: + assert f.order == "natural" + elif order not in ("default", "best"): + # default and best may return any ordering + assert f.order == order + else: + print(order, f.order) # still passes, but just for info + + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +posdef_As = list( + generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True) +) + + +@pytest.mark.parametrize("A", posdef_As) +@pytest.mark.parametrize("supernodal_mode", [None, "auto", "simplicial", "supernodal"]) +def test_supernodal_mode(A, supernodal_mode): + N = A.shape[0] + f = CholeskyFactor(A, supernodal_mode=supernodal_mode) + p = f.perm + count = f.colcount + if supernodal_mode not in (None, "auto"): + assert f.is_super == (supernodal_mode == "supernodal") + assert is_valid_permutation(p) + assert len(count) == N + assert np.all(count >= 0) + assert np.all(count <= N) + + +@pytest.mark.parametrize("A", posdef_As) +@pytest.mark.parametrize("order", ORDERS) +def test_order(A, order): + N = A.shape[0] + f = CholeskyFactor(A, order=order) + p = f.perm + count = f.colcount + assert is_valid_permutation(p) + assert len(count) == N + assert np.all(count >= 0) + assert np.all(count <= N) + + +@pytest.mark.parametrize("A", posdef_As) +@pytest.mark.parametrize("sym_kind", [None, "sym"]) +def test_kind_sym(A, sym_kind): + N = A.shape[0] + f = CholeskyFactor(A, sym_kind=sym_kind) + p = f.perm + count = f.colcount + assert is_valid_permutation(p) + assert len(count) == N + assert np.all(count >= 0) + assert np.all(count <= N) + + +@pytest.mark.parametrize( + "A", list(generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05)) +) +@pytest.mark.parametrize("sym_kind", ["row", "col"]) +def test_kind_rowcol(A, sym_kind): + N = A.shape[0] if sym_kind == "row" else A.shape[1] + f = CholeskyFactor(A, sym_kind=sym_kind) + p = f.perm + count = f.colcount + assert is_valid_permutation(p) + assert len(count) == N + assert np.all(count >= 0) + assert np.all(count <= N) diff --git a/tests/test_cholmod/test_bisect.py b/tests/test_cholmod/test_bisect.py new file mode 100644 index 00000000..fc8a975e --- /dev/null +++ b/tests/test_cholmod/test_bisect.py @@ -0,0 +1,158 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_bisect.py +# Created: 2025-08-21 11:15 +# ============================================================================= + +"""Unit tests for the cholmod.bisect function.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.cholmod import bisect + +from ..helpers import generate_random_matrices + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.fixture +def A_default(): + return sparse.csc_array([[1, 2], [3, 4]]) + + +def test_bad_kind(A_default): + with pytest.raises(ValueError, match="Unknown factorization kind"): + bisect(A_default, kind="invalid") + + +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_nonsquare_input(kind): + A = sparse.csc_array((10, 8)) + with pytest.raises(ValueError, match="must be square"): + bisect(A, kind=kind) + + +def test_empty_defaults(): + empty_A = sparse.csc_array((0, 0)) + s = bisect(empty_A) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(s, empty_p, strict=True) + + +def test_empty_row(): + empty_A = sparse.csc_array((0, 3)) + s = bisect(empty_A, kind="row") + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(s, empty_p, strict=True) + + +def test_empty_col(): + empty_A = sparse.csc_array((3, 0)) + s = bisect(empty_A, kind="col") + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(s, empty_p, strict=True) + + +def _expect_s_zero_bisect(N, itype): + expect_s = np.empty(N, dtype=itype) + k = N // 2 + expect_s[:k] = 0 + expect_s[k:] = 1 + expect_s[-1] = 2 # last node is the separator + return expect_s + + +def test_square_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + s = bisect(zero_A) + expect_s = _expect_s_zero_bisect(N, zero_A.indptr.dtype) + assert_array_equal(s, expect_s, strict=True) + + +def test_nonsquare_zero_input_row(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + s = bisect(zero_A, kind="row") + expect_s = _expect_s_zero_bisect(M, zero_A.indptr.dtype) + assert_array_equal(s, expect_s, strict=True) + + +def test_nonsquare_zero_input_col(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + s = bisect(zero_A, kind="col") + expect_s = _expect_s_zero_bisect(N, zero_A.indptr.dtype) + assert_array_equal(s, expect_s, strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + itype = singleton_A.indptr.dtype + s = bisect(singleton_A) + # The only node *is* the separator + assert_array_equal(s, np.array([2], dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_bisect_known(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + print("\nA_example:") + print(A.toarray()) + print() + s = bisect(A) + # Computed with MATLAB CHOLMOD bisect function s = bisect(A, 'sym') + expect_s = np.array([0, 1, 1, 0, 1, 0, 0, 1, 0, 2, 2], dtype=itype) + assert_array_equal(s, expect_s, strict=True) + + +def test_rowcol(davis_example_chol): + A = davis_example_chol + s_col = bisect(A, kind="col") + # Check that the factorization is correct + itype = A.indptr.dtype + # Computed with MATLAB CHOLMOD bisect function s = bisect(A, 'col') + expect_s = np.array([2, 1, 1, 2, 1, 2, 1, 1, 0, 2, 1], dtype=itype) + assert_array_equal(s_col, expect_s, strict=True) + # Compare with the factorization of the transpose + # A is symmetric (A = A.T), so A @ A.T == A.T @ A + s_row = bisect(A.T.tocsc(), kind="row") + assert_array_equal(s_row, s_col, strict=True) + + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +pos_def_As = list( + generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True) +) +general_As = list(generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05)) + + +def _test_kind(A, kind): + N = A.shape[0] + s = bisect(A, kind=kind) + assert len(s) == N + assert np.isin(s, [0, 1, 2]).all() + + +@pytest.mark.parametrize("A", pos_def_As) +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_kind(A, kind): + _test_kind(A, kind) + + +@pytest.mark.parametrize("A", general_As) +@pytest.mark.parametrize("kind", ["row", "col"]) +def test_rowcol_kind(A, kind): + _test_kind(A, kind) diff --git a/tests/test_cholmod/test_cho_factor.py b/tests/test_cholmod/test_cho_factor.py new file mode 100644 index 00000000..8d5a3915 --- /dev/null +++ b/tests/test_cholmod/test_cho_factor.py @@ -0,0 +1,152 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_cho_factor.py +# Created: 2025-09-04 19:32 +# ============================================================================= + +"""Unit tests for the CholeskyFactor object.""" + +import warnings + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from scipy import sparse + +from sksparse.cholmod import CholmodInvalidInputError, cho_factor + +from ..helpers import generate_random_matrices + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_convert_factor(davis_example_chol, dtype): + atol = 1e-15 if dtype in (np.float64, np.complex128) else 1e-6 + A = davis_example_chol.astype(dtype) + f = cho_factor(A, lower=True) + L, D = f.get_factor(kind="LDL") + assert_allclose((L @ D @ L.T.conj()).toarray(), A.toarray(), atol=atol) + + +@pytest.mark.parametrize("order", [None, "amd"]) +def test_view_vs_get(davis_example_chol, order): + A = davis_example_chol + f = cho_factor(A, lower=True, order=order) + Lv = f.factor + pv = f.perm + L = f.get_factor() + p = f.get_perm() + assert Lv is not L # different objects + assert pv is not p + assert_allclose(Lv.toarray(), L.toarray(), atol=1e-15) + assert_allclose(pv, p, atol=1e-15) + + +@pytest.fixture +def A_small(): + return sparse.csc_array( + np.array( + [[10, 0, 3, 0], + [0, 5, 0, -2], + [3, 0, 5, 0], + [0, -2, 0, 2]] + ), + dtype=np.float64, + ) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_determinant(A_small, dtype): + A = A_small.astype(dtype) + rtol = 1e-7 if A.dtype in (np.float64, np.complex128) else 1e-6 + + f = cho_factor(A, lower=True) + + # In some versions of numpy, a warning is raised by slogdet for + # these complex types: (np.complex64, np.complex128). Make sure that is the + # warning that is raised, and not some other warning. + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") + expect_det = np.linalg.det(A.toarray()) + expect_sign, expect_logdet = np.linalg.slogdet(A.toarray()) + + if record: + assert record[0].category is RuntimeWarning + assert "divide by zero" in str(record[0].message) or "invalid value" in str( + record[0].message + ) + else: + pass + + assert_allclose(f.det(), expect_det, rtol=rtol, strict=True) + assert_allclose(f.slogdet(), (expect_sign, expect_logdet), rtol=rtol, strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_inv(A_small, dtype): + atol = 1e-12 if dtype in (np.float64, np.complex128) else 1e-3 + A = A_small.astype(dtype) + f = cho_factor(A) + Ainv = f.inv() + I = np.eye(A.shape[0], dtype=A.dtype) + assert_allclose((A @ Ainv).toarray(), I, atol=atol, strict=True) + + +def test_bad_refactor_type(A_small): + A = A_small.astype(np.float64) + f = cho_factor(A) + with pytest.raises(CholmodInvalidInputError): + f.factorize(A.astype(np.complex128)) + + +test_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True, dtype=dtype + ) +] + + +def _create_randomized_matrix(A): + """Create a new matrix with the same sparsity pattern as A but different values.""" + Bl = sparse.tril(A, -1).copy() + rng = np.random.default_rng(56) + if np.issubdtype(A.dtype, np.complexfloating): + Bl.data = rng.random(Bl.nnz, dtype=A.real.dtype) + 1j * rng.random( + Bl.nnz, dtype=A.real.dtype + ) + else: + Bl.data = rng.random(Bl.nnz, dtype=A.dtype) + B = Bl + Bl.T.conj() + # Ensure positive definiteness by adding to the diagonal + B.setdiag(A.diagonal()) + B += sparse.diags_array(np.full(B.shape[0], B.shape[0], dtype=B.dtype)) + return B.tocsc() + + +@pytest.mark.parametrize("copy", [False, True]) +@pytest.mark.parametrize("A", test_As) +def test_refactor(A, copy): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-3 + f = cho_factor(A, lower=True) + L = f.get_factor() + assert_allclose((L @ L.T.conj()).toarray(), A.toarray(), atol=atol) + # Create a new matrix with the same sparsity pattern but different values + B = _create_randomized_matrix(A) + # Factor the new matrix with the same sparsity pattern + if copy: + # Use a copy of the factorization object to ensure that we are taking + # the relevant parameters from the underlying cholmod_common object. + g = f.copy() + g.factorize(B) + Lb = g.get_factor() + else: + f.factorize(B) + Lb = f.get_factor() + assert_allclose((Lb @ Lb.T.conj()).toarray(), B.toarray(), atol=atol) diff --git a/tests/test_cholmod/test_cholesky.py b/tests/test_cholmod/test_cholesky.py new file mode 100644 index 00000000..2153fda2 --- /dev/null +++ b/tests/test_cholmod/test_cholesky.py @@ -0,0 +1,173 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_cholmod.py +# Created: 2025-08-12 14:52 +# ============================================================================= + +"""Unit tests for the cholmod module.""" + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import sparse + +from sksparse.cholmod import CholmodNotPositiveDefiniteError, cholesky + +from ..helpers import generate_random_matrices, is_valid_permutation + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + R = cholesky(empty_A) + assert_array_equal(R.toarray(), empty_A.toarray(), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_zero_input(itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + with pytest.raises(CholmodNotPositiveDefiniteError, match="not positive definite"): + cholesky(zero_A) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_matrix(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + L = cholesky(singleton_A, lower=True) + expect_L = singleton_A.copy() + assert_array_equal(L.toarray(), expect_L.toarray(), strict=True) + + +# See: Davis, Timothy A. (2006). Direct Methods for Sparse Linear Systems, +# pp 708 (Equation 2.1). +@pytest.fixture +def noncanonical_A(): + """Return a small non-canonical example matrix from Davis (2006).""" + N = 11 + rows = np.array([5, 6, 2, 7, 9, 10, 5, 9, 7, 10, 8, 9, 10, 9, 10, 10]) + cols = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 9]) + rng = np.random.default_rng(565656) + vals = rng.random(len(rows), dtype=np.float64) + L = sparse.coo_array((vals, (rows, cols)), shape=(N, N)) + A = (L + L.T).tocsc() # make it symmetric + # NOTE As of scipy v1.16.2, sparse.csc_array.setdiag() does not guarantee + # sorted indices. This line breaks A.has_canonical_format and + # A.has_sorted_indices! A.has_sorted_indices returns True, but A.indices is + # NOT sorted! + A.setdiag(N) # make it strongly positive definite + return A + + +def test_noncanonical_input(noncanonical_A): + A = noncanonical_A + expect_unsorted_cols = [0, 1, 2, 3, 4, 5, 6, 7, 9] + + # Show that A is not in canonical format (unsorted indices) + for p in range(A.shape[1]): + col_idx = A.indices[A.indptr[p] : A.indptr[p + 1]] + if not np.all(np.diff(col_idx) > 0): + assert p in expect_unsorted_cols + print(f"Column {p} is not sorted: {col_idx}") + + R = cholesky(A) + assert_allclose((R.T.conj() @ R).toarray(), A.toarray(), atol=1e-12) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_itype(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + R = cholesky(A) + assert R.indptr.dtype == itype + assert R.indices.dtype == itype + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_not_positive_definite(dtype): + # Create a simple non-positive definite matrix + A = sparse.eye_array(10, dtype=dtype).todok() + A[5:, 5:] = 0 # make it not positive definite + A = A.tocsc() + with pytest.raises( + CholmodNotPositiveDefiniteError, match="not positive definite.*column 5" + ): + cholesky(A) + + +test_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True, dtype=dtype + ) +] + + +@pytest.mark.parametrize("A", test_As[:1]) +def test_natural_ordering(A): + _L, p = cholesky(A, order="natural", lower=True) + assert_array_equal(p, np.arange(A.shape[0])) + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize( + "order", + [ + None, + "default", + "best", + "natural", + "amd", + "metis", + "nesdis", + "colamd", + "postordered", + ], +) +def test_ordering(A, order): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + if order is None: + L = cholesky(A, order=order, lower=True) + assert_allclose((L @ L.T.conj()).toarray(), A.toarray(), atol=atol) + else: + L, p = cholesky(A, order=order, lower=True) + assert is_valid_permutation(p) + PAPT = A[p][:, p] + assert_allclose((L @ L.T.conj()).toarray(), PAPT.toarray(), atol=atol) + + +@pytest.mark.parametrize("A", test_As) +def test_lower(A): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + R = cholesky(A) + L = cholesky(A, lower=True) + assert_allclose(R.T.conj().toarray(), L.toarray(), atol=atol) + + +@pytest.mark.parametrize("beta", [0.0, 1.0, 3.4]) +@pytest.mark.parametrize("order", [None, "amd"]) +@pytest.mark.parametrize("A", test_As) +def test_beta(A, beta, order): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + N = A.shape[0] + + if order is None: + L = cholesky(A, beta, lower=True) + expect_LL = (A + beta * sparse.eye_array(N)).toarray() + else: + L, p = cholesky(A, beta, lower=True, order=order) + expect_LL = (A[p][:, p] + beta * sparse.eye_array(N)).toarray() + + assert_allclose((L @ L.T.conj()).toarray(), expect_LL, atol=atol) diff --git a/tests/test_cholmod/test_cholmod.py b/tests/test_cholmod/test_cholmod.py new file mode 100644 index 00000000..15178d19 --- /dev/null +++ b/tests/test_cholmod/test_cholmod.py @@ -0,0 +1,267 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_cholmod.py +# Created: 2025-08-15 23:03 +# ============================================================================= + +"""Unit tests for the cholmod.cholmod function.""" + +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import linalg as la +from scipy import sparse +from scipy.io import mmread + +from sksparse.cholmod import ( + CholmodError, + CholmodNotPositiveDefiniteError, + CholmodWarning, + cho_factor, +) + +from ..helpers import generate_random_matrices + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +class TestBadBShape: + @pytest.fixture(scope="class") + def N(self): + return 5 + + @pytest.fixture(scope="class") + def f(self, N): + A = sparse.eye_array(N).tocsc() + return cho_factor(A) + + def test_b_0D_dense(self, f): + b = np.empty([]) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_dense(self, f): + b = np.empty((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_sparse(self, f): + b = sparse.coo_array((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_KD_dense(self, f, N): + b = np.empty((N - 1, N)) + with pytest.raises(ValueError, match="same number of rows as L"): + f.solve(b) + + def test_b_KD_sparse(self, f, N): + b = sparse.csc_array((N - 1, N)) + with pytest.raises(ValueError, match="same number of rows as L"): + f.solve(b) + + +@pytest.mark.parametrize("K", [0, 1, 3]) # arbitrary number of rhs +def test_empty_dense_input(K): + empty_A = sparse.csc_array((0, 0)) + empty_b = np.empty((0, K)) + x = cho_factor(empty_A).solve(empty_b) + assert_array_equal(x, empty_b, strict=True) + + +@pytest.mark.parametrize("K", [0, 1, 3]) # arbitrary number of rhs +def test_empty_sparse_input(K): + empty_A = sparse.csc_array((0, 0)) + empty_b = sparse.csc_array((0, K)) + x = cho_factor(empty_A).solve(empty_b) + assert_array_equal(x.toarray(), empty_b.toarray(), strict=True) + + +def test_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + with pytest.raises(CholmodError, match="not positive definite"): + cho_factor(zero_A).solve(np.zeros((N,))) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_dense(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = np.array([1], dtype=dtype) + x = cho_factor(singleton_A).solve(b) + assert_allclose(x, b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_sparse(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = sparse.coo_array([1], dtype=dtype) + x = cho_factor(singleton_A).solve(b) + assert_allclose(x.toarray(), b.toarray()) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_itype_1D(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + x = cho_factor(A).solve(b) + assert isinstance(x, sparse.coo_array) + assert x.coords[0].dtype == itype + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_itype_2D(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + K = 3 # arbitrary number of rhs + s = np.arange(1, N + 1, dtype=A.dtype) + data = np.array([i * s for i in range(1, K + 1)]).T + expect_x = sparse.csc_array(data, dtype=A.dtype) + b = A @ expect_x + x = cho_factor(A).solve(b) + assert isinstance(x, sparse.csc_array) + assert x.indptr.dtype == itype + assert x.indices.dtype == itype + + +def test_exactly_singular(davis_example_chol): + A = davis_example_chol.todok() + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A exactly singular + A[:, -1] = 0.0 + A[-1, :] = 0.0 + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + with pytest.raises(CholmodNotPositiveDefiniteError, match="not positive definite"): + cho_factor(A).solve(b) + + +def test_nearly_singular(davis_example_chol): + A = davis_example_chol.todok() + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A nearly singular + A[:, -1] = 0.0 + A[-1, :] = 0.0 + A[-1, -1] = 0.5 * np.finfo(A.dtype).eps + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + with pytest.warns(CholmodWarning, match="nearly singular"): + cho_factor(A).solve(b) + + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +test_As = [ + A + # for dtype in DTYPES # FIXME? single precision dtypes are not close + for dtype in [np.float64, np.complex128] + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True, dtype=dtype + ) +] + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize( + "order", + [ + None, + "default", + "best", + "natural", + "amd", + "metis", + "nesdis", + "colamd", + "postordered", + ], +) +@pytest.mark.parametrize("K", [0, 1, 3], ids=lambda k: f"K={k}") +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_solve(A, order, K, is_sparse): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + + # Build RHS + N = A.shape[0] + s = np.arange(1, N + 1, dtype=A.dtype) + + if K == 0: + data = s # (N,) + else: + data = np.array([i * s for i in range(1, K + 1)], dtype=A.dtype).T # (N, K) + + if is_sparse: + expect_x = sparse.coo_array(data, dtype=A.dtype) + else: + expect_x = np.asarray(data, dtype=A.dtype) + + # Solve the system + b = A @ expect_x + x = cho_factor(A, order=order).solve(b) + + # Compare + if is_sparse: + assert_allclose(x.toarray(), expect_x.toarray(), atol=atol) + else: + assert_allclose(x, expect_x, atol=atol) + + +# Test solve on "real-world" matrices +def _load_problem(name): + """Load a matrix and RHS from a Matrix Market file.""" + data_path = Path(__file__).parent.parent / "data" + matrix_file = data_path / f"{name}.mtx.gz" + + if not matrix_file.exists(): + raise FileNotFoundError(f"Matrix Market file {matrix_file} not found.") + + A = mmread(matrix_file, spmatrix=False).tocsc() + + # Possibly load RHS + rhs_file = data_path / f"{name}_rhs1.mtx.gz" + + if not rhs_file.exists(): + raise FileNotFoundError(f"Matrix Market file {rhs_file} not found.") + + b = mmread(rhs_file) + + return A, b + + +@pytest.mark.parametrize("problem", ["well1033", "illc1033", "well1850", "illc1850"]) +def test_solve_real(problem): + A, b = _load_problem(problem) + # Solve the normal equations A^T A x = A^T b + ATA = (A.T @ A).tocsc() + ATb = A.T @ b + atol = 1e-7 if A.dtype in (np.float64, np.complex128) else 1e-3 + expect_x = np.linalg.lstsq(A.toarray(), b)[0] + f = cho_factor(ATA) + assert_allclose(f.solve(ATb), expect_x, atol=atol) diff --git a/tests/test_cholmod/test_etree.py b/tests/test_cholmod/test_etree.py new file mode 100644 index 00000000..c6fd06b9 --- /dev/null +++ b/tests/test_cholmod/test_etree.py @@ -0,0 +1,175 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_etree.py +# Created: 2025-08-20 09:07 +# ============================================================================= + +"""Unit tests for the cholmod.etree function.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.cholmod import etree + +from ..helpers import generate_random_matrices, is_valid_permutation + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.fixture +def A_default(): + return sparse.csc_array([[1, 2], [3, 4]]) + + +def test_bad_kind(A_default): + with pytest.raises(ValueError, match="Unknown factorization kind"): + etree(A_default, kind="invalid") + + +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_nonsquare_input(kind): + A = sparse.csc_array((10, 8)) + with pytest.raises(ValueError, match="must be square"): + etree(A, kind=kind) + + +def test_empty_defaults(): + empty_A = sparse.csc_array((0, 0)) + parent, post = etree(empty_A, return_post=True) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(parent, empty_p, strict=True) + assert_array_equal(post, empty_p, strict=True) + + +def test_empty_row(): + empty_A = sparse.csc_array((0, 3)) + parent, post = etree(empty_A, kind="row", return_post=True) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(parent, empty_p, strict=True) + assert_array_equal(post, empty_p, strict=True) + + +def test_empty_col(): + empty_A = sparse.csc_array((3, 0)) + parent, post = etree(empty_A, kind="col", return_post=True) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(parent, empty_p, strict=True) + assert_array_equal(post, empty_p, strict=True) + + +def test_square_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + parent, post = etree(zero_A, return_post=True) + assert_array_equal(parent, np.full(N, -1, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(post, np.arange(N, dtype=zero_A.indptr.dtype), strict=True) + + +def test_nonsquare_zero_input_row(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + parent, post = etree(zero_A, return_post=True, kind="row") + assert_array_equal(parent, np.full(M, -1, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(post, np.arange(M, dtype=zero_A.indptr.dtype), strict=True) + + +def test_nonsquare_zero_input_col(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + parent, post = etree(zero_A, return_post=True, kind="col") + assert_array_equal(parent, np.full(N, -1, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(post, np.arange(N, dtype=zero_A.indptr.dtype), strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + itype = singleton_A.indptr.dtype + parent, post = etree(singleton_A, return_post=True) + assert_array_equal(parent, np.array([-1], dtype=itype), strict=True) + assert_array_equal(post, np.array([0], dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_etree_known(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + print("\nA_example:") + print(A.toarray()) + print() + parent, post = etree(A, return_post=True) + expect_parent = np.array([5, 2, 7, 5, 7, 6, 8, 9, 9, 10, -1], dtype=itype) + expect_post = np.array([1, 2, 4, 7, 0, 3, 5, 6, 8, 9, 10], dtype=itype) + assert_array_equal(parent, expect_parent, strict=True) + assert_array_equal(post, expect_post, strict=True) + + +def test_symlo(davis_example_chol): + # kind="lo" is the same as kind="sym" with A.T (lower triangular) + A = davis_example_chol + S_sym = etree(A, kind="sym", return_post=True) + S_lo = etree(A.T.tocsc(), kind="lo", return_post=True) + for x, y in zip(S_lo, S_sym): + if sparse.issparse(x): + assert_array_equal(x.toarray(), y.toarray(), strict=True) + else: + assert_array_equal(x, y, strict=True) + + +def test_rowcol(davis_example_chol): + A = davis_example_chol + S_col = etree(A, kind="col", return_post=True) + # Check that the factorization is correct + N = A.shape[0] + itype = A.indptr.dtype + expect_parent = np.array([3, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1], dtype=itype) + expect_post = np.arange(N, dtype=itype) + parent, post = S_col + assert_array_equal(parent, expect_parent, strict=True) + assert_array_equal(post, expect_post, strict=True) + # Compare with the factorization of the transpose + # A is symmetric (A = A.T), so A @ A.T == A.T @ A + S_row = etree(A.T.tocsc(), kind="row", return_post=True) + for x, y in zip(S_row, S_col): + if sparse.issparse(x): + assert_array_equal(x.toarray(), x.toarray(), strict=True) + else: + assert_array_equal(x, y, strict=True) + + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +pos_def_As = list( + generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True) +) +general_As = list(generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05)) + + +def _test_kind(A, kind): + N = A.shape[0] + parent, post = etree(A, kind=kind, return_post=True) + assert len(parent) == N + assert np.all(parent >= -1) + assert np.all(parent < N) + assert len(post) == N + assert is_valid_permutation(post, N) + + +@pytest.mark.parametrize("A", pos_def_As) +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_kind(A, kind): + _test_kind(A, kind) + + +@pytest.mark.parametrize("A", general_As) +@pytest.mark.parametrize("kind", ["row", "col"]) +def test_rowcol_kind(A, kind): + _test_kind(A, kind) diff --git a/tests/test_cholmod/test_ldl.py b/tests/test_cholmod/test_ldl.py new file mode 100644 index 00000000..7025fe33 --- /dev/null +++ b/tests/test_cholmod/test_ldl.py @@ -0,0 +1,140 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_ldl.py +# Created: 2025-08-14 14:12 +# ============================================================================= + +"""Unit tests for the cholmod.ldl function.""" + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import sparse + +from sksparse.cholmod import CholmodNotPositiveDefiniteError, ldl + +from ..helpers import generate_random_matrices, is_valid_permutation + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + L, D = ldl(empty_A) + assert_array_equal(L.toarray(), empty_A.toarray(), strict=True) + assert_array_equal(D.toarray(), empty_A.toarray(), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_zero_input(itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + with pytest.raises(CholmodNotPositiveDefiniteError, match="not positive definite"): + ldl(zero_A) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_matrix(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + L, D = ldl(singleton_A) + expect_L = expect_D = singleton_A.copy() + assert_array_equal(L.toarray(), expect_L.toarray(), strict=True) + assert_array_equal(D.toarray(), expect_D.toarray(), strict=True) + + +@pytest.mark.parametrize( + "A", + generate_random_matrices(N_trials=1, N_max=200, d_scale=0.05, pos_def_only=True), +) +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_itype(A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + L, _ = ldl(A) + assert L.indptr.dtype == itype + assert L.indices.dtype == itype + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_not_positive_definite(dtype): + # Create a simple non-positive definite matrix + A = sparse.csc_array([[0, 2], [2, 1]], dtype=dtype) + with pytest.raises(CholmodNotPositiveDefiniteError): + ldl(A) + + +test_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True, dtype=dtype + ) +] + + +@pytest.mark.parametrize("A", test_As[:1]) +def test_natural_ordering(A): + _L, _D, p = ldl(A, order="natural") + assert_array_equal(p, np.arange(A.shape[0])) + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize( + "order", + [ + None, + "default", + "best", + "natural", + "amd", + "metis", + "nesdis", + "colamd", + "postordered", + ], +) +def test_ordering(A, order): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + if order is None: + L, D = ldl(A, order=order) + assert_allclose((L @ D @ L.T.conj()).toarray(), A.toarray(), atol=atol) + else: + L, D, p = ldl(A, order=order) + assert is_valid_permutation(p) + PAPT = A[p][:, p] + assert_allclose((L @ D @ L.T.conj()).toarray(), PAPT.toarray(), atol=atol) + + +@pytest.mark.parametrize("A", test_As) +def test_lower(A): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + R, Dr = ldl(A, lower=False) + L, Dl = ldl(A) + assert_allclose(R.T.conj().toarray(), L.toarray(), atol=atol) + assert_allclose(Dr.toarray(), Dl.toarray(), atol=atol) + + +@pytest.mark.parametrize("beta", [0.0, 1.0, 3.4]) +@pytest.mark.parametrize("order", [None, "amd"]) +@pytest.mark.parametrize("A", test_As) +def test_beta(A, beta, order): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + N = A.shape[0] + + if order is None: + L, D = ldl(A, beta) + expect_LDL = (A + beta * sparse.eye_array(N)).toarray() + else: + L, D, p = ldl(A, beta, order=order) + expect_LDL = (A[p][:, p] + beta * sparse.eye_array(N)).toarray() + + assert_allclose((L @ D @ L.T.conj()).toarray(), expect_LDL, atol=atol) diff --git a/tests/test_cholmod/test_ldl_factor.py b/tests/test_cholmod/test_ldl_factor.py new file mode 100644 index 00000000..c09745c2 --- /dev/null +++ b/tests/test_cholmod/test_ldl_factor.py @@ -0,0 +1,153 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_ldl_factor.py +# Created: 2025-09-05 11:05 +# ============================================================================= + +"""Unit tests for the CholeskyFactor object.""" + +import warnings + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from scipy import sparse + +from sksparse.cholmod import ldl_factor + +from ..helpers import generate_random_matrices + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_convert_factor(davis_example_chol, dtype): + atol = 1e-15 if dtype in (np.float64, np.complex128) else 1e-6 + A = davis_example_chol.astype(dtype) + f = ldl_factor(A) + L = f.get_factor(kind="LL") + assert_allclose((L @ L.T.conj()).toarray(), A.toarray(), atol=atol) + + +@pytest.mark.parametrize("order", [None, "amd"]) +def test_view_vs_get(davis_example_chol, order): + A = davis_example_chol + f = ldl_factor(A) + LDv = f.factor + pv = f.perm + L, D = f.get_factor() + p = f.get_perm() + assert LDv is not L # different objects + assert LDv is not D + assert pv is not p + # Split the view into L and D + Dv = sparse.diags_array(LDv.diagonal()) + with pytest.raises(ValueError, match="read-only"): + LDv.setdiag(1.0) + LDv = LDv.copy() + LDv.setdiag(1.0) + assert_allclose(LDv.toarray(), L.toarray(), atol=1e-15) + assert_allclose(Dv.toarray(), D.toarray(), atol=1e-15) + assert_allclose(pv, p, atol=1e-15) + + +@pytest.fixture +def A_small(): + return sparse.csc_array( + np.array( + [[10, 0, 3, 0], + [0, 5, 0, -2], + [3, 0, 5, 0], + [0, -2, 0, 2]] + ), + dtype=np.float64, + ) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_determinant(A_small, dtype): + A = A_small.astype(dtype) + rtol = 1e-7 if A.dtype in (np.float64, np.complex128) else 1e-6 + + f = ldl_factor(A, lower=True) + + # In some versions of numpy, a warning is raised by slogdet for + # these complex types: (np.complex64, np.complex128). Make sure that is the + # warning that is raised, and not some other warning. + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") + expect_det = np.linalg.det(A.toarray()) + expect_sign, expect_logdet = np.linalg.slogdet(A.toarray()) + + if record: + assert record[0].category is RuntimeWarning + assert "divide by zero" in str(record[0].message) or "invalid value" in str( + record[0].message + ) + else: + pass + + assert_allclose(f.det(), expect_det, rtol=rtol, strict=True) + assert_allclose(f.slogdet(), (expect_sign, expect_logdet), rtol=rtol, strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_inv(A_small, dtype): + atol = 1e-12 if dtype in (np.float64, np.complex128) else 1e-3 + A = A_small.astype(dtype) + f = ldl_factor(A) + Ainv = f.inv() + I = np.eye(A.shape[0], dtype=A.dtype) + assert_allclose((A @ Ainv).toarray(), I, atol=atol, strict=True) + + +test_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True, dtype=dtype + ) +] + + +def _create_randomized_matrix(A): + """Create a new matrix with the same sparsity pattern as A but different values.""" + Bl = sparse.tril(A, -1).copy() + rng = np.random.default_rng(56) + if np.issubdtype(A.dtype, np.complexfloating): + Bl.data = rng.random(Bl.nnz, dtype=A.real.dtype) + 1j * rng.random( + Bl.nnz, dtype=A.real.dtype + ) + else: + Bl.data = rng.random(Bl.nnz, dtype=A.dtype) + B = Bl + Bl.T.conj() + # Ensure positive definiteness by adding to the diagonal + B.setdiag(A.diagonal()) + B += sparse.diags_array(np.full(B.shape[0], B.shape[0], dtype=B.dtype)) + return B.tocsc() + + +@pytest.mark.parametrize("copy", [False]) +@pytest.mark.parametrize("A", test_As) +def test_refactor(A, copy): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-3 + f = ldl_factor(A) + L, D = f.get_factor() + assert_allclose((L @ D @ L.T.conj()).toarray(), A.toarray(), atol=atol) + # Create a new matrix with the same sparsity pattern but different values + B = _create_randomized_matrix(A) + # Factor the new matrix with the same sparsity pattern + if copy: + # Use a copy of the factorization object to ensure that we are taking + # the relevant parameters from the underlying cholmod_common object. + g = f.copy() + g.factorize(B) + Lb, Db = g.get_factor() + else: + f.factorize(B) + Lb, Db = f.get_factor() + assert_allclose((Lb @ Db @ Lb.T.conj()).toarray(), B.toarray(), atol=atol) diff --git a/tests/test_cholmod/test_ldlsolve.py b/tests/test_cholmod/test_ldlsolve.py new file mode 100644 index 00000000..78dc5a82 --- /dev/null +++ b/tests/test_cholmod/test_ldlsolve.py @@ -0,0 +1,205 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_ldlsolve.py +# Created: 2025-08-15 09:03 +# ============================================================================= + +"""Unit tests for the cholmod.ldlsolve function.""" + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import linalg as la +from scipy import sparse + +from sksparse.cholmod import CholmodError, CholmodWarning, ldl_factor + +from ..helpers import generate_random_matrices + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +class TestBadBShapeDense: + @pytest.fixture(scope="class") + def N(self): + return 5 + + @pytest.fixture(scope="class") + def f(self, N): + """Create a pair of empty L and D matrices. Only shapes matter.""" + A = sparse.eye_array(N).tocsc() + return ldl_factor(A) + + def test_b_0D_dense(self, f): + b = np.empty([]) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_dense(self, f): + b = np.empty((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_sparse(self, f): + b = sparse.coo_array((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_KD_dense(self, f, N): + b = np.empty((N - 1, N)) + with pytest.raises(ValueError, match="same number of rows as L"): + f.solve(b) + + def test_b_KD_sparse(self, f, N): + b = sparse.csc_array((N - 1, N)) + with pytest.raises(ValueError, match="same number of rows as L"): + f.solve(b) + + +@pytest.mark.parametrize("K", [0, 1, 3]) # arbitrary number of rhs +def test_empty_dense_input(K): + empty_A = sparse.csc_array((0, 0)) + empty_b = np.empty((0, K)) + x = ldl_factor(empty_A).solve(empty_b) + assert_array_equal(x, empty_b, strict=True) + + +@pytest.mark.parametrize("K", [0, 1, 3]) # arbitrary number of rhs +def test_empty_sparse_input(K): + empty_A = sparse.csc_array((0, 0)) + empty_b = sparse.csc_array((0, K)) + x = ldl_factor(empty_A).solve(empty_b) + assert_array_equal(x.toarray(), empty_b.toarray(), strict=True) + + +def test_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + with pytest.raises(CholmodError, match="not positive definite"): + ldl_factor(zero_A).solve(np.zeros((N,))) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_dense(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = np.array([1], dtype=dtype) + x = ldl_factor(singleton_A).solve(b) + assert_allclose(x, b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_sparse(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = sparse.coo_array([1], dtype=dtype) + x = ldl_factor(singleton_A).solve(b) + assert_allclose(x.toarray(), b.toarray()) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_itype_1D(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + x = ldl_factor(A).solve(b) + assert isinstance(x, sparse.coo_array) + assert x.coords[0].dtype == itype + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_itype_2D(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + K = 3 # arbitrary number of rhs + s = np.arange(1, N + 1, dtype=A.dtype) + data = np.array([i * s for i in range(1, K + 1)]).T + expect_x = sparse.csc_array(data, dtype=A.dtype) + b = A @ expect_x + x = ldl_factor(A).solve(b) + assert isinstance(x, sparse.csc_array) + assert x.indptr.dtype == itype + assert x.indices.dtype == itype + + +# NOTE *exactly* singular matrices are not positive definite, so they fail in +# the ldl() function. +def test_nearly_singular(davis_example_chol): + A = davis_example_chol.todok() + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A nearly singular + A[:, -1] = 0.0 + A[-1, :] = 0.0 + A[-1, -1] = 0.5 * np.finfo(A.dtype).eps + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + with pytest.warns(CholmodWarning, match="nearly singular"): + ldl_factor(A).solve(b) + + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +test_As = [ + A + # for dtype in DTYPES # FIXME? single precision dtypes are not close + for dtype in [np.float64, np.complex128] + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True, dtype=dtype + ) +] + + +@pytest.fixture(params=test_As) +def Am(request): + return request.param + + +@pytest.fixture(params=[None, "amd"], ids=lambda x: f"order={x}") +def ldl_decomp(Am, request): + return Am, ldl_factor(Am, order=request.param) + + +@pytest.mark.parametrize("K", [0, 1, 3], ids=lambda k: f"K={k}") +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_ldlsolve(ldl_decomp, K, is_sparse): + A, f = ldl_decomp + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-5 + + # Build RHS + N = A.shape[0] + s = np.arange(1, N + 1, dtype=A.dtype) + + if K == 0: + data = s # (N,) + else: + data = np.array([i * s for i in range(1, K + 1)], dtype=A.dtype).T # (N, K) + + if is_sparse: + expect_x = sparse.coo_array(data, dtype=A.dtype) + else: + expect_x = np.asarray(data, dtype=A.dtype) + + # Solve the system + b = A @ expect_x + x = f.solve(b) + + # Compare + if is_sparse: + assert_allclose(x.toarray(), expect_x.toarray(), atol=atol) + else: + assert_allclose(x, expect_x, atol=atol) diff --git a/tests/test_cholmod/test_ldlupdown.py b/tests/test_cholmod/test_ldlupdown.py new file mode 100644 index 00000000..05615874 --- /dev/null +++ b/tests/test_cholmod/test_ldlupdown.py @@ -0,0 +1,222 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_ldlupdown.py +# Created: 2025-08-18 10:35 +# ============================================================================= + +"""Unit tests for the ldlupdate and ldlrowmod functions in sksparse.cholmod.""" + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from scipy import sparse +from scipy.sparse.linalg import LaplacianNd + +from sksparse.cholmod import ldl_factor + +Ng = 15 # arbitrary problem size A = (Ng**2, Ng**2) + + +@pytest.fixture +def A(): + # Create (negative) Laplacian matrix that is symmetric positive definite + G = LaplacianNd((Ng, Ng)) + A = -G.tosparse().tocsc().astype(float) + A.setdiag(A.diagonal() + 1) # make it positive definite + return A + + +@pytest.fixture +def expect_x(): + return sparse.dok_array(np.arange(Ng * Ng)) + + +@pytest.fixture +def b(A, expect_x): + return A @ expect_x + + +@pytest.fixture(params=[None, "natural", "default", "amd"]) +def f(A, request): + return ldl_factor(A, order=request.param) + + +def _create_update_matrix(L): + # See CHOLMOD/MATLAB/Test/test0.m for the original example + N = L.shape[0] + rng = np.random.default_rng(5656) # seed=5656 no failure for order=None + k = rng.integers(1, N // 4) + cols = rng.choice(N, size=k, replace=False) # random columns to update + + # Take existing pattern from L + C = L[:, cols].copy() + C.data = rng.normal(size=C.nnz).astype(L.dtype) # random values for the update + + # Add one entry to make sure L gets some fill-in + row = rng.integers(0, N) + C = C.todok() + C[row, 0] = 1.0 + + return C.tocsc() + + +def test_ldlupdown(A, f): + L, D = f.get_factor() + p = f.get_perm() + + # Verify that the factorization is correct + S = A[p][:, p] + assert_allclose((L @ D @ L.T).toarray(), S.toarray(), atol=1e-12) + + # Compute a rank-k update of LDL.T factorization + Cp = _create_update_matrix(L) + C = Cp[np.argsort(p), :] # unpermute C into A space + + # NOTE this test fails on order=None and 'natural' for some random seeds. + # We get an occasional infinite loop or actual failure. The same failure + # occurs with ldlupdate function, so issue is not in the object wrapper. + # Probably something in CHOLMOD itself. + + # Update the factorization + f.update(C) + Lc, Dc = f.get_factor() + + # Verify that the updated factorization is correct + Sc = S + Cp @ Cp.T + assert_allclose((Lc @ Dc @ Lc.T).toarray(), Sc.toarray(), atol=1e-12) + + # Downdate back to the original factorization + f.downdate(C) + Ld, Dd = f.get_factor() + assert_allclose((Ld @ Dd @ Ld.T).toarray(), S.toarray(), atol=1e-12) + + +def test_resymbol(A, f): + L, D = f.get_factor() + p = f.get_perm() + + # Verify that the factorization is correct + S = A[p][:, p] + assert_allclose((L @ D @ L.T).toarray(), S.toarray(), atol=1e-12) + + # Compute a rank-k update of LDL.T factorization + Cp = _create_update_matrix(L) + C = Cp[np.argsort(p), :] # unpermute C into A space + + # Update the factorization + f.update(C) + Lc, Dc = f.get_factor() + + # Verify that the updated factorization is correct + Sc = S + Cp @ Cp.T + assert_allclose((Lc @ Dc @ Lc.T).toarray(), Sc.toarray(), atol=1e-12) + + # Downdate back to the original factorization + f.downdate(C) + Ld, Dd = f.get_factor() + assert_allclose((Ld @ Dd @ Ld.T).toarray(), S.toarray(), atol=1e-12) + + print("\nBefore resymbol:") + print(f"{ L.nnz=}") + print(f"{Ld.nnz=}") + + # Test resymbol + f.resymbol(S) + Lr = f.get_factor()[0] + + print("After resymbol:") + print(f"{Lr.nnz=}") + + # Compare with the original factorization + assert_allclose(Lr.toarray(), L.toarray(), atol=1e-12) + + +def test_resymbol_perm(A, f): + L, D = f.get_factor() + p = f.get_perm() + S = A[p][:, p] + Cp = _create_update_matrix(L) + C = Cp[np.argsort(p), :] # unpermute C into A space + f.update(C) + f.downdate(C) + g = f.copy() + assert g is not f + f.resymbol(S, is_permuted=True) + g.resymbol(A, is_permuted=False) + # Both factorizations should be identical + Lf, Df = f.get_factor() + Lg, Dg = g.get_factor() + assert_allclose(Lf.toarray(), Lg.toarray(), atol=1e-15) + assert_allclose(Df.toarray(), Dg.toarray(), atol=1e-15) + + +def test_ldlrowmod(A, expect_x, b, f): + L, D = f.get_factor() + p = f.get_perm() + S = A[p][:, p] + + # ------------------------------------------------------------------------- + # Delete row 3 of A + # ------------------------------------------------------------------------- + # Row 3 corresponds to the p_inv(3) in S and LDL + # Invert the permutation + p_inv = np.argsort(p) + + k = 3 + + # Store for later + A_col_k = A[:, [k]].copy() # column k of A + + pk = p_inv[k] # index in S and LDL + I = sparse.eye_array(*A.shape).tocsc() + + Ak = A.copy() + Ak[k, :] = I[k, :] + Ak[:, [k]] = I[:, [k]] + + # Remove row and column k from the factorization + f.rowdel(pk) + Lk, Dk = f.get_factor() + + # Remove from the original matrix + Sk = S.copy() + Sk[pk, :] = I[pk, :] + Sk[:, [pk]] = I[:, [pk]] + + assert_allclose((Lk @ Dk @ Lk.T).toarray(), Sk.toarray(), atol=1e-12) + + # Solve the modified system + x = f.solve(b) + xs = sparse.linalg.spsolve(Ak, b.tocoo()) + + assert_allclose((Ak @ x).toarray(), b.toarray(), atol=1e-12) + assert_allclose(x.toarray(), xs, atol=1e-12) + + # ------------------------------------------------------------------------- + # Add row 3 back to the factorization + # ------------------------------------------------------------------------- + W = A_col_k + Aa = Ak.copy() + Aa[k, :] = W.T + Aa[:, [k]] = W + + C = W[p].tocsc() # permuted version + Sa = Sk.copy() + Sa[pk, :] = C.T + Sa[:, [pk]] = C + + f.rowadd(pk, C) + La, Da = f.get_factor() + + assert_allclose((La @ Da @ La.T).toarray(), Sa.toarray(), atol=1e-12) + + # Solve the modified system + x = f.solve(b) + xs = sparse.linalg.spsolve(Aa, b.tocoo()) + + assert_allclose((Aa @ x).toarray(), b.toarray(), atol=1e-12) + assert_allclose(x.toarray(), xs, atol=1e-12) diff --git a/tests/test_cholmod/test_metis.py b/tests/test_cholmod/test_metis.py new file mode 100644 index 00000000..7dd61cb1 --- /dev/null +++ b/tests/test_cholmod/test_metis.py @@ -0,0 +1,147 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_metis.py +# Created: 2025-08-21 13:27 +# ============================================================================= + +"""Unit tests for the cholmod.metis function.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.cholmod import metis + +from ..helpers import generate_random_matrices, is_valid_permutation + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.fixture +def A_default(): + return sparse.csc_array([[1, 2], [3, 4]]) + + +def test_bad_kind(A_default): + with pytest.raises(ValueError, match="Unknown factorization kind"): + metis(A_default, kind="invalid") + + +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_nonsquare_input(kind): + A = sparse.csc_array((10, 8)) + with pytest.raises(ValueError, match="must be square"): + metis(A, kind=kind) + + +def test_empty_defaults(): + empty_A = sparse.csc_array((0, 0)) + p = metis(empty_A) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(p, empty_p, strict=True) + + +def test_empty_row(): + empty_A = sparse.csc_array((0, 3)) + p = metis(empty_A, kind="row") + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(p, empty_p, strict=True) + + +def test_empty_col(): + empty_A = sparse.csc_array((3, 0)) + p = metis(empty_A, kind="col") + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(p, empty_p, strict=True) + + +def test_square_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + p = metis(zero_A) + expect_p = np.arange(N, dtype=zero_A.indptr.dtype) + assert_array_equal(p, expect_p, strict=True) + + +def test_nonsquare_zero_input_row(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + p = metis(zero_A, kind="row") + expect_p = np.arange(M, dtype=zero_A.indptr.dtype) + assert_array_equal(p, expect_p, strict=True) + + +def test_nonsquare_zero_input_col(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + p = metis(zero_A, kind="col") + expect_p = np.arange(N, dtype=zero_A.indptr.dtype) + assert_array_equal(p, expect_p, strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + itype = singleton_A.indptr.dtype + p = metis(singleton_A) + assert_array_equal(p, np.array([0], dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_metis_known(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + print("\nA_example:") + print(A.toarray()) + print() + p = metis(A) + # Computed with MATLAB CHOLMOD metis function p = metis(A, 'sym') + expect_p = np.array([8, 3, 6, 0, 5, 2, 4, 7, 1, 9, 10], dtype=itype) + assert_array_equal(p, expect_p, strict=True) + + +def test_rowcol(davis_example_chol): + A = davis_example_chol + s_col = metis(A, kind="col") + # Check that the factorization is correct + itype = A.indptr.dtype + # Computed with MATLAB CHOLMOD metis function p = metis(A, 'col') + expect_p = np.array([6, 1, 4, 2, 7, 10, 8, 3, 0, 5, 9], dtype=itype) + assert_array_equal(s_col, expect_p, strict=True) + # Compare with the factorization of the transpose + # A is symmetric (A = A.T), so A @ A.T == A.T @ A + s_row = metis(A.T.tocsc(), kind="row") + assert_array_equal(s_row, s_col, strict=True) + + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +pos_def_As = list( + generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True) +) +general_As = list(generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05)) + + +def _test_kind(A, kind): + N = A.shape[0] + p = metis(A, kind=kind) + assert is_valid_permutation(p, N) + + +@pytest.mark.parametrize("A", pos_def_As) +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_kind(A, kind): + _test_kind(A, kind) + + +@pytest.mark.parametrize("A", general_As) +@pytest.mark.parametrize("kind", ["row", "col"]) +def test_rowcol_kind(A, kind): + _test_kind(A, kind) diff --git a/tests/test_cholmod/test_nesdis.py b/tests/test_cholmod/test_nesdis.py new file mode 100644 index 00000000..63f6beb0 --- /dev/null +++ b/tests/test_cholmod/test_nesdis.py @@ -0,0 +1,220 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_nesdis.py +# Created: 2025-08-21 12:31 +# ============================================================================= + +"""Unit tests for the cholmod.nesdis function.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.cholmod import nesdis + +from ..helpers import generate_random_matrices, is_valid_permutation + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.fixture +def A_default(): + return sparse.csc_array([[1, 2], [3, 4]]) + + +def test_bad_kind(A_default): + with pytest.raises(ValueError, match="Unknown factorization kind"): + nesdis(A_default, kind="invalid") + + +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_nonsquare_input(kind): + A = sparse.csc_array((10, 8)) + with pytest.raises(ValueError, match="must be square"): + nesdis(A, kind=kind) + + +def test_empty_defaults(): + empty_A = sparse.csc_array((0, 0)) + itype = empty_A.indptr.dtype + p, st = nesdis(empty_A, return_separator=True) + expect_p = np.array([], dtype=itype) + expect_cp = np.array([-1], dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_p, strict=True) + + +def test_empty_row(): + empty_A = sparse.csc_array((0, 3)) + itype = empty_A.indptr.dtype + p, st = nesdis(empty_A, kind="row", return_separator=True) + expect_p = np.array([], dtype=itype) + expect_cp = np.array([-1], dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_p, strict=True) + + +def test_empty_col(): + empty_A = sparse.csc_array((3, 0)) + itype = empty_A.indptr.dtype + p, st = nesdis(empty_A, kind="col", return_separator=True) + expect_p = np.array([], dtype=itype) + expect_cp = np.array([-1], dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_p, strict=True) + + +def test_square_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + itype = zero_A.indptr.dtype + p, st = nesdis(zero_A, return_separator=True) + expect_p = np.arange(N, dtype=itype) + expect_cp = np.array([-1], dtype=itype) + expect_cmember = np.zeros(N, dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_cmember, strict=True) + + +def test_nonsquare_zero_input_row(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + itype = zero_A.indptr.dtype + p, st = nesdis(zero_A, kind="row", return_separator=True) + expect_p = np.arange(M, dtype=itype) + expect_cp = np.array([-1], dtype=itype) + expect_cmember = np.zeros(M, dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_cmember, strict=True) + + +def test_nonsquare_zero_input_col(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + itype = zero_A.indptr.dtype + p, st = nesdis(zero_A, kind="col", return_separator=True) + expect_p = np.arange(N, dtype=itype) + expect_cp = np.array([-1], dtype=itype) + expect_cmember = np.zeros(N, dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_cmember, strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + itype = singleton_A.indptr.dtype + p, st = nesdis(singleton_A, return_separator=True) + expect_p = np.array([0], dtype=itype) + expect_cp = np.array([-1], dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_p, strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_bisect_known(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + print("\nA_example:") + print(A.toarray()) + print() + p, st = nesdis(A, return_separator=True) + # Computed with MATLAB CHOLMOD nesdis function p = nesdis(A, 'sym') + expect_p = np.array([1, 4, 6, 8, 0, 3, 5, 2, 9, 10, 7], dtype=itype) + expect_cp = np.array([-1], dtype=itype) + expect_cmember = np.zeros(A.shape[0], dtype=itype) + assert_array_equal(p, expect_p, strict=True) + assert_array_equal(st.cp, expect_cp, strict=True) + assert_array_equal(st.cmember, expect_cmember, strict=True) + + +def test_rowcol(davis_example_chol): + A = davis_example_chol + s_col = nesdis(A, kind="col", return_separator=True) + p_col, st_col = s_col + # Check that the factorization is correct + itype = A.indptr.dtype + # Computed with MATLAB CHOLMOD nesdis function p = nesdis(A, 'col') + expect_p = np.array([8, 3, 5, 0, 6, 10, 9, 7, 4, 2, 1], dtype=itype) + expect_cp = np.array([-1], dtype=itype) + expect_cmember = np.zeros(A.shape[0], dtype=itype) + assert_array_equal(p_col, expect_p, strict=True) + assert_array_equal(st_col.cp, expect_cp, strict=True) + assert_array_equal(st_col.cmember, expect_cmember, strict=True) + # Compare with the factorization of the transpose + # A is symmetric (A = A.T), so A @ A.T == A.T @ A + p_row, st_row = nesdis(A.T.tocsc(), kind="row", return_separator=True) + assert_array_equal(p_row, p_col, strict=True) + assert_array_equal(st_row.cp, st_col.cp, strict=True) + assert_array_equal(st_row.cmember, st_col.cmember, strict=True) + + +# TODO test options nd_small, etc. + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +pos_def_As = list( + generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True) +) +general_As = list(generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05)) + + +def _test_kind(A, kind): + N = A.shape[0] + p, st = nesdis(A, kind=kind, return_separator=True) + assert is_valid_permutation(p, N) + assert len(st.cmember) == N + assert np.all(st.cmember >= 0) + assert np.all(st.cmember < N) + assert len(st.cp) == st.cmember.max() + 1 + + +@pytest.mark.parametrize("A", pos_def_As) +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_kind(A, kind): + _test_kind(A, kind) + + +@pytest.mark.parametrize("A", general_As) +@pytest.mark.parametrize("kind", ["row", "col"]) +def test_rowcol_kind(A, kind): + _test_kind(A, kind) + + +# ----------------------------------------------------------------------------- +# Test pruning of the separator tree +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize("A", general_As) +def test_prune_septree(A): + N = A.shape[0] + p, st = nesdis(A, return_separator=True) + assert is_valid_permutation(p, N) + assert len(st.cmember) == N + assert np.all(st.cmember >= 0) + assert np.all(st.cmember < N) + assert len(st.cp) == st.cmember.max() + 1 + + st_pruned = st.prune() + assert st_pruned is not st + assert len(st_pruned.cmember) == N + assert np.all(st_pruned.cmember >= 0) + assert np.all(st_pruned.cmember < N) + assert len(st_pruned.cp) == st_pruned.cmember.max() + 1 + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_cholmod/test_symbfact.py b/tests/test_cholmod/test_symbfact.py new file mode 100644 index 00000000..e2b367fe --- /dev/null +++ b/tests/test_cholmod/test_symbfact.py @@ -0,0 +1,214 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_symbfact.py +# Created: 2025-08-19 12:36 +# ============================================================================= + +"""Unit tests for the cholmod.symbfact function.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.cholmod import symbfact + +from ..helpers import generate_random_matrices, is_valid_permutation + +DTYPES = [np.float32, np.float64, np.complex64, np.complex128] + + +@pytest.fixture +def A_default(): + return sparse.csc_array([[1, 2], [3, 4]]) + + +def test_bad_kind(A_default): + with pytest.raises(ValueError, match="Unknown factorization kind"): + symbfact(A_default, kind="invalid") + + +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_nonsquare_input(kind): + A = sparse.csc_array((10, 8)) + with pytest.raises(ValueError, match="must be square"): + symbfact(A, kind=kind) + + +def test_empty_defaults(): + empty_A = sparse.csc_array((0, 0)) + count, h, parent, post, L = symbfact(empty_A, return_factor=True) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(count, empty_p, strict=True) + assert h == 0 + assert_array_equal(parent, empty_p, strict=True) + assert_array_equal(post, empty_p, strict=True) + assert_array_equal(L.toarray(), empty_A.toarray(), strict=True) + + +def test_empty_row(): + empty_A = sparse.csc_array((0, 3)) + count, h, parent, post, L = symbfact(empty_A, kind="row", return_factor=True) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(count, empty_p, strict=True) + assert h == 0 + assert_array_equal(parent, empty_p, strict=True) + assert_array_equal(post, empty_p, strict=True) + assert_array_equal(L.toarray(), empty_A.toarray(), strict=True) + + +def test_empty_col(): + empty_A = sparse.csc_array((3, 0)) + count, h, parent, post, L = symbfact(empty_A, kind="col", return_factor=True) + empty_p = np.array([], dtype=empty_A.indptr.dtype) + assert_array_equal(count, empty_p, strict=True) + assert h == 0 + assert_array_equal(parent, empty_p, strict=True) + assert_array_equal(post, empty_p, strict=True) + assert_array_equal(L.toarray(), empty_A.toarray(), strict=True) + + +def test_square_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + count, h, parent, post, L = symbfact(zero_A, return_factor=True) + assert_array_equal(count, np.zeros(N, dtype=zero_A.indptr.dtype), strict=True) + assert h == 1 + assert_array_equal(parent, np.full(N, -1, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(post, np.arange(N, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(L.toarray(), np.eye(N, dtype=zero_A.dtype), strict=True) + + +def test_nonsquare_zero_input_row(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + count, h, parent, post, L = symbfact(zero_A, return_factor=True, kind="row") + assert_array_equal(count, np.zeros(M, dtype=zero_A.indptr.dtype), strict=True) + assert h == 1 + assert_array_equal(parent, np.full(M, -1, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(post, np.arange(M, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(L.toarray(), np.eye(M, dtype=zero_A.dtype), strict=True) + + +def test_nonsquare_zero_input_col(): + M, N = 10, 7 + zero_A = sparse.csc_array((M, N)) + count, h, parent, post, L = symbfact(zero_A, return_factor=True, kind="col") + assert_array_equal(count, np.zeros(N, dtype=zero_A.indptr.dtype), strict=True) + assert h == 1 + assert_array_equal(parent, np.full(N, -1, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(post, np.arange(N, dtype=zero_A.indptr.dtype), strict=True) + assert_array_equal(L.toarray(), np.eye(N, dtype=zero_A.dtype), strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + itype = singleton_A.indptr.dtype + count, h, parent, post, L = symbfact(singleton_A, return_factor=True) + assert_array_equal(count, np.array([1], dtype=itype), strict=True) + assert h == 1 + assert_array_equal(parent, np.array([-1], dtype=itype), strict=True) + assert_array_equal(post, np.array([0], dtype=itype), strict=True) + assert_array_equal(L.toarray(), np.array([[1]], dtype=bool), strict=True) + + +@pytest.mark.parametrize("itype", [np.int32, np.int64]) +def test_symbfact_known(davis_example_chol, itype): + A = davis_example_chol + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + print("\nA_example:") + print(A.toarray()) + print() + count, h, parent, post, L = symbfact(A, lower=True, return_factor=True) + expect_count = np.array([3, 3, 4, 3, 3, 4, 4, 3, 3, 2, 1], dtype=itype) + expect_h = 6 + expect_parent = np.array([5, 2, 7, 5, 7, 6, 8, 9, 9, 10, -1], dtype=itype) + expect_post = np.array([1, 2, 4, 7, 0, 3, 5, 6, 8, 9, 10], dtype=itype) + # Check results + assert_array_equal(count, expect_count, strict=True) + assert h == expect_h + assert_array_equal(parent, expect_parent, strict=True) + assert_array_equal(post, expect_post, strict=True) + # Check self-consistency of L + colcount = L.sum(axis=0) + indptr = np.cumulative_sum(colcount, include_initial=True, dtype=itype) + assert_array_equal(colcount, expect_count) + assert_array_equal(L.indptr, indptr, strict=True) + + +def test_symlo(davis_example_chol): + # kind="lo" is the same as kind="sym" with A.T (lower triangular) + A = davis_example_chol + S_sym = symbfact(A, kind="sym", return_factor=True) + S_lo = symbfact(A.T.tocsc(), kind="lo", return_factor=True) + for x, y in zip(S_lo, S_sym): + if sparse.issparse(x): + assert_array_equal(x.toarray(), y.toarray(), strict=True) + else: + assert_array_equal(x, y, strict=True) + + +def test_rowcol(davis_example_chol): + A = davis_example_chol + S_col = symbfact(A, kind="col", return_factor=True) + # Check that the factorization is correct + N = A.shape[0] + itype = A.indptr.dtype + expect_count = np.array([7, 6, 8, 8, 7, 6, 5, 4, 3, 2, 1], dtype=itype) + expect_h = 10 + expect_parent = np.array([3, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1], dtype=itype) + expect_post = np.arange(N, dtype=itype) + count, h, parent, post, L = S_col + assert_array_equal(count, expect_count, strict=True) + assert h == expect_h + assert_array_equal(parent, expect_parent, strict=True) + assert_array_equal(post, expect_post, strict=True) + # Compare with the factorization of the transpose + # A is symmetric (A = A.T), so A @ A.T == A.T @ A + S_row = symbfact(A.T.tocsc(), kind="row", return_factor=True) + for x, y in zip(S_row, S_col): + if sparse.issparse(x): + assert_array_equal(x.toarray(), x.toarray(), strict=True) + else: + assert_array_equal(x, y, strict=True) + + +# ----------------------------------------------------------------------------- +# Test many random matrices of various dtypes +# ----------------------------------------------------------------------------- +pos_def_As = list( + generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05, pos_def_only=True) +) +general_As = list(generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05)) + + +def _test_kind(A, kind): + N = A.shape[0] + count, h, parent, post, L = symbfact(A, kind=kind, return_factor=True) + assert len(count) == N + assert np.all(count >= 0) + assert np.all(count <= N) + assert h >= 1 + assert len(parent) == N + assert np.all(parent >= -1) + assert np.all(parent < N) + assert len(post) == N + assert is_valid_permutation(post, N) + + +@pytest.mark.parametrize("A", pos_def_As) +@pytest.mark.parametrize("kind", [None, "sym"]) +def test_kind(A, kind): + _test_kind(A, kind) + + +@pytest.mark.parametrize("A", general_As) +@pytest.mark.parametrize("kind", ["row", "col"]) +def test_rowcol_kind(A, kind): + _test_kind(A, kind) diff --git a/tests/test_colamd.py b/tests/test_colamd.py new file mode 100644 index 00000000..3ad8d13b --- /dev/null +++ b/tests/test_colamd.py @@ -0,0 +1,271 @@ +# Test cases for the sksparse.colamd module. +# +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_colamd.py +# Created: 2025-07-31 11:42 +# ============================================================================= + +"""Test cases for the sksparse.colamd module.""" + +# import matplotlib.pyplot as plt # DEBUG only +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.colamd import COLAMDStats, colamd, colamd_get_defaults, symamd + +from .helpers import generate_random_matrices, is_valid_permutation + + +class _BasicInputMixin: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_empty_input(self, itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + assert_array_equal( + self.colamd_func(empty_A), np.array([], dtype=itype), strict=True + ) + + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_zero_input(self, itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + assert_array_equal( + self.colamd_func(zero_A), np.arange(N, dtype=itype), strict=True + ) + + def test_singleton_matrix(self): + singleton_A = sparse.csc_array([[1]]) + assert_array_equal( + self.colamd_func(singleton_A), np.array([0], dtype=np.int32), strict=True + ) + + +class TestColamdInput(_BasicInputMixin): + colamd_func = staticmethod(colamd) + + def test_2D_row_input(self): + N = 10 + A = sparse.csc_array(np.arange(N)[np.newaxis, :]) # (1, N) + q = self.colamd_func(A) + assert_array_equal(q, np.arange(N, dtype=np.int32), strict=True) + + def test_2D_col_input(self): + N = 10 + A = sparse.csc_array(np.arange(N)[:, np.newaxis]) # (N, 1) + q = self.colamd_func(A) + assert_array_equal(q, np.zeros(1, dtype=np.int32), strict=True) + + +class TestSymamdInput(_BasicInputMixin): + colamd_func = staticmethod(symamd) + + +class _RandomInputMixin: + @pytest.mark.parametrize("itype", [np.int32, np.int64]) + def test_itype(self, A, itype): + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + q = self.colamd_func(A) + assert q.dtype == itype + assert q.shape == (A.shape[0],) + assert is_valid_permutation(q) + + @pytest.mark.parametrize("aggressive", [True, False]) + def test_aggressive(self, A, aggressive): + q = self.colamd_func(A, aggressive=aggressive) + assert is_valid_permutation(q) + + +@pytest.mark.parametrize( + "A", + list(generate_random_matrices(N_trials=100, N_max=200, d_scale=0.05)), +) +class TestColamdRandomInput(_RandomInputMixin): + colamd_func = staticmethod(colamd) + + +@pytest.mark.parametrize( + "A", + list( + generate_random_matrices( + N_trials=100, N_max=200, d_scale=0.05, square_only=True + ) + ), +) +class TestSymamdRandomInput(_RandomInputMixin): + colamd_func = staticmethod(symamd) + + +COLAMD_DEFAULT_DENSE = 10 # NOTE depends on the default in colamd.c +DENSE_THRESHOLDS = [None, 5, 2] + + +@pytest.mark.parametrize("row_or_col", ["row", "col"]) +@pytest.mark.parametrize("dense_thresh", DENSE_THRESHOLDS) +def test_colamd_with_dense(dense_thresh, row_or_col): + M = 1000 # arbitrary size + N = 800 + rng = np.random.default_rng(56) + A = sparse.random_array((M, N), density=0.001, format="lil", rng=rng) + + max_N_rowcols, max_N_elems = (M, N) if row_or_col == "row" else (N, M) + + # Create a known number of dense rows/cols above the threshold + # thresh is actually dense_thresh * sqrt(N) == dense_thresh * 10 + # max(A[i] for i in range(N)) is ~ 5 for N = 1000, density = 0.001 + thresh = int( + (dense_thresh if dense_thresh is not None else COLAMD_DEFAULT_DENSE) + * np.sqrt(max_N_elems) + ) + + N_dense = 10 # arbitrary choice for number of dense rows/columns + N_elems = min(2 * thresh, max_N_elems) # arbitrary choice for enough elements + + dense_idx = rng.choice(max_N_rowcols, size=N_dense, replace=False) + # TODO use a different set of indices for each row/col (see test_ccolamd.py) + other_idx = rng.choice(max_N_elems, size=N_elems, replace=False) + + for i in dense_idx: + if row_or_col == "row": + A[i, other_idx] = rng.random(size=len(other_idx)) + else: + A[other_idx, i] = rng.random(size=len(other_idx)) + + A = A.tocsc() + + kwargs = {f"dense_{row_or_col}_thresh": dense_thresh, "return_info": True} + q, stats = colamd(A, **kwargs) + + assert is_valid_permutation(q) + + # DEBUG: plot the matrix before and after permutation + # fig, axs = plt.subplots(num=1, ncols=2, clear=True) + # axs[0].spy(A, markersize=1) + # axs[1].spy(A[:, q], markersize=1) + # plt.show() + + # Expect dense cols at the end of the permutation, but maybe not in order + # NOTE *empty* columns are also moved to the end of the matrix, + # so we need to check the stats.N_cols_ignored value + if row_or_col == "col": + assert_array_equal( + np.sort(q[-stats.N_cols_ignored : -(stats.N_cols_ignored - N_dense)]), + np.sort(dense_idx), + ) + + +@pytest.mark.parametrize("dense_thresh", DENSE_THRESHOLDS) +def test_symamd_with_dense(dense_thresh): + N = 1000 # arbitrary size + rng = np.random.default_rng(56) + A = sparse.random_array((N, N), density=0.001, format="lil", rng=rng) + + # Create a known number of dense rows/cols above the threshold + # thresh is actually dense_thresh * sqrt(N) == dense_thresh * 10 + # max(A[i] for i in range(N)) is ~ 5 for N = 1000, density = 0.001 + thresh = int( + (dense_thresh if dense_thresh is not None else COLAMD_DEFAULT_DENSE) + * np.sqrt(N) + ) + + N_dense = 10 # arbitrary choice for number of dense rows/columns + N_elems = min(2 * thresh, N) # arbitrary choice for enough elements + + dense_idx = rng.choice(N, size=N_dense, replace=False) + other_idx = rng.choice(N, size=N_elems, replace=False) + + for i in dense_idx: + A[i, other_idx] = rng.random(size=len(other_idx)) + + A = A + A.T # make it symmetric + A = A.tocsc() + + q, stats = symamd(A, return_info=True) + + assert is_valid_permutation(q) + + # # DEBUG: plot the matrix before and after permutation + # fig, axs = plt.subplots(num=1, ncols=2, clear=True) + # axs[0].spy(A, markersize=1) + # axs[1].spy(A[q][:, q], markersize=1) + # plt.show() + + # NOTE I am not sure what the expected behavior is here for symamd (vs + # colamd) The test *almost* passes, but there are some empty rows/columns + # interspersed with the dense rows/columns at the end of the matrix. There + # may not be a deterministic order of the dense/empty rows/columns that + # applies to every matrix, so we cannot make a strong assertion here. + + # Check that the same number of rows/columns are ignored. + assert stats.N_rows_ignored == stats.N_cols_ignored + + # Expect dense cols at the end of the permutation, but maybe not in order. + # *empty* columns are also moved to the end of the matrix, so we need to + # check the stats.N_cols_ignored value + # assert_array_equal( + # np.sort(q[-stats.N_cols_ignored:-(stats.N_cols_ignored - N_dense)]), + # np.sort(dense_idx) + # ) + + +def test_info_can_24(): + # The can_24 matrix is used in the SuiteSparse AMD MATLAB/amd_demo.m file. + expect_info = COLAMDStats.from_array( + np.array( + [ + 0, # N_rows_ignored + 0, # N_cols_ignored + 1, # Ncmpa + 0, # status + -1, # info1 + -1, # info2 + 0, # info3 + ] + ) + ) + + # Load the can_24 matrix from a file + can_24_path = Path("tests") / "data" / "can_24" + with can_24_path.open() as fp: + can_24 = np.genfromtxt(fp, dtype=int) + + A = sparse.csc_array((can_24[:, 2], (can_24[:, 0] - 1, can_24[:, 1] - 1))) + q, info = colamd(A, return_info=True) + + assert is_valid_permutation(q) + assert info == expect_info + + +def test_colamd_defaults(): + """Test that COLAMD uses the default control settings.""" + # The default control settings are (from colamd.c:1095-1097): + # knobs[COLAMD_DENSE_ROW] = 10 ; + # knobs[COLAMD_DENSE_COL] = 10 ; + # knobs[COLAMD_AGGRESSIVE] = TRUE ; + expect_knobs = { + "dense_row_thresh": 10, + "dense_col_thresh": 10, + "aggressive": True, + } + knobs = colamd_get_defaults() + assert knobs == expect_knobs + + # A = sparse.csc_array([[1, 2], [3, 4]]) + # p = amd(A, **knobs) + # assert is_valid_permutation(p) + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_data/illc1033.mtx.gz b/tests/test_data/illc1033.mtx.gz deleted file mode 100644 index 5a91e315..00000000 Binary files a/tests/test_data/illc1033.mtx.gz and /dev/null differ diff --git a/tests/test_data/illc1033_rhs1.mtx.gz b/tests/test_data/illc1033_rhs1.mtx.gz deleted file mode 100644 index 8b1bf8cb..00000000 Binary files a/tests/test_data/illc1033_rhs1.mtx.gz and /dev/null differ diff --git a/tests/test_data/illc1850.mtx.gz b/tests/test_data/illc1850.mtx.gz deleted file mode 100644 index a99b507f..00000000 Binary files a/tests/test_data/illc1850.mtx.gz and /dev/null differ diff --git a/tests/test_data/illc1850_rhs1.mtx.gz b/tests/test_data/illc1850_rhs1.mtx.gz deleted file mode 100644 index bbca7dc3..00000000 Binary files a/tests/test_data/illc1850_rhs1.mtx.gz and /dev/null differ diff --git a/tests/test_data/well1033.mtx.gz b/tests/test_data/well1033.mtx.gz deleted file mode 100644 index 888d3313..00000000 Binary files a/tests/test_data/well1033.mtx.gz and /dev/null differ diff --git a/tests/test_data/well1033_rhs1.mtx.gz b/tests/test_data/well1033_rhs1.mtx.gz deleted file mode 100644 index f9b3e4a9..00000000 Binary files a/tests/test_data/well1033_rhs1.mtx.gz and /dev/null differ diff --git a/tests/test_data/well1850.mtx.gz b/tests/test_data/well1850.mtx.gz deleted file mode 100644 index d81d60ea..00000000 Binary files a/tests/test_data/well1850.mtx.gz and /dev/null differ diff --git a/tests/test_data/well1850_rhs1.mtx.gz b/tests/test_data/well1850_rhs1.mtx.gz deleted file mode 100644 index 31e9fbbb..00000000 Binary files a/tests/test_data/well1850_rhs1.mtx.gz and /dev/null differ diff --git a/tests/test_klu.py b/tests/test_klu.py new file mode 100644 index 00000000..e51c8e36 --- /dev/null +++ b/tests/test_klu.py @@ -0,0 +1,563 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_klu.py +# Created: 2025-10-31 12:25 +# ============================================================================= + +"""Unit tests for the klu module.""" + +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import linalg as la +from scipy import sparse +from scipy.io import mmread + +from sksparse.klu import ( + KLUControl, + KLUError, + KLUFactor, + KLUInvalidError, + KLUSingularMatrixWarning, + klu_factor, + klu_solve, +) + +from .helpers import generate_random_matrices + +ITYPES = [np.int32, np.int64] +DTYPES = [np.float64, np.complex128] + + +def assert_LU_equals_A(f, A, atol=1e-15): + r"""Check that L U + F = R P A Q.""" + L, U, F, p, q, r = f.L, f.U, f.F, f.perm_r, f.perm_c, f.rscale + LUF = (L @ U + F).toarray() + PRinvAQ = (r[:, np.newaxis] * A[p][:, q]).toarray() + assert_allclose(LUF, PRinvAQ, atol=atol, strict=True) + + +# ----------------------------------------------------------------------------- +# Simple Tests +# ----------------------------------------------------------------------------- +def test_empty_input(): + empty_A = sparse.csc_array((0, 0)) + with pytest.raises(KLUInvalidError, match="invalid input"): + _f = KLUFactor(empty_A) + + +def test_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + f = KLUFactor(zero_A) + assert not f.is_numeric + assert f.lnz is None + assert f.unz is None + assert f.nnz is None + assert f.shape == (N, N) + assert f.itype == zero_A.indptr.dtype + assert f.dtype == zero_A.dtype + with pytest.raises(KLUError, match="Numeric factorization not present"): + _L = f.L + + +def test_singleton(): + dtype = np.float64 + singleton_A = sparse.csc_array([[1]], dtype=dtype) + f = KLUFactor(singleton_A).factorize(singleton_A) + assert f.is_numeric + assert f.lnz == 1 + assert f.unz == 1 + assert f.nnz == 2 # nnz(L) + nnz(U) + assert f.shape == (1, 1) + assert f.itype == singleton_A.indptr.dtype + assert f.dtype == dtype + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +def test_types(davis_example_qr, itype, dtype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + A.data = A.data.astype(dtype) + f = KLUFactor(A) + assert f.itype == itype + assert f.dtype == dtype + + +# ----------------------------------------------------------------------------- +# Numeric Factorization +# ----------------------------------------------------------------------------- +def test_bad_factorize_itype(davis_example_qr): + A = davis_example_qr + A.indptr = A.indptr.astype(np.int32) + A.indices = A.indices.astype(np.int32) + f = KLUFactor(A) + B = A.copy() + B.indptr = B.indptr.astype(np.int64) + B.indices = B.indices.astype(np.int64) + with pytest.raises(ValueError, match="integer.*does not match"): + f.factorize(B) + + +def test_bad_factorize_dtype(davis_example_qr): + A = davis_example_qr.astype(np.float64) + f = KLUFactor(A) + with pytest.raises(ValueError, match="type.*does not match"): + f.factorize(A.astype(np.complex128)) + + +def test_bad_factorize_shape(davis_example_qr): + A = davis_example_qr + f = KLUFactor(A) + with pytest.raises(ValueError, match="shape.*does not match"): + f.factorize(A[:-1, :-1]) # remove last row and col + + +def test_bad_factorize_structure(davis_example_qr): + A = davis_example_qr + f = KLUFactor(A) + B = A.copy().todok() + # Change the structure of the matrix by adding a new non-zero + B[0, 1] = 2.3 + B = B.tocsc() + f.factorize(B) # passes + assert_LU_equals_A(f, B) + + +@pytest.mark.xfail(reason="Does not error, but gives wrong answer.") +def test_bad_refactorize_structure(davis_example_qr): + A = davis_example_qr + f = klu_factor(A) + B = A.copy().todok() + # Change the structure of the matrix by adding a new non-zero + B[0, 1] = 2.3 + B = B.tocsc() + f.factorize(B) # just gives wrong answer without error + assert_LU_equals_A(f, B) + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +def test_davis_example_qr(davis_example_qr, itype, dtype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + A.data = A.data.astype(dtype) + + f = klu_factor(A) + assert f.is_numeric + assert f.shape == A.shape + + # Get the factors + p, q = f.perm_r, f.perm_c + + # Values from MATLAB klu + # >> [LU, info, c] = klu(A); + # >> LU.p - 1 + # >> LU.q - 1 + expect_p = np.array([4, 7, 5, 1, 2, 0, 6, 3], dtype=itype) + expect_q = np.array([4, 5, 7, 1, 2, 0, 6, 3], dtype=itype) + + assert_array_equal(p, expect_p) + assert_array_equal(q, expect_q) + assert f.lnz == 16 # == nnz(L) in MATLAB + assert f.unz == 17 # == nnz(U) in MATLAB + assert f.nnz == 33 # == nnz(L) + nnz(U) in MATLAB + assert_LU_equals_A(f, A) + + +def test_iter(davis_example_qr): + A = davis_example_qr + L, U, p, q, r, F, _rblocks = klu_factor(A) + LUF = (L @ U + F).toarray() + PRinvAQ = (r[:, np.newaxis] * A[p][:, q]).toarray() + assert_allclose(LUF, PRinvAQ, atol=1e-15, strict=True) + + +test_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, square_only=True, dtype=dtype + ) +] + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("A", test_As) +def test_copy_symbolic(A, itype): + A.setdiag(A.diagonal() + 1.0) # make non-singular + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + f = KLUFactor(A) + g = f.copy() + assert g is not f + assert g.shape == f.shape + assert g.itype == f.itype + assert g.dtype == f.dtype + assert g.info == f.info + # Test that numeric factorization can be done on the copy + f.factorize(A) + g.factorize(A) + assert_LU_equals_A(f, A, atol=1e-12) + assert_LU_equals_A(g, A, atol=1e-12) + + +@pytest.mark.parametrize("copy", [False, True]) +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("A", test_As) +def test_refactor(A, itype, copy): + atol = 1e-8 + A.setdiag(A.diagonal() + 1.0) # make non-singular + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + f = klu_factor(A) + assert_LU_equals_A(f, A, atol=atol) + # Create a new matrix with the same sparsity pattern but different values + B = A.copy() + rng = np.random.default_rng(56) + B.data = rng.random(len(B.data)).astype(dtype=B.dtype) + # Factor the new matrix with the same sparsity pattern + if copy: + g = f.copy() + assert g is not f + g.factorize(B) + assert_LU_equals_A(g, B, atol=atol) + else: + f.factorize(B) + assert_LU_equals_A(f, B, atol=atol) + + +@pytest.mark.parametrize("A", test_As[:1]) +def test_sorted(A): + A.setdiag(A.diagonal() + 1.0) # make non-singular + f = klu_factor(A) + L, U = f.L, f.U + assert L.has_sorted_indices + assert L.has_canonical_format + assert U.has_sorted_indices + assert U.has_canonical_format + + +# ----------------------------------------------------------------------------- +# Solve +# ----------------------------------------------------------------------------- +class TestBadBShape: + @pytest.fixture(scope="class") + def N(self): + return 5 + + @pytest.fixture(scope="class") + def A(self, N): + return sparse.eye_array(N).tocsc() + + @pytest.fixture(scope="class") + def f(self, A): + return klu_factor(A) + + def test_b_0D_dense(self, f, A): + b = np.empty([]) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_dense(self, f, A): + b = np.empty((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_sparse(self, f, A): + b = sparse.coo_array((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_KD_dense(self, f, A, N): + b = np.empty((N - 1, N)) + with pytest.raises(ValueError, match="compatible shape with A"): + f.solve(b) + + def test_b_KD_sparse(self, f, A, N): + b = sparse.csc_array((N - 1, N)) + with pytest.raises(ValueError, match="compatible shape with A"): + f.solve(b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_dense(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = np.array([1], dtype=dtype) + x = KLUFactor(singleton_A).factorize(singleton_A).solve(b) + assert_allclose(x, b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_sparse(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = sparse.coo_array([1], dtype=dtype) + x = KLUFactor(singleton_A).factorize(singleton_A).solve(b) + assert_allclose(x.toarray(), b.toarray()) + + +@pytest.mark.parametrize("itype", ITYPES) +def test_itype_1D(davis_example_qr, itype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + x = klu_solve(A, b) + assert isinstance(x, sparse.coo_array) + assert x.coords[0].dtype == itype + + +@pytest.mark.parametrize("itype", ITYPES) +def test_itype_2D(davis_example_qr, itype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + K = 3 # arbitrary number of rhs + s = np.arange(1, N + 1, dtype=A.dtype) + data = np.array([i * s for i in range(1, K + 1)]).T + expect_x = sparse.csc_array(data, dtype=A.dtype) + b = A @ expect_x + x = klu_solve(A, b) + assert isinstance(x, sparse.csc_array) + assert x.indptr.dtype == itype + assert x.indices.dtype == itype + + +def test_exactly_singular(davis_example_qr): + A = davis_example_qr.todok() + A.setdiag(A.diagonal() + 1.0) # make non-singular + + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A exactly singular + s = -3 + A[:, s] = 0.0 + A[s, :] = 0.0 + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + + with pytest.raises(KLUError, match="indefinite or singular to working precision"): + klu_solve(A, b) + + +def test_nearly_singular(davis_example_qr): + A = davis_example_qr.todok() + A.setdiag(A.diagonal() + 1.0) # make non-singular + + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A nearly singular + A[:, -1] = 0.0 + A[-1, :] = 0.0 + A[-1, -1] = 0.5 * np.finfo(A.dtype).eps + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + f = klu_factor(A, scale="none") # disable row-scaling to trigger warning + print(f"{f.info.scale=}") + with pytest.warns(KLUSingularMatrixWarning, match="nearly singular"): + f.solve(b) + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize("K", [0, 1, 3], ids=lambda k: f"K={k}") +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_solve(A, K, is_sparse): + atol = 1e-12 + A.setdiag(A.diagonal() + 1.0) # make non-singular + + # Build RHS + N = A.shape[0] + s = np.arange(1, N + 1, dtype=A.dtype) + + if K == 0: + data = s # (N,) + else: + data = np.array([i * s for i in range(1, K + 1)], dtype=A.dtype).T # (N, K) + + if is_sparse: + expect_x = sparse.coo_array(data, dtype=A.dtype) + else: + expect_x = np.asarray(data, dtype=A.dtype) + + # Solve the system + b = A @ expect_x + x = klu_solve(A, b) + xt = klu_solve(A.T.tocsc(), b.T, transpose=True) + + # Compare + if is_sparse: + assert_allclose(x.toarray(), expect_x.toarray(), atol=atol) + assert_allclose(xt.toarray(), expect_x.T.toarray(), atol=atol) + else: + assert_allclose(x, expect_x, atol=atol) + assert_allclose(xt, expect_x.T, atol=atol) + + +# Test solve on "real-world" matrices +def _load_problem(name): + """Load a matrix and RHS from a Matrix Market file.""" + data_path = Path(__file__).parent / "data" + matrix_file = data_path / f"{name}.mtx.gz" + + if not matrix_file.exists(): + raise FileNotFoundError(f"Matrix Market file {matrix_file} not found.") + + A = mmread(matrix_file, spmatrix=False).tocsc() + + # Possibly load RHS + rhs_file = data_path / f"{name}_rhs1.mtx.gz" + + if not rhs_file.exists(): + raise FileNotFoundError(f"Matrix Market file {rhs_file} not found.") + + b = mmread(rhs_file) + + return A, b + + +# TODO @pytest.mark.slow +@pytest.mark.parametrize("problem", ["well1033", "illc1033", "well1850", "illc1850"]) +def test_solve_real(problem): + A, b = _load_problem(problem) + # Solve the normal equations A^T A x = A^T b + ATA = (A.T @ A).tocsc() + ATb = A.T @ b + expect_x = np.linalg.lstsq(A.toarray(), b)[0] + x = klu_solve(ATA, ATb) + assert_allclose(x, expect_x, atol=1e-7) + + +# ----------------------------------------------------------------------------- +# Test Control and Info +# ----------------------------------------------------------------------------- +def test_info(davis_example_qr): + A = davis_example_qr + f = klu_factor(A) + info = f.info + # Values from MATLAB klu + # >> [LU, info, c] = klu(A); + assert info.noffdiag == 0 + assert info.nrealloc == 0 + assert_allclose(info.rcond, 0.084848, rtol=1e-4) + assert info.singular_col == 8 # dimension of A + assert_allclose(info.rgrowth, 0.509, rtol=1e-3) + assert info.flops == 28 + assert info.nblocks == 1 + assert info.ordering == "AMD" + assert info.scale == "max" + assert info.lnz == 16 + assert info.unz == 17 + assert info.nzoff == 0 + assert info.tol == 0.001 + assert info.mempeak != 0 # number varies with system + + +@pytest.mark.parametrize("scale", [None, "none_no_check", "none", "sum", "max"]) +def test_row_scale(davis_example_qr, scale): + A = davis_example_qr + A.setdiag(A.diagonal() + 1.0) # make non-singular + f = klu_factor(A, scale=scale) + if scale is None: + assert f.info.scale == "max" # default + else: + assert f.info.scale == scale + assert_LU_equals_A(f, A) + if scale == "none": + assert_allclose(f.L.diagonal(), 1.0) + + +ORDERINGS = [None, "AMD", "COLAMD"] + + +@pytest.mark.parametrize("ordering", ORDERINGS) +def test_ordering(davis_example_qr, ordering): + A = davis_example_qr + A.setdiag(A.diagonal() + 1.0) # make non-singular + # Ordering must be done *before* symbolic factorization + f = klu_factor(A, ordering=ordering) + expect_x = np.arange(1, A.shape[0] + 1, dtype=A.dtype) + b = A @ expect_x + x = f.solve(b) + assert_LU_equals_A(f, A) + assert_allclose(x, expect_x, atol=1e-15, strict=True) + if ordering is None: + assert f.info.ordering == "AMD" # default + else: + assert f.info.ordering == ordering + + +@pytest.mark.parametrize("ordering", ["user_perm", "user_func"]) +def test_bad_ordering(davis_example_qr, ordering): + A = davis_example_qr + with pytest.raises(NotImplementedError, match="not yet supported"): + klu_factor(A, ordering=ordering) + + +def test_bad_control_tol(): + c = KLUControl() + match_str = r"tol must be a float in the range \[0, 1\]" + with pytest.raises(ValueError, match=match_str): + c.tol = -0.1 + with pytest.raises(ValueError, match=match_str): + c.tol = 10 + with pytest.raises(ValueError, match=match_str): + c.tol = "invalid" + + +def test_bad_control_memgrow(): + c = KLUControl() + match_str = "memgrow must be a float greater than 1.0" + with pytest.raises(ValueError, match=match_str): + c.memgrow = -1 + with pytest.raises(ValueError, match=match_str): + c.memgrow = 0.5 + with pytest.raises(ValueError, match=match_str): + c.memgrow = "invalid" + + +def test_bad_control_initmem_amd(): + c = KLUControl() + match_str = "initmem_amd must be a float greater than 1.0" + with pytest.raises(ValueError, match=match_str): + c.initmem_amd = -1 + with pytest.raises(ValueError, match=match_str): + c.initmem_amd = 0.5 + with pytest.raises(ValueError, match=match_str): + c.initmem_amd = "invalid" + + +def test_bad_control_initmem(): + c = KLUControl() + match_str = "initmem must be a float greater than 0.0" + with pytest.raises(ValueError, match=match_str): + c.initmem = -1 + with pytest.raises(ValueError, match=match_str): + c.initmem = "invalid" + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_spqr.py b/tests/test_spqr.py new file mode 100644 index 00000000..cdde9aae --- /dev/null +++ b/tests/test_spqr.py @@ -0,0 +1,772 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_spqr.py +# Created: 2025-11-07 13:31 +# ============================================================================= + +"""Unit tests for the scikit-sparse.spqr module.""" + +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import linalg as la +from scipy import sparse +from scipy.io import mmread + +from sksparse.spqr import ( + SPQRFactor, + SPQRRankDeficiencyWarning, + spqr, + spqr_factor, + spqr_qmult, + spqr_solve, +) + +from .helpers import generate_random_matrices + +ITYPES = [np.int32, np.int64] +DTYPES = [np.float64, np.complex128] + + +def assert_solve_dense(A, f, atol=1e-15): + N = f.shape[1] + expect_x = np.arange(1, N + 1, dtype=f.dtype) + b = A @ expect_x + x = f.solve(b) + assert_allclose(x, expect_x, atol=atol, strict=True) + + +# ----------------------------------------------------------------------------- +# Simple Tests +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize("itype", ITYPES) +def test_empty_input(itype): + empty_A = sparse.csc_array((0, 0)) + empty_A.indptr = empty_A.indptr.astype(itype) + empty_A.indices = empty_A.indices.astype(itype) + f = SPQRFactor(empty_A) + assert f.rank is None # no numeric factorization yet + # assert_allclose(f.Q.toarray(), empty_A.toarray(), strict=True) + # assert_allclose(f.R.toarray(), empty_A.toarray(), strict=True) + assert_array_equal(f.perm, np.array([], dtype=itype), strict=True) + + +@pytest.mark.parametrize("itype", ITYPES) +def test_zero_input(itype): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + zero_A.indptr = zero_A.indptr.astype(itype) + zero_A.indices = zero_A.indices.astype(itype) + f = spqr_factor(zero_A) + assert f.rank == 0 + # assert_allclose(f.Q.toarray(), np.eye(N, dtype=A.dtype), strict=True) + # assert_allclose(f.R.toarray(), np.array([], dtype=A.dtype), strict=True) + assert_array_equal(f.perm, np.arange(N, dtype=itype), strict=True) + + +def test_singleton(): + dtype = np.float64 + singleton_A = sparse.csc_array([[1]], dtype=dtype) + f = spqr_factor(singleton_A) + assert f.is_numeric + assert f.rank == 1 + assert f.shape == (1, 1) + assert_array_equal(f.perm, np.array([0], dtype=np.int32), strict=True) + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +def test_types(davis_example_qr, itype, dtype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + A.data = A.data.astype(dtype) + f = SPQRFactor(A) + assert f.itype == itype + assert f.dtype == dtype + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64, np.complex64, np.complex128]) +def test_type_promotion(davis_example_qr, dtype): + A = davis_example_qr.astype(dtype) + f = SPQRFactor(A) + expect_dtype = np.float64 if np.issubdtype(dtype, np.floating) else np.complex128 + assert f.dtype == expect_dtype + + +# ----------------------------------------------------------------------------- +# Numeric Factorization +# ----------------------------------------------------------------------------- +def test_bad_factorize_itype(davis_example_qr): + A = davis_example_qr + A.indptr = A.indptr.astype(np.int32) + A.indices = A.indices.astype(np.int32) + f = SPQRFactor(A) + B = A.copy() + B.indptr = B.indptr.astype(np.int64) + B.indices = B.indices.astype(np.int64) + with pytest.raises(ValueError, match="integer.*does not match"): + f.factorize(B) + + +def test_bad_factorize_dtype(davis_example_qr): + A = davis_example_qr.astype(np.float64) + f = SPQRFactor(A) + with pytest.raises(ValueError, match="type.*does not match"): + f.factorize(A.astype(np.complex128)) + + +def test_bad_factorize_shape(davis_example_qr): + A = davis_example_qr + f = SPQRFactor(A) + with pytest.raises(ValueError, match="shape.*does not match"): + f.factorize(A[:-1, :]) # remove last row + + +@pytest.mark.xfail(reason="No pattern check is done in SPQRFactor") +def test_bad_factorize_structure(davis_example_qr): + A = davis_example_qr + f = SPQRFactor(A) + assert_solve_dense(A, f) + B = A.copy().todok() + # Change the structure of the matrix by adding a new non-zero + B[0, 1] = 2.3 + B = B.tocsc() + B.indptr = B.indptr.astype(A.indptr.dtype) + B.indices = B.indices.astype(A.indices.dtype) + # No error is raised since there is no pattern check + f.factorize(B) + # Try solving a system to ensure factorization was successful + assert_solve_dense(B, f) + + +@pytest.mark.xfail(reason="No pattern check is done in SPQRFactor") +def test_bad_refactorize_structure(davis_example_qr): + A = davis_example_qr + f = spqr_factor(A) + assert_solve_dense(A, f) + B = A.copy().todok() + # Change the structure of the matrix by adding a new non-zero + B[0, 1] = 2.3 + B = B.tocsc() + B.indptr = B.indptr.astype(A.indptr.dtype) + B.indices = B.indices.astype(A.indices.dtype) + # No error is raised since there is no pattern check + f.factorize(B) + # Try solving a system to ensure factorization was successful + assert_solve_dense(B, f) + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +def test_davis_example_qr(davis_example_qr, itype, dtype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + A.data = A.data.astype(dtype) + + f = SPQRFactor(A).factorize(A) + assert f.is_numeric + + # Get the factors + p = f.perm + + # Values from MATLAB spqr + # >> [Q, R, E] = spqr(A); + # >> [p j x] = find(E); + expect_p = np.array([0, 3, 2, 1, 7, 4, 5, 6], dtype=itype) + + assert_array_equal(p, expect_p) + assert_solve_dense(A, f) + + +test_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, shape_kind="M >= N", dtype=dtype + ) +] + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("A", test_As) +def test_copy_symbolic(A, itype): + A = A.copy() + A.setdiag(A.diagonal() + 1.0) # make non-singular + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + f = SPQRFactor(A) + g = f.copy() + assert g is not f + assert g.shape == f.shape + assert g.itype == f.itype + assert g.dtype == f.dtype + # Test that numeric factorization + solve can be done on the copy + f.factorize(A) + assert_solve_dense(A, f) + del f # ensure no shared state + g.factorize(A) + assert_solve_dense(A, g) + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("A", test_As) +def test_copy_numeric(A, itype): + A = A.copy() + A.setdiag(A.diagonal() + 1.0) # make non-singular + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + f = spqr_factor(A) + g = f.copy() + assert g is not f + assert_solve_dense(A, f) + del f # ensure no shared state + assert_solve_dense(A, g) + + +@pytest.mark.parametrize("copy", [False, True]) +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("A", test_As) +def test_refactor(A, itype, copy): + A = A.copy() + A.setdiag(A.diagonal() + 1.0) # make non-singular + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + f = spqr_factor(A) + # Create a new matrix with the same sparsity pattern but different values + B = A.copy() + rng = np.random.default_rng(56) + B.data = rng.random(len(B.data)).astype(dtype=B.dtype) + # Factor the new matrix with the same sparsity pattern + if copy: + g = f.copy() + assert g is not f + assert_solve_dense(A, f) # make sure copy didn't affect f + del f # ensure no shared state + # NOTE this test *does not* check that the numeric copy is correct, + # because it entirely recomputes it with the factorize call. + g.factorize(B) + assert_solve_dense(B, g) + else: + f.factorize(B) + assert_solve_dense(B, f) + + +# ----------------------------------------------------------------------------- +# Qmult +# ----------------------------------------------------------------------------- +class TestBadQmultShape: + @pytest.fixture(scope="class") + def N(self): + return 5 + + @pytest.fixture(scope="class") + def A(self, N): + return sparse.eye_array(N).tocsc() + + @pytest.fixture(scope="class") + def f(self, A): + return spqr_factor(A) + + def test_X_0D_dense(self, f, A): + X = np.empty([]) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.qmult(X) + + def test_X_3D_dense(self, f, A): + X = np.empty((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.qmult(X) + + def test_X_3D_sparse(self, f, A): + X = sparse.coo_array((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.qmult(X) + + def test_X_KD_dense(self, f, A, N): + X = np.empty((N - 1, N)) + with pytest.raises(ValueError, match="compatible shape with Q"): + f.qmult(X) + + def test_X_KD_sparse(self, f, A, N): + X = sparse.csc_array((N - 1, N)) + with pytest.raises(ValueError, match="compatible shape with Q"): + f.qmult(X) + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_qmult(davis_example_qr, is_sparse, itype, dtype): + A = davis_example_qr.astype(dtype) + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + f = spqr_factor(A) + + if is_sparse: + I = sparse.eye_array(A.shape[0], dtype=dtype).tocsc() + I.indptr = I.indptr.astype(itype) + I.indices = I.indices.astype(itype) + else: + I = np.eye(A.shape[0], dtype=dtype) + + Q = f.qmult(I, "QX") + QTQ = f.qmult(Q, "QTX") + QQT = f.qmult(Q, "XQT") + + if is_sparse: + assert_allclose(QTQ.toarray(), I.toarray(), atol=1e-15, strict=True) + assert_allclose(QQT.toarray(), I.toarray(), atol=1e-15, strict=True) + else: + assert_allclose(QTQ, I, atol=1e-15, strict=True) + assert_allclose(QQT, I, atol=1e-15, strict=True) + + QT = f.qmult(I, "QTX") + QTQ = f.qmult(QT, "XQ") + + if is_sparse: + assert_allclose(QTQ.toarray(), I.toarray(), atol=1e-15, strict=True) + else: + assert_allclose(QTQ, I, atol=1e-15, strict=True) + + +# ----------------------------------------------------------------------------- +# Solve +# ----------------------------------------------------------------------------- +class TestBadSolveShape: + @pytest.fixture(scope="class") + def N(self): + return 5 + + @pytest.fixture(scope="class") + def A(self, N): + return sparse.eye_array(N).tocsc() + + @pytest.fixture(scope="class") + def f(self, A): + return spqr_factor(A) + + def test_b_0D_dense(self, f, A): + b = np.empty([]) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_dense(self, f, A): + b = np.empty((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_sparse(self, f, A): + b = sparse.coo_array((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_KD_dense(self, f, A, N): + b = np.empty((N - 1, N)) + with pytest.raises(ValueError, match="compatible shape with A"): + f.solve(b) + + def test_b_KD_sparse(self, f, A, N): + b = sparse.csc_array((N - 1, N)) + with pytest.raises(ValueError, match="compatible shape with A"): + f.solve(b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_dense(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = np.array([1], dtype=dtype) + x = SPQRFactor(singleton_A).factorize(singleton_A).solve(b) + assert_allclose(x, b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_sparse(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = sparse.coo_array([1], dtype=dtype) + x = SPQRFactor(singleton_A).factorize(singleton_A).solve(b) + assert_allclose(x.toarray(), b.toarray()) + + +@pytest.mark.parametrize("itype", ITYPES) +def test_itype_1D(davis_example_qr, itype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + x = spqr_solve(A, b) + assert isinstance(x, sparse.coo_array) + assert x.coords[0].dtype == itype + + +@pytest.mark.parametrize("itype", ITYPES) +def test_itype_2D(davis_example_qr, itype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + K = 3 # arbitrary number of rhs + s = np.arange(1, N + 1, dtype=A.dtype) + data = np.array([i * s for i in range(1, K + 1)]).T + expect_x = sparse.csc_array(data, dtype=A.dtype) + b = A @ expect_x + x = spqr_solve(A, b) + assert isinstance(x, sparse.csc_array) + assert x.indptr.dtype == itype + assert x.indices.dtype == itype + + +def test_exactly_singular(davis_example_qr): + A = davis_example_qr.todok() + A.setdiag(A.diagonal() + 1.0) # make non-singular + + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A exactly singular + s = 7 + A[:, s] = 0.0 + A[s, :] = 0.0 + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + f = spqr_factor(A) + assert f.rank == N - 1 + + # We just get a "0" in the singular position of the solution + with pytest.warns(SPQRRankDeficiencyWarning, match="rank deficient"): + x = f.solve(b) + + x = x.toarray() + assert x[s] == 0.0 + idx = np.arange(N) != s + assert_allclose(x[idx], expect_x.toarray()[idx], atol=1e-15, strict=True) + assert_allclose(A @ x, b.toarray(), atol=1e-15, strict=True) + + +def test_nearly_singular(davis_example_qr): + A = davis_example_qr.todok() + A.setdiag(A.diagonal() + 1.0) # make non-singular + + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A nearly singular + s = 5 # arbitrary singular row/column + A[:, s] = 0.0 + A[s, :] = 0.0 + A[s, s] = 0.5 * np.finfo(A.dtype).eps + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + f = spqr_factor(A) + assert f.rank == N - 1 + + # We just get a "0" in the singular position of the solution + with pytest.warns(SPQRRankDeficiencyWarning, match="rank deficient"): + x = f.solve(b) + + x = x.toarray() + assert x[s] == 0.0 + idx = np.arange(N) != s + assert_allclose(x[idx], expect_x.toarray()[idx], atol=1e-15, strict=True) + assert_allclose(A @ x, b.toarray(), atol=1e-15, strict=True) + + +# Base test function for solving systems +def _test_solve(A, K, is_sparse, transpose, underdetermined): + atol = 1e-12 + A = A.copy() + if underdetermined: + A = A.T.conj().tocsc() + A.setdiag(A.diagonal() + 1.0) # make non-singular + + # Build RHS + M, N = A.shape + s = np.arange(1, N + 1, dtype=A.dtype) + + if K == 0: + data = s # (N,) + else: + data = np.array([i * s for i in range(1, K + 1)], dtype=A.dtype).T # (N, K) + + if is_sparse: + expect_x = sparse.coo_array(data, dtype=A.dtype) + else: + expect_x = np.asarray(data, dtype=A.dtype) + + # Solve the system + if not transpose: + b = A @ expect_x + else: + b = A.T.conj() @ expect_x + + f = spqr_factor(A) + assert f.rank == (N if not underdetermined else M) + + x = f.solve(b, transpose=transpose) + + # Check residuals in all cases + if not transpose: + resid = A @ x - b + else: + resid = A.T.conj() @ x - b + + if is_sparse: + resid = resid.toarray() + + assert_allclose(resid, np.zeros_like(resid), atol=1e-10, strict=True) + + # In underdetermined case, the solution is not unique + if not underdetermined: + if is_sparse: + assert_allclose(x.toarray(), expect_x.toarray(), atol=atol, strict=True) + else: + assert_allclose(x, expect_x, atol=atol, strict=True) + + +square_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices( + N_trials=10, N_max=200, d_scale=0.05, shape_kind="square", dtype=dtype + ) +] + + +@pytest.mark.parametrize("A", square_As) +@pytest.mark.parametrize("transpose", [False, True], ids=["A", "A^T"]) +@pytest.mark.parametrize("K", [0, 1, 3], ids=lambda k: f"K={k}") +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_solve_square(A, K, is_sparse, transpose): + _test_solve(A, K, is_sparse, transpose, underdetermined=False) + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize( + "underdetermined", [False, True], ids=["overdetermined", "underdetermined"] +) +@pytest.mark.parametrize("K", [0, 1, 3], ids=lambda k: f"K={k}") +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_solve_overunder(A, K, is_sparse, underdetermined): + _test_solve(A, K, is_sparse, transpose=False, underdetermined=underdetermined) + + +def test_min2norm(davis_example_qr): + A = davis_example_qr.todok() + A.setdiag(A.diagonal() + 1.0) # make non-singular + A = A[:-2, :].tocsc() # make underdetermined + M, N = A.shape + + expect_x = np.arange(1, N + 1, dtype=A.dtype) + b = A @ expect_x + + # Solve with min 2-norm solver + x = spqr_solve(A, b, min2norm=True) + + assert_allclose(A @ x, b, atol=1e-15, strict=True) + + # Solve with normal solver and check norm + xf = spqr_solve(A, b, min2norm=False) + + print() + print(f"||x||_2 = {la.norm(x):.6e}") + print(f"||xf||_2 = {la.norm(xf):.6e}") + assert la.norm(x) <= la.norm(xf) + + +# Test solve on "real-world" matrices +def _load_problem(name): + """Load a matrix and RHS from a Matrix Market file.""" + data_path = Path(__file__).parent / "data" + matrix_file = data_path / f"{name}.mtx.gz" + + if not matrix_file.exists(): + raise FileNotFoundError(f"Matrix Market file {matrix_file} not found.") + + A = mmread(matrix_file, spmatrix=False).tocsc() + + # Possibly load RHS + rhs_file = data_path / f"{name}_rhs1.mtx.gz" + + if not rhs_file.exists(): + raise FileNotFoundError(f"Matrix Market file {rhs_file} not found.") + + b = mmread(rhs_file) + + return A, b + + +@pytest.mark.parametrize("problem", ["well1033", "illc1033", "well1850", "illc1850"]) +def test_solve_real(problem): + A, b = _load_problem(problem) + # Solve the normal equations A^T A x = A^T b + ATA = (A.T @ A).tocsc() + ATb = A.T @ b + expect_x = np.linalg.lstsq(A.toarray(), b)[0] + x = spqr_solve(ATA, ATb) + assert_allclose(x, expect_x, rtol=1e-6, atol=1e-8) + + +# ----------------------------------------------------------------------------- +# Test Control and Info +# ----------------------------------------------------------------------------- +def test_info(davis_example_qr): + A = davis_example_qr + f = spqr_factor(A) + info = f.info + print() + print(info) + # Values from MATLAB spqr + # >> [Q, R, E, info] = spqr(A); + assert info.nnzR_upper_bound == 36 # == 100 + assert info.nnzH_upper_bound == 9 + assert info.nf == 1 + assert info.rank_A_estimate == 8 + assert info.n1cols == 0 + assert info.n1rows == 0 + assert info.ordering == "colamd" + assert info.memory > 0 # == 6184 + assert info.flops_upper_bound == 303 # == 847 + assert_allclose(info.tol, 5.1728e-13, rtol=1e-4) + assert info.norm_E_fro == 0 + assert info.analyze_time > 0 # == 6.4135e-05 + assert info.factorize_time > 0 # == 3.0994e-05 + assert info.solve_time == 0 # == 1.1683e-05 + assert_allclose( + info.total_time, # == 1.7700e-04 + info.analyze_time + info.factorize_time + info.solve_time, + ) + assert info.flops == 303 # == 847 + + +ORDERS = [ + None, + "default", + "fixed", + "natural", + "colamd", + "cholmod", + "amd", + "metis", + "best", + "bestamd", +] + + +def test_bad_ordering(davis_example_qr): + A = davis_example_qr + with pytest.raises(ValueError, match="Unknown ordering"): + SPQRFactor(A, order="invalid") + + +@pytest.mark.parametrize("order", ORDERS) +def test_ordering(davis_example_qr, order): + A = davis_example_qr + A.setdiag(A.diagonal() + 1.0) # make non-singular + f = spqr_factor(A, order=order) + assert_solve_dense(A, f) + + +# ----------------------------------------------------------------------------- +# Test spqr +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize("itype", ITYPES) +def test_spqr_r(A, itype): + A = A.copy() + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + R, p = spqr(A, mode="r") + RTR = (R.T.conj() @ R).toarray() + ATA = (A.T.conj() @ A)[p[:, np.newaxis], p].toarray() + assert_allclose(RTR, ATA, atol=1e-14, strict=True) + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize("itype", ITYPES) +def test_spqr_full(A, itype): + A = A.copy() + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + Q, R, p = spqr(A, mode="full") + assert_allclose((Q @ R).toarray(), A[:, p].toarray(), atol=1e-14, strict=True) + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize("itype", ITYPES) +def test_spqr_householder(A, itype): + A = A.copy() + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + Ht, R, p = spqr(A, mode="householder") + + M, N = A.shape + H, tau, v = Ht + + assert H.shape[0] == M # number of columns not known exactly + assert v.shape == (H.shape[0],) # row permutation of H + assert tau.shape == (H.shape[1],) # column coefficients of H + + assert H.dtype == A.dtype + assert tau.dtype == A.dtype + assert v.dtype == itype + + Q = spqr_qmult(Ht, np.eye(A.shape[0], dtype=A.dtype)) + assert_allclose(Q @ R, A[:, p].toarray(), atol=1e-14, strict=True) + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_spqr_qmult(davis_example_qr, is_sparse, itype, dtype): + A = davis_example_qr.astype(dtype) + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + Ht, R, p = spqr(A, mode="householder") + + if is_sparse: + I = sparse.eye_array(A.shape[0], dtype=dtype).tocsc() + I.indptr = I.indptr.astype(itype) + I.indices = I.indices.astype(itype) + else: + I = np.eye(A.shape[0], dtype=dtype) + + Q = spqr_qmult(Ht, I, "QX") + QTQ = spqr_qmult(Ht, Q, "QTX") + QQT = spqr_qmult(Ht, Q, "XQT") + + if is_sparse: + assert_allclose(QTQ.toarray(), I.toarray(), atol=1e-15, strict=True) + assert_allclose(QQT.toarray(), I.toarray(), atol=1e-15, strict=True) + else: + assert_allclose(QTQ, I, atol=1e-15, strict=True) + assert_allclose(QQT, I, atol=1e-15, strict=True) + + QT = spqr_qmult(Ht, I, "QTX") + QTQ = spqr_qmult(Ht, QT, "XQ") + + if is_sparse: + assert_allclose(QTQ.toarray(), I.toarray(), atol=1e-15, strict=True) + else: + assert_allclose(QTQ, I, atol=1e-15, strict=True) + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_umfpack.py b/tests/test_umfpack.py new file mode 100644 index 00000000..ed1ff2ec --- /dev/null +++ b/tests/test_umfpack.py @@ -0,0 +1,572 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 the scikit-sparse developers. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_umfpack.py +# Created: 2025-10-16 11:54 +# ============================================================================= + +"""Unit tests for the umfpack module.""" + +import warnings +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import linalg as la +from scipy import sparse +from scipy.io import mmread + +from sksparse.umfpack import ( + UMFControl, + UMFFactor, + UMFPACKDifferentPatternError, + UMFPACKError, + UMFPACKNonpositiveError, + UMFPACKSingularMatrixWarning, + umf_factor, + umf_solve, +) + +from .helpers import generate_random_matrices + +ITYPES = [np.int32, np.int64] +DTYPES = [np.float64, np.complex128] + + +def assert_LU_equals_A(f, A, atol=1e-15): + """Check that L U = P R A Q.""" + L, U, p, q, r = f.L, f.U, f.perm_r, f.perm_c, f.rscale + LU = (L @ U).toarray() + PRAQ = (r[:, np.newaxis] * A).tocsc()[p][:, q].toarray() + assert_allclose(LU, PRAQ, atol=atol, strict=True) + + +# ----------------------------------------------------------------------------- +# Simple Tests +# ----------------------------------------------------------------------------- +def test_empty_input(): + empty_A = sparse.csc_array((0, 0)) + with pytest.raises(UMFPACKNonpositiveError, match="non-positive"): + _f = UMFFactor(empty_A) + + +def test_zero_input(): + N = 10 # arbitrary + zero_A = sparse.csc_array((N, N)) + f = UMFFactor(zero_A) + assert f.nnz is None + assert f.shape == (N, N) + assert f.itype == zero_A.indptr.dtype + assert f.dtype == zero_A.dtype + with pytest.raises(UMFPACKError, match="Numeric factorization not present"): + _L = f.L + + +def test_singleton(): + dtype = np.float64 + singleton_A = sparse.csc_array([[1]], dtype=dtype) + f = umf_factor(singleton_A) + assert f.is_numeric + assert f.lnz == 1 + assert f.unz == 1 + assert f.nnz == 2 + assert f.shape == (1, 1) + assert f.itype == singleton_A.indptr.dtype + assert f.dtype == dtype + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +def test_types(davis_example_qr, itype, dtype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + A.data = A.data.astype(dtype) + f = UMFFactor(A) + assert f.itype == itype + assert f.dtype == dtype + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64, np.complex64, np.complex128]) +def test_type_promotion(davis_example_qr, dtype): + A = davis_example_qr.astype(dtype) + f = UMFFactor(A) + expect_dtype = np.float64 if np.issubdtype(dtype, np.floating) else np.complex128 + assert f.dtype == expect_dtype + + +# ----------------------------------------------------------------------------- +# Numeric Factorization +# ----------------------------------------------------------------------------- +def test_bad_factorize_itype(davis_example_qr): + A = davis_example_qr + A.indptr = A.indptr.astype(np.int32) + A.indices = A.indices.astype(np.int32) + f = UMFFactor(A) + B = A.copy() + B.indptr = B.indptr.astype(np.int64) + B.indices = B.indices.astype(np.int64) + with pytest.raises(ValueError, match="integer.*does not match"): + f.factorize(B) + + +def test_bad_factorize_dtype(davis_example_qr): + A = davis_example_qr.astype(np.float64) + f = UMFFactor(A) + with pytest.raises(ValueError, match="type.*does not match"): + f.factorize(A.astype(np.complex128)) + + +def test_bad_factorize_shape(davis_example_qr): + A = davis_example_qr + f = UMFFactor(A) + with pytest.raises(ValueError, match="shape.*does not match"): + f.factorize(A[:-1, :]) # remove last row + + +def test_bad_factorize_structure(davis_example_qr): + A = davis_example_qr + f = UMFFactor(A) + B = A.copy().todok() + # Change the structure of the matrix by adding a new non-zero + B[0, 1] = 2.3 + B = B.tocsc() + B.indptr = B.indptr.astype(A.indptr.dtype) + B.indices = B.indices.astype(A.indices.dtype) + with pytest.raises(UMFPACKDifferentPatternError, match="different nonzero pattern"): + f.factorize(B) + + +def test_bad_refactorize_structure(davis_example_qr): + A = davis_example_qr + f = umf_factor(A) + B = A.copy().todok() + # Change the structure of the matrix by adding a new non-zero + B[0, 1] = 2.3 + B = B.tocsc() + B.indptr = B.indptr.astype(A.indptr.dtype) + B.indices = B.indices.astype(A.indices.dtype) + with pytest.raises(UMFPACKDifferentPatternError, match="different nonzero pattern"): + f.factorize(B) + + +@pytest.mark.parametrize("itype", ITYPES) +@pytest.mark.parametrize("dtype", DTYPES) +def test_davis_example_qr(davis_example_qr, itype, dtype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + A.data = A.data.astype(dtype) + + f = umf_factor(A) + assert f.is_numeric + assert f.L is f.L # test cached properties + assert f.U is f.U + + # Get the factors + p, q = f.perm_r, f.perm_c + + # Values from MATLAB umfpack + # >> [L, U, P, Q, R] = umfpack(A); + # >> [p j x] = find(P'); + # >> [q j x] = find(Q); + expect_p = np.array([0, 3, 1, 2, 6, 7, 4, 5], dtype=itype) + expect_q = np.array([0, 3, 1, 2, 6, 7, 5, 4], dtype=itype) + + assert_array_equal(p, expect_p) + assert_array_equal(q, expect_q) + assert f.lnz == 15 # == nnz(L) in MATLAB + assert f.unz == 16 # == nnz(U) in MATLAB + assert f.nnz == 31 # == nnz(L) + nnz(U) in MATLAB + assert f.nz_udiag == 8 # == nnz(diag(U)) in MATLAB + assert_LU_equals_A(f, A) + + +def _numpy_slogdet(A): + """Compute sign and logdet of A using numpy. Suppress warnings.""" + # In some versions of numpy, a warning is raised by slogdet for + # these complex types: (np.complex64, np.complex128). Make sure that is the + # warning that is raised, and not some other warning. + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") + sign, logdet = np.linalg.slogdet(A.toarray()) + + if record: + assert record[0].category is RuntimeWarning + assert "divide by zero" in str(record[0].message) or "invalid value" in str( + record[0].message + ) + else: + pass + + return sign, logdet + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_eye_determinant(dtype): + N = 3 + A = 10 * sparse.eye_array(N, dtype=dtype).tocsc() + f = umf_factor(A) + rtol = 1e-7 + expect_sign, expect_logdet = _numpy_slogdet(A) + assert expect_sign == 1 + assert expect_logdet == N * np.log(10) + assert_allclose(f.slogdet(), (expect_sign, expect_logdet), rtol=rtol, strict=True) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_determinant(davis_example_qr, dtype): + A = davis_example_qr + # Set the data to random values + rng = np.random.default_rng(56) + A.data = rng.random(len(A.data)).astype(dtype=dtype) + if np.issubdtype(dtype, np.complexfloating): + A.data += 1j * rng.random(len(A.data)).astype(dtype=dtype) + A.setdiag(A.diagonal() + 1.0) # make non-singular + f = umf_factor(A) + rtol = 1e-7 + expect_sign, expect_logdet = _numpy_slogdet(A) + assert_allclose(f.slogdet(), (expect_sign, expect_logdet), rtol=rtol, strict=True) + + +def test_iter(davis_example_qr): + A = davis_example_qr + L, U, p, q, r = umf_factor(A) + LU = (L @ U).toarray() + PRAQ = (r[:, np.newaxis] * A).tocsc()[p][:, q].toarray() + assert_allclose(LU, PRAQ, atol=1e-15, strict=True) + + +test_As = [ + A + for dtype in DTYPES + for A in generate_random_matrices(N_trials=10, N_max=200, d_scale=0.05, dtype=dtype) +] + + +@pytest.mark.parametrize("copy", [False, True]) +@pytest.mark.parametrize("A", test_As) +def test_refactor(A, copy): + atol = 1e-12 if A.dtype in (np.float64, np.complex128) else 1e-6 + A.setdiag(A.diagonal() + 1.0) # make non-singular + f = umf_factor(A) + assert_LU_equals_A(f, A, atol=atol) + # Create a new matrix with the same sparsity pattern but different values + B = A.copy() + rng = np.random.default_rng(56) + B.data = rng.random(len(B.data)).astype(dtype=B.dtype) + # Factor the new matrix with the same sparsity pattern + if copy: + g = f.copy() + assert g is not f + g.factorize(B) + assert_LU_equals_A(g, B, atol=atol) + else: + f.factorize(B) + assert_LU_equals_A(f, B, atol=atol) + + +# ----------------------------------------------------------------------------- +# Solve +# ----------------------------------------------------------------------------- +class TestBadBShape: + @pytest.fixture(scope="class") + def N(self): + return 5 + + @pytest.fixture(scope="class") + def A(self, N): + return sparse.eye_array(N).tocsc() + + @pytest.fixture(scope="class") + def f(self, A): + return umf_factor(A) + + def test_b_0D_dense(self, f, A): + b = np.empty([]) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_dense(self, f, A): + b = np.empty((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_3D_sparse(self, f, A): + b = sparse.coo_array((2, 3, 4)) + with pytest.raises(ValueError, match="must be a 1D or 2D array"): + f.solve(b) + + def test_b_KD_dense(self, f, A, N): + b = np.empty((N - 1, N)) + with pytest.raises(ValueError, match="same number of rows as A"): + f.solve(b) + + def test_b_KD_sparse(self, f, A, N): + b = sparse.csc_array((N - 1, N)) + with pytest.raises(ValueError, match="same number of rows as A"): + f.solve(b) + + +def test_bad_A_shape_solve(): + A = sparse.csc_array([[1, 2, 3], [3, 4, 4]]).astype(float) + assert A.shape == (2, 3) + b = np.array([1, 2]).astype(float) + f = umf_factor(A) + with pytest.raises(ValueError, match="must be square"): + f.solve(b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_dense(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = np.array([1], dtype=dtype) + x = UMFFactor(singleton_A).factorize().solve(b) + assert_allclose(x, b) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_singleton_sparse(dtype): + singleton_A = sparse.csc_array([[1]], dtype=dtype) + b = sparse.coo_array([1], dtype=dtype) + x = UMFFactor(singleton_A).factorize().solve(b) + assert_allclose(x.toarray(), b.toarray()) + + +@pytest.mark.parametrize("itype", ITYPES) +def test_itype_1D(davis_example_qr, itype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + x = umf_solve(A, b) + assert isinstance(x, sparse.coo_array) + assert x.coords[0].dtype == itype + + +@pytest.mark.parametrize("itype", ITYPES) +def test_itype_2D(davis_example_qr, itype): + A = davis_example_qr + A.indptr = A.indptr.astype(itype) + A.indices = A.indices.astype(itype) + N = A.shape[0] + K = 3 # arbitrary number of rhs + s = np.arange(1, N + 1, dtype=A.dtype) + data = np.array([i * s for i in range(1, K + 1)]).T + expect_x = sparse.csc_array(data, dtype=A.dtype) + b = A @ expect_x + x = umf_solve(A, b) + assert isinstance(x, sparse.csc_array) + assert x.indptr.dtype == itype + assert x.indices.dtype == itype + + +def test_exactly_singular(davis_example_qr): + A = davis_example_qr.todok() + A.setdiag(A.diagonal() + 1.0) # make non-singular + + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A exactly singular + s = -3 + A[:, s] = 0.0 + A[s, :] = 0.0 + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + + with pytest.raises( + UMFPACKError, match="indefinite or singular to working precision" + ): + umf_solve(A, b) + + +def test_nearly_singular(davis_example_qr): + A = davis_example_qr.todok() + A.setdiag(A.diagonal() + 1.0) # make non-singular + + N = A.shape[0] + lam0 = la.eigvalsh(A.toarray()).min() + + # Make A nearly singular + A[:, -1] = 0.0 + A[-1, :] = 0.0 + A[-1, -1] = 0.5 * np.finfo(A.dtype).eps + A = A.tocsc() + + lam1 = la.eigvalsh(A.toarray()).min() + print(f"\nMin eigenvalue: {lam0:.2e} -> {lam1:.2e}\n") + + expect_x = sparse.coo_array(np.arange(1, N + 1, dtype=A.dtype)) + b = A @ expect_x + f = umf_factor(A, row_scale="none") # turn off scaling to trigger warning + with pytest.warns(UMFPACKSingularMatrixWarning, match="nearly singular"): + f.solve(b) + + +@pytest.mark.parametrize("A", test_As) +@pytest.mark.parametrize("K", [0, 1, 3], ids=lambda k: f"K={k}") +@pytest.mark.parametrize("is_sparse", [False, True], ids=["dense", "sparse"]) +def test_solve(A, K, is_sparse): + atol = 1e-12 + + # Build RHS + N = A.shape[0] + s = np.arange(1, N + 1, dtype=A.dtype) + + if K == 0: + data = s # (N,) + else: + data = np.array([i * s for i in range(1, K + 1)], dtype=A.dtype).T # (N, K) + + if is_sparse: + expect_x = sparse.coo_array(data, dtype=A.dtype) + else: + expect_x = np.asarray(data, dtype=A.dtype) + + # Solve the system + b = A @ expect_x + x = umf_solve(A, b) + + # Compare + if is_sparse: + assert_allclose(x.toarray(), expect_x.toarray(), atol=atol) + else: + assert_allclose(x, expect_x, atol=atol) + + +# Test solve on "real-world" matrices +def _load_problem(name): + """Load a matrix and RHS from a Matrix Market file.""" + data_path = Path(__file__).parent / "data" + matrix_file = data_path / f"{name}.mtx.gz" + + if not matrix_file.exists(): + raise FileNotFoundError(f"Matrix Market file {matrix_file} not found.") + + A = mmread(matrix_file, spmatrix=False).tocsc() + + # Possibly load RHS + rhs_file = data_path / f"{name}_rhs1.mtx.gz" + + if not rhs_file.exists(): + raise FileNotFoundError(f"Matrix Market file {rhs_file} not found.") + + b = mmread(rhs_file) + + return A, b + + +# TODO @pytest.mark.slow +@pytest.mark.parametrize("problem", ["well1033", "illc1033", "well1850", "illc1850"]) +def test_solve_real(problem): + A, b = _load_problem(problem) + # Solve the normal equations A^T A x = A^T b + ATA = (A.T @ A).tocsc() + ATb = A.T @ b + expect_x = np.linalg.lstsq(A.toarray(), b)[0] + x = umf_solve(ATA, ATb) + assert_allclose(x, expect_x, atol=1e-7) + + +# ----------------------------------------------------------------------------- +# Test Info and Control +# ----------------------------------------------------------------------------- +# Copied from umfpack.h, subject to change +CONTROL_DEFAULTS = { + "print_level": 1, + "dense_row": 0.2, + "dense_col": 0.2, + "pivot_tol": 0.1, + "sym_pivot_tol": 0.001, + "blas3_block_size": 32, + "alloc_init": 0.7, + "front_alloc_init": 0.5, + "ir_steps": 2, + "row_scale": "sum", # UMFPACK_SCALE_SUM + "strategy": "auto", # UMFPACK_STRATEGY_AUTO + "amd_dense": 10.0, # AMD_DEFAULT_DENSE + "fixQ": 0, + "aggressive": True, + "droptol": 0.0, + "ordering_method": "amd", # UMFPACK_ORDERING_AMD + "singletons": True, + "sym_thresh": 0.3, + "nnzdiag_thresh": 0.9, +} + + +def test_default_controls(): + c = UMFControl() + for key, expect_value in CONTROL_DEFAULTS.items(): + actual_value = getattr(c, key) + assert actual_value == expect_value, ( + f"Control '{key}': expected {expect_value}, got {actual_value}" + ) + + +def test_ir_steps(davis_example_qr): + A = davis_example_qr + A.setdiag(A.diagonal() + 1.0) # make non-singular + N_steps = 0 + c = UMFControl(ir_steps=N_steps) # arbitrary > default + f = umf_factor(A, control=c) + assert f.control.ir_steps == N_steps + expect_x = np.arange(1, A.shape[0] + 1, dtype=A.dtype) + b = A @ expect_x + x = f.solve(b) + assert_allclose(x, expect_x, atol=1e-15, strict=True) + print(f"{f.info.ir_attempted=}, {f.info.ir_attempted=}") + assert f.info.ir_attempted == N_steps + + +@pytest.mark.parametrize("scale", ["none", "sum", "max"]) +def test_row_scale(davis_example_qr, scale): + A = davis_example_qr + A.setdiag(A.diagonal() + 1.0) # make non-singular + f = UMFFactor(A) + # Row scaling can be done *after* symbolic, but *before* numeric + f.control.row_scale = scale + assert f.control.row_scale == scale + f.factorize() + assert f.info.was_scaled == scale + assert_LU_equals_A(f, A) + if scale in [None, "none"]: + assert_allclose(f.rscale, 1.0) + assert_allclose(f.L.diagonal(), 1.0) + + +ORDERINGS = ["none", "cholmod", "amd", "metis", "best", "metis_guard"] + + +@pytest.mark.parametrize("ordering", ORDERINGS) +def test_ordering(davis_example_qr, ordering): + A = davis_example_qr + A.setdiag(A.diagonal() + 1.0) # make non-singular + # Ordering must be done *before* symbolic factorization + f = umf_factor(A, ordering_method=ordering) + expect_x = np.arange(1, A.shape[0] + 1, dtype=A.dtype) + b = A @ expect_x + x = f.solve(b) + assert_LU_equals_A(f, A) + assert_allclose(x, expect_x, atol=1e-15, strict=True) + if ordering in [None, "none", "amd", "metis"]: + assert f.info.ordering_used == ordering + else: # ["cholmod", "best", "metis_guard"] + # May choose AMD or METIS + assert f.info.ordering_used in ["amd", "metis"] + + +# ============================================================================= +# ============================================================================= diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..854fa053 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,101 @@ +# Part of the scikit-sparse project. +# Copyright (C) 2025 Bernard Roesler. All rights reserved. +# See pyproject.toml for full author list and LICENSE.txt for license details. +# SPDX-License-Identifier: BSD-2-Clause +# +# ============================================================================= +# File: test_utils.py +# Created: 2025-08-12 21:32 +# ============================================================================= + +"""Test cases for utility functions in scikit-sparse.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from scipy import sparse + +from sksparse.utils import validate_csc_input + + +def test_1D_input(): + with pytest.raises(ValueError, match="Input must be 2D"): + validate_csc_input(np.arange(10)) + + +def test_nonsquare_require_square(): + with pytest.raises(ValueError, match="Input must be square"): + validate_csc_input(sparse.csc_array((3, 4)), require_square=True) + + +def test_ND_input(): + with pytest.raises(ValueError, match="Input must be 2D"): + validate_csc_input(np.empty((2, 3, 4))) + + +@pytest.mark.parametrize("matrix_type", ["dense", "csc", "coo", "csc_matrix"]) +def test_input_conversion(matrix_type): + A = sparse.csc_array(np.arange(12.0).reshape(3, 4)) + + match matrix_type: + case "dense": + A = A.toarray() + case "csc": + A = A.tocsc() + case "coo": + A = A.tocoo() + case "csc_matrix": + A = sparse.csc_matrix(A) + case _: + raise ValueError(f"Unknown matrix type: {matrix_type}") + + if matrix_type == "csc": + result, use_int32, out_itype = validate_csc_input(A) + else: + with pytest.warns( + sparse.SparseEfficiencyWarning, match="not in CSC array format" + ): + result, use_int32, out_itype = validate_csc_input(A) + + assert isinstance(result, sparse.csc_array) + assert_array_equal( + result.toarray(), A.toarray() if sparse.issparse(A) else A, strict=True + ) + assert use_int32 + assert out_itype == np.int32 + + +DTYPES = [ + bool, + int, + float, + complex, + np.bool_, + np.int8, + np.int16, + np.int32, + np.int64, + np.float32, + np.float64, + np.complex64, + np.complex128, +] + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_data_types(dtype): + A = sparse.csc_array(np.arange(12).reshape(3, 4)).astype(dtype) + result, use_int32, out_itype = validate_csc_input(A) + assert isinstance(result, sparse.csc_array) + + # bools and ints are coerced to at least float32 + if np.issubdtype(dtype, np.bool_) or np.issubdtype(dtype, np.integer): + expected_dtype = np.result_type(dtype, np.float32) + else: + expected_dtype = dtype + + assert result.dtype == expected_dtype + assert use_int32 + assert out_itype == np.int32 + assert use_int32 + assert out_itype == np.int32